Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
facbbd6
Add Tau
StephenButtolph Dec 4, 2025
7a061e5
wip
StephenButtolph Dec 4, 2025
71fa659
wip
StephenButtolph Dec 4, 2025
f5637c4
wip
StephenButtolph Dec 4, 2025
8b038bf
Move BeforeBlock and AfterBlock into gastime
StephenButtolph Dec 5, 2025
af596f7
Replace full blocks with headers in hook.Points
StephenButtolph Dec 5, 2025
178724f
Update gas target immediately after block execution
StephenButtolph Dec 5, 2025
19c3fb9
Add test
StephenButtolph Dec 5, 2025
3b7e488
reduce comments
StephenButtolph Dec 5, 2025
6851d5a
Update hook.Points
StephenButtolph Dec 5, 2025
0d8cb33
Merged
StephenButtolph Dec 5, 2025
50fffca
Merged
StephenButtolph Dec 5, 2025
2db7585
worstcase.State compiling
StephenButtolph Dec 5, 2025
d612654
nits
StephenButtolph Dec 5, 2025
d8e67f3
merged
StephenButtolph Dec 8, 2025
b4450bb
ok
StephenButtolph Dec 8, 2025
f34665a
nit
StephenButtolph Dec 8, 2025
0373d0a
lint?
StephenButtolph Dec 8, 2025
94e1fb9
lint
StephenButtolph Dec 8, 2025
5c4a6e1
Update test
StephenButtolph Dec 9, 2025
16badd0
Add genesis block target override
StephenButtolph Dec 9, 2025
abe0d6d
merged
StephenButtolph Dec 9, 2025
8a8b9e2
address comments
StephenButtolph Dec 9, 2025
2fd4865
merged
StephenButtolph Dec 10, 2025
9a0d4b0
wip
StephenButtolph Dec 10, 2025
537bc52
re-push execution file
StephenButtolph Dec 10, 2025
0e2c83e
merged
StephenButtolph Dec 10, 2025
02cbc85
nits
StephenButtolph Dec 10, 2025
ad66e07
Default to max gas target
StephenButtolph Dec 10, 2025
2339b16
Merge branch 'main' into StephenButtolph/update-target-sooner
StephenButtolph Dec 10, 2025
73f2f6c
Merge branch 'StephenButtolph/update-target-sooner' into StephenButto…
StephenButtolph Dec 10, 2025
c09eec4
Merge branch 'StephenButtolph/fix-gas-target-changes' into StephenBut…
StephenButtolph Dec 10, 2025
cb79047
Merge branch 'main' into StephenButtolph/update-target-sooner
StephenButtolph Dec 10, 2025
f206ac4
Merge branch 'StephenButtolph/update-target-sooner' into StephenButto…
StephenButtolph Dec 10, 2025
b3aa8ea
Merge branch 'StephenButtolph/fix-gas-target-changes' into StephenBut…
StephenButtolph Dec 10, 2025
1956975
wip
StephenButtolph Dec 10, 2025
67171b5
lint
StephenButtolph Dec 10, 2025
45008cf
Merge branch 'StephenButtolph/worstcase' of github.com:ava-labs/strev…
StephenButtolph Dec 10, 2025
66ff5c5
Merge branch 'StephenButtolph/fix-gas-target-changes' into StephenBut…
StephenButtolph Dec 10, 2025
f0df77c
wip
StephenButtolph Dec 10, 2025
2e71b8e
wip
StephenButtolph Dec 10, 2025
1c7b5b1
wip
StephenButtolph Dec 10, 2025
cac97ef
nits
StephenButtolph Dec 10, 2025
3ec8531
lint
StephenButtolph Dec 10, 2025
4edd091
merged
StephenButtolph Dec 11, 2025
fa76f25
lint
StephenButtolph Dec 11, 2025
b6ef7f8
minor refactor
StephenButtolph Dec 11, 2025
21d3e54
update comments + errors
StephenButtolph Dec 11, 2025
14c26f6
Cleanup tests
StephenButtolph Dec 11, 2025
3db949d
nit
StephenButtolph Dec 11, 2025
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
29 changes: 29 additions & 0 deletions hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,39 @@ package hook

import (
"github.com/ava-labs/avalanchego/vms/components/gas"
"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/core/state"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/params"
"github.com/holiman/uint256"

"github.com/ava-labs/strevm/intmath"
saeparams "github.com/ava-labs/strevm/params"
)

// AccountDebit includes an amount that an account should have debited,
// along with the nonce used to debit the account.
type AccountDebit struct {
Nonce uint64
Amount uint256.Int
}

// Op is an operation that can be applied to state during the execution of a
// block.
type Op struct {
// Gas consumed by this operation
Gas gas.Gas
// GasPrice this operation is willing to spend
GasPrice uint256.Int
// From specifies the set of accounts and the authorization of funds to be
// removed from the accounts.
From map[common.Address]AccountDebit
// To specifies the amount to increase account balances by. These funds are
// not necessarily tied to the funds consumed in the From field. The sum of
// the To amounts may even exceed the sum of the From amounts.
To map[common.Address]uint256.Int
}

// Points define user-injected hook points.
type Points interface {
// GasTargetAfter returns the gas target that should go into effect
Expand All @@ -28,6 +53,10 @@ type Points interface {
// For example, if the block timestamp is 10.75 seconds and the gas rate is
// 100 gas/second, then this method should return 75 gas.
SubSecondBlockTime(gasRate gas.Gas, h *types.Header) gas.Gas
// ExtraBlockOps returns operations outside of the normal EVM state changes
// to perform while executing the block. These operations will be performed
// during both worst-case and actual execution.
ExtraBlockOps(*types.Block) ([]Op, error)
// BeforeExecutingBlock is called immediately prior to executing the block.
BeforeExecutingBlock(params.Rules, *state.StateDB, *types.Block) error
// AfterExecutingBlock is called immediately after executing the block.
Expand Down
5 changes: 5 additions & 0 deletions hook/hookstest/stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ func (s *Stub) SubSecondBlockTime(gas.Gas, *types.Header) gas.Gas {
return s.SubSecondTime
}

// ExtraBlockOps always returns no operations and nil.
func (*Stub) ExtraBlockOps(*types.Block) ([]hook.Op, error) {
return nil, nil
}

// BeforeExecutingBlock is a no-op that always returns nil.
func (*Stub) BeforeExecutingBlock(params.Rules, *state.StateDB, *types.Block) error {
return nil
Expand Down
14 changes: 10 additions & 4 deletions params/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
// [Streaming Asynchronous Execution]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution
package params

// Lambda is the denominator for computing the minimum gas consumed per
// transaction. For a transaction with gas limit `g`, the minimum consumption is
// ceil(g/Lambda).
const Lambda = 2
const (
// Lambda is the denominator for computing the minimum gas consumed per
// transaction. For a transaction with gas limit `g`, the minimum
// consumption is ceil(g/Lambda).
Lambda = 2

// Tau is the number of seconds after a block has finished executing before
// it is settled.
Tau = 5
)
270 changes: 270 additions & 0 deletions worstcase/state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

// Package worstcase provides the worst-case balance and nonce tracking needed
// to safely include transactions that are guaranteed to be valid during
// execution.
package worstcase

import (
"errors"
"fmt"
"math"
"math/big"

"github.com/ava-labs/avalanchego/vms/components/gas"
"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/core"
"github.com/ava-labs/libevm/core/state"
"github.com/ava-labs/libevm/core/txpool"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/params"
"github.com/holiman/uint256"

"github.com/ava-labs/strevm/gastime"
"github.com/ava-labs/strevm/hook"
saeparams "github.com/ava-labs/strevm/params"
)

// State tracks the worst-case gas price and account state as operations are
// executed.
//
// Usage of the [State] should follow the pattern:
// 1. [State.StartBlock] for each block to be included.
// 2. [State.GasLimit] and [State.BaseFee] to query the block's parameters.
// 3. [State.ApplyTx] or [State.Apply] for each transaction or operation to
// include in the block.
// 4. [State.FinishBlock] to finalize the block's gas time.
// 5. Repeat from step 1 for the next block.
type State struct {
hooks hook.Points
config *params.ChainConfig

db *state.StateDB
clock *gastime.Time

qSize, blockSize, maxBlockSize gas.Gas

baseFee *uint256.Int
curr *types.Header
rules params.Rules
signer types.Signer
}

// NewState constructs a new worst-case state.
//
// The [state.StateDB] MUST be opened at the state immediately following the
// last-executed block upon which the worst-case state is built. Similarly, the
// [gastime.Time] MUST be a clone of the gas clock at the same point. The
// StateDB will only be used as a scratchpad for tracking accounts, and will NOT
// be committed.
func NewState(
hooks hook.Points,
config *params.ChainConfig,
db *state.StateDB,
fromExecTime *gastime.Time,
) *State {
return &State{
hooks: hooks,
config: config,
db: db,
clock: fromExecTime,
}
}

const rateToMaxBlockSize = saeparams.Tau * saeparams.Lambda

var (
errNonConsecutiveBlocks = errors.New("non-consecutive block numbers")
errQueueFull = errors.New("queue full")
)

// StartBlock updates the worst-case state to the beginning of the provided
// block.
//
// It is not necessary for [types.Header.GasLimit] or [types.Header.BaseFee] to
// be set.
//
// If the queue is too full to accept another block, an error is returned.
func (s *State) StartBlock(hdr *types.Header) error {
if c := s.curr; c != nil {
if num, next := c.Number.Uint64(), hdr.Number.Uint64(); next != num+1 {
return fmt.Errorf("%w: %d then %d", errNonConsecutiveBlocks, num, next)
}
}

s.clock.BeforeBlock(s.hooks, hdr)
s.blockSize = 0

const (
maxQSizeMultiplier = 2
// In order to avoid overflow when calculating the queue size, we cap
// the maximum gas rate to a safe value.
//
// This follows from:
// maxBlockSize = maxRate * Tau * Lambda
// maxQSizeInStart = maxQSizeMultiplier * maxBlockSize
// maxQSizeInFinish = maxQSizeInStart + maxBlockSize
maxRate gas.Gas = math.MaxUint64 / rateToMaxBlockSize / (maxQSizeMultiplier + 1)
)
r := min(s.clock.Rate(), maxRate)
s.maxBlockSize = r * rateToMaxBlockSize
if maxQSize := maxQSizeMultiplier * s.maxBlockSize; s.qSize > maxQSize {
return fmt.Errorf("%w: current size %d exceeds maximum size %d", errQueueFull, s.qSize, maxQSize)
}

s.baseFee = s.clock.BaseFee()

s.curr = types.CopyHeader(hdr)
s.curr.GasLimit = uint64(s.maxBlockSize)
s.curr.BaseFee = s.baseFee.ToBig()

// For both rules and signer, we MUST use the block's timestamp, not the
// execution clock's, otherwise we might enable an upgrade too early.
s.rules = s.config.Rules(hdr.Number, true, hdr.Time)
s.signer = types.MakeSigner(s.config, hdr.Number, hdr.Time)
return nil
}

// GasLimit returns the available gas limit for the current block.
func (s *State) GasLimit() uint64 {
return uint64(s.maxBlockSize)
}

// BaseFee returns the worst-case base fee for the current block.
func (s *State) BaseFee() *uint256.Int {
return s.baseFee
}

type (
// AccountDebit includes an amount that an account should have debited,
// along with the nonce used to debit the account.
AccountDebit = hook.AccountDebit
// Op is an operation that can be applied to a [State].
Op = hook.Op
)

var errCostOverflow = errors.New("Cost() overflows uint256")

// ApplyTx validates the transaction both intrinsically and in the context of
// worst-case gas assumptions of all previous operations. This provides an upper
// bound on the total cost of the transaction such that a nil error returned by
// ApplyTx guarantees that the sender of the transaction will have sufficient
// balance to cover its costs if consensus accepts the same operation set
// (and order) as was applied.
//
// If the transaction can not be applied, an error is returned and the state is
// not modified.
//
// TODO: Consider exporting txToOp and expecting users to call Apply directly.
func (s *State) ApplyTx(tx *types.Transaction) error {
opts := &txpool.ValidationOptions{
Config: s.config,
Accept: 0 |
1<<types.LegacyTxType |
1<<types.AccessListTxType |
1<<types.DynamicFeeTxType,
MaxSize: math.MaxUint, // TODO(arr4n)
MinTip: big.NewInt(0),
}
if err := txpool.ValidateTransaction(tx, s.curr, s.signer, opts); err != nil {
return fmt.Errorf("validating transaction: %w", err)
}

op, err := txToOp(s.signer, tx)
if err != nil {
return fmt.Errorf("converting transaction to operation: %w", err)
}
return s.Apply(op)
}

func txToOp(signer types.Signer, tx *types.Transaction) (Op, error) {
from, err := types.Sender(signer, tx)
if err != nil {
return Op{}, fmt.Errorf("determining sender: %w", err)
}

var gasPrice uint256.Int
if overflow := gasPrice.SetFromBig(tx.GasFeeCap()); overflow {
return Op{}, core.ErrFeeCapVeryHigh
}
var amount uint256.Int
if overflow := amount.SetFromBig(tx.Cost()); overflow {
return Op{}, errCostOverflow
}
return Op{
Gas: gas.Gas(tx.Gas()),
GasPrice: gasPrice,
From: map[common.Address]hook.AccountDebit{
from: {
Nonce: tx.Nonce(),
Amount: amount,
},
},
// To is not populated here because this transaction may revert.
}, nil
}

// Apply attempts to apply the operation to this state.
//
// If the operation can not be applied, an error is returned and the state is
// not modified.
//
// Operations are invalid if:
//
// - The operation consumes more gas than the block has available.
// - The operation specifies too low of a gas price.
// - The operation is from an account with an incorrect or invalid nonce.
// - The operation is from an account with an insufficient balance.
func (s *State) Apply(o Op) error {
// ----- Gas -----
if o.Gas > s.maxBlockSize-s.blockSize {
return core.ErrGasLimitReached
}

// ----- GasPrice -----
if o.GasPrice.Cmp(s.baseFee) < 0 {
return core.ErrFeeCapTooLow
}

// ----- From -----
for from, ad := range o.From {
switch nonce, next := ad.Nonce, s.db.GetNonce(from); {
case nonce < next:
return fmt.Errorf("%w: %d < %d", core.ErrNonceTooLow, nonce, next)
case nonce > next:
return fmt.Errorf("%w: %d > %d", core.ErrNonceTooHigh, nonce, next)
case next == math.MaxUint64:
return core.ErrNonceMax
}

if bal := s.db.GetBalance(from); ad.Amount.Cmp(bal) > 0 {
return core.ErrInsufficientFunds
}
}

// ----- Inclusion -----
s.blockSize += o.Gas
executeOp(s.db, o)
return nil
}

func executeOp(db *state.StateDB, o Op) {
for from, ad := range o.From {
db.SetNonce(from, ad.Nonce+1)
db.SubBalance(from, &ad.Amount)
}

for to, amount := range o.To {
db.AddBalance(to, &amount)
}
}

// FinishBlock advances the [gastime.Time] in preparation for the next block.
func (s *State) FinishBlock() error {
if err := s.clock.AfterBlock(s.blockSize, s.hooks, s.curr); err != nil {
return fmt.Errorf("finishing block gas time update: %w", err)
}
s.qSize += s.blockSize
return nil
}
Loading