From 7c0e6e43140014af60b19145b43a8bc649b95bce Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Sun, 8 Jun 2025 20:48:52 +0100 Subject: [PATCH 01/42] feat: `worstcase` package for validity checking --- gastime/gastime.go | 8 +- gastime/gastime_test.go | 2 +- proxytime/proxytime.go | 4 +- proxytime/proxytime_test.go | 2 +- worstcase/txinclusion.go | 146 ++++++++++++++++++++++++++++++++ worstcase/txinclusion_test.go | 151 ++++++++++++++++++++++++++++++++++ 6 files changed, 305 insertions(+), 8 deletions(-) create mode 100644 worstcase/txinclusion.go create mode 100644 worstcase/txinclusion_test.go diff --git a/gastime/gastime.go b/gastime/gastime.go index bbb8b27..fe4c298 100644 --- a/gastime/gastime.go +++ b/gastime/gastime.go @@ -64,10 +64,10 @@ func (tm *Time) Tick(g gas.Gas) { tm.excess += quo } -// Tick is equivalent to [proxytime.Time.FastForward] except that it may also -// update the gas excess. -func (tm *Time) FastForward(to uint64) { - sec, frac := tm.Time.FastForward(to) +// FastForwardTo is equivalent to [proxytime.Time.FastForwardTo] except that it +// may also update the gas excess. +func (tm *Time) FastForwardTo(to uint64) { + sec, frac := tm.Time.FastForwardTo(to) if sec == 0 && frac.Numerator == 0 { return } diff --git a/gastime/gastime_test.go b/gastime/gastime_test.go index 351a257..958334d 100644 --- a/gastime/gastime_test.go +++ b/gastime/gastime_test.go @@ -170,7 +170,7 @@ func TestExcess(t *testing.T) { case ff > 0 && tk > 0: t.Fatalf("Bad test setup (%q) only FastForward() or Tick() before", s.desc) case ff > 0: - tm.FastForward(ff) + tm.FastForwardTo(ff) case tk > 0: tm.Tick(tk) } diff --git a/proxytime/proxytime.go b/proxytime/proxytime.go index 824e011..cadc71f 100644 --- a/proxytime/proxytime.go +++ b/proxytime/proxytime.go @@ -65,10 +65,10 @@ func (tm *Time[D]) Tick(d D) { tm.fraction %= tm.hertz } -// FastForward sets the time to the specified Unix timestamp if it is in the +// FastForwardTo sets the time to the specified Unix timestamp if it is in the // future, returning the integer and fraction number of seconds by which the // time was advanced. -func (tm *Time[D]) FastForward(to uint64) (uint64, FractionalSecond[D]) { +func (tm *Time[D]) FastForwardTo(to uint64) (uint64, FractionalSecond[D]) { if to <= tm.seconds { return 0, FractionalSecond[D]{0, tm.hertz} } diff --git a/proxytime/proxytime_test.go b/proxytime/proxytime_test.go index 9cd2d70..6dfec6a 100644 --- a/proxytime/proxytime_test.go +++ b/proxytime/proxytime_test.go @@ -156,7 +156,7 @@ func TestFastForward(t *testing.T) { for _, s := range steps { tm.Tick(s.tickBefore) - gotSec, gotFrac := tm.FastForward(s.ffTo) + gotSec, gotFrac := tm.FastForwardTo(s.ffTo) assert.Equal(t, s.wantSec, gotSec) assert.Equal(t, s.wantFrac, gotFrac) diff --git a/worstcase/txinclusion.go b/worstcase/txinclusion.go new file mode 100644 index 0000000..0ef0116 --- /dev/null +++ b/worstcase/txinclusion.go @@ -0,0 +1,146 @@ +// Package worstcase is a pessimist, always seeing the glass as half empty. But +// where others see full glasses and opportunities, package worstcase sees DoS +// vulnerabilities. +package worstcase + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/state" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/params" + "github.com/ava-labs/strevm/gastime" + "github.com/holiman/uint256" +) + +type TransactionIncluder struct { + db *state.StateDB + + config *params.ChainConfig + rules params.Rules + signer types.Signer + + clock *gastime.Time + + qLength, maxQLength, blockSize, maxBlockSize gas.Gas +} + +func NewTxIncluder( + db *state.StateDB, + config *params.ChainConfig, + fromHeight *big.Int, + fromBlockTime uint64, + fromExecTime *gastime.Time, + maxQueueSeconds, maxBlockSeconds uint64, +) *TransactionIncluder { + t := fromExecTime + inc := &TransactionIncluder{ + db: db, + config: config, + clock: t, + maxQLength: t.Rate() * gas.Gas(maxQueueSeconds), + maxBlockSize: t.Rate() * gas.Gas(maxBlockSeconds), + } + inc.StartBlock(fromHeight, fromBlockTime) + return inc +} + +func (inc *TransactionIncluder) StartBlock(num *big.Int, timestamp uint64) { + inc.clock.Tick(inc.blockSize) + inc.blockSize = 0 + + inc.clock.FastForwardTo(timestamp) + + // 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. + inc.rules = inc.config.Rules(num, true, timestamp) + inc.signer = types.MakeSigner(inc.config, num, timestamp) +} + +var ( + ErrQueueTooFull = errors.New("queue too full") + ErrBlockTooFull = errors.New("block too full") +) + +func (inc *TransactionIncluder) Include(tx *types.Transaction) error { + switch g := gas.Gas(tx.Gas()); { + case g > inc.maxQLength-inc.qLength: + return ErrQueueTooFull + case g > inc.maxBlockSize-inc.blockSize: + return ErrBlockTooFull + } + if err := checkStateless(tx, inc.rules); err != nil { + return err + } + + from, err := types.Sender(inc.signer, tx) + if err != nil { + return fmt.Errorf("determining sender: %w", err) + } + + // ----- Nonce ----- + switch nonce, next := tx.Nonce(), inc.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+1 < next: + return core.ErrNonceMax + } + + // ----- Balance covers worst-case gas cost + tx value ----- + price := uint256.NewInt(uint64(inc.clock.Price())) + if cap, min := tx.GasFeeCap(), price.ToBig(); cap.Cmp(min) < 0 { + return core.ErrFeeCapTooLow + } + gasCost := new(uint256.Int).Mul( + price, + uint256.NewInt(tx.Gas()), + ) + txCost := new(uint256.Int).Add( + gasCost, + uint256.MustFromBig(tx.Value()), + ) + if bal := inc.db.GetBalance(from); bal.Cmp(txCost) < 0 { + return core.ErrInsufficientFunds + } + + // ----- Inclusion ----- + g := gas.Gas(tx.Gas()) + inc.qLength += g + inc.blockSize += g + + inc.db.SetNonce(from, inc.db.GetNonce(from)+1) + inc.db.SubBalance(from, txCost) + + return nil +} + +func checkStateless(tx *types.Transaction, rules params.Rules) error { + contractCreation := tx.To() == nil + + // ----- Init-code length ----- + if contractCreation && len(tx.Data()) > params.MaxInitCodeSize { + return core.ErrMaxInitCodeSizeExceeded + } + + // ----- Intrinsic gas ----- + intrinsic, err := core.IntrinsicGas( + tx.Data(), tx.AccessList(), + contractCreation, rules.IsHomestead, + rules.IsIstanbul, // EIP-2028 + rules.IsShanghai, // EIP-3869 + ) + if err != nil { + return fmt.Errorf("calculating intrinsic gas requirement: %w", err) + } + if tx.Gas() < intrinsic { + return fmt.Errorf("%w: %d < %d", core.ErrIntrinsicGas, tx.Gas(), intrinsic) + } + + return nil +} diff --git a/worstcase/txinclusion_test.go b/worstcase/txinclusion_test.go new file mode 100644 index 0000000..07a12c4 --- /dev/null +++ b/worstcase/txinclusion_test.go @@ -0,0 +1,151 @@ +package worstcase + +import ( + "math/big" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/state" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/crypto" + "github.com/ava-labs/libevm/params" + "github.com/ava-labs/strevm/gastime" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" +) + +func newDB(tb testing.TB) *state.StateDB { + tb.Helper() + db, err := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) + require.NoError(tb, err, "state.New([empty root], [fresh memory db])") + return db +} + +func newTxIncluder(tb testing.TB) (*TransactionIncluder, *state.StateDB) { + tb.Helper() + db := newDB(tb) + return NewTxIncluder( + db, + params.TestChainConfig, + big.NewInt(0), 0, gastime.New(0, 1e6, 0), 5, 2, + ), db +} + +func TestNonContextualTransactionRejection(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err, "libevm/crypto.GenerateKey()") + eoa := crypto.PubkeyToAddress(key.PublicKey) + + tests := []struct { + name string + stateSetup func(*state.StateDB) + tx types.TxData + wantErrIs error + }{ + { + name: "nil_err", + stateSetup: func(db *state.StateDB) { + db.SetBalance(eoa, uint256.NewInt(params.TxGas)) + }, + tx: &types.LegacyTx{ + Nonce: 0, + Gas: params.TxGas, + GasPrice: big.NewInt(1), + To: &common.Address{}, + }, + wantErrIs: nil, + }, + { + name: "nonce_too_low", + stateSetup: func(db *state.StateDB) { + db.SetNonce(eoa, 1) + }, + tx: &types.LegacyTx{ + Nonce: 0, + Gas: params.TxGas, + To: &common.Address{}, + }, + wantErrIs: core.ErrNonceTooLow, + }, + { + name: "nonce_too_high", + stateSetup: func(db *state.StateDB) { + db.SetNonce(eoa, 1) + }, + tx: &types.LegacyTx{ + Nonce: 2, + Gas: params.TxGas, + To: &common.Address{}, + }, + wantErrIs: core.ErrNonceTooHigh, + }, + { + name: "exceed_max_init_code_size", + tx: &types.LegacyTx{ + To: nil, // i.e. contract creation + Data: make([]byte, params.MaxInitCodeSize+1), + }, + wantErrIs: core.ErrMaxInitCodeSizeExceeded, + }, + { + name: "not_cover_intrinsic_gas", + tx: &types.LegacyTx{ + Gas: params.TxGas - 1, + To: &common.Address{}, + }, + wantErrIs: core.ErrIntrinsicGas, + }, + { + name: "gas_price_too_low", + tx: &types.LegacyTx{ + Gas: params.TxGas, + GasPrice: big.NewInt(0), + To: &common.Address{}, + }, + wantErrIs: core.ErrFeeCapTooLow, + }, + { + name: "insufficient_funds_for_gas", + stateSetup: func(db *state.StateDB) { + db.SetBalance(eoa, uint256.NewInt(params.TxGas-1)) + }, + tx: &types.LegacyTx{ + Gas: params.TxGas, + GasPrice: big.NewInt(1), + To: &common.Address{}, + }, + wantErrIs: core.ErrInsufficientFunds, + }, + { + name: "insufficient_funds_for_gas_and_value", + stateSetup: func(db *state.StateDB) { + db.SetBalance(eoa, uint256.NewInt(params.TxGas)) + }, + tx: &types.LegacyTx{ + Gas: params.TxGas, + GasPrice: big.NewInt(1), + Value: big.NewInt(1), + To: &common.Address{}, + }, + wantErrIs: core.ErrInsufficientFunds, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + inc, db := newTxIncluder(t) + if tt.stateSetup != nil { + tt.stateSetup(db) + } + tx := types.MustSignNewTx(key, types.LatestSigner(inc.config), tt.tx) + require.ErrorIs(t, inc.Include(tx), tt.wantErrIs) + }) + } +} + +func TestContextualTransactionRejection(t *testing.T) { + // TODO(arr4n) test rejection of transactions in the context of other + // transactions, e.g. exhausting balance, gas price increasing, etc. +} From 2d2b12ef9acf3b2a0bd93a7b37b10f8ca11dbc5b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 9 Jun 2025 11:54:51 -0400 Subject: [PATCH 02/42] wip --- builder.go | 1 + scripts/build.sh | 11 +++++++++++ vm.go | 6 ++++-- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100755 scripts/build.sh diff --git a/builder.go b/builder.go index a3935b7..d4a02b1 100644 --- a/builder.go +++ b/builder.go @@ -19,6 +19,7 @@ import ( ) func (vm *VM) buildBlock(ctx context.Context, timestamp uint64, parent *Block) (*Block, error) { + timestamp = max(timestamp, parent.Time()) block, err := sink.FromPriorityMutex( ctx, vm.mempool, sink.MaxPriority, func(_ <-chan sink.Priority, pool *queue.Priority[*pendingTx]) (*Block, error) { diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..218ff88 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +VMID="sr96zN6VeXJ4y5fY5EFziQrPSiy4LJPUMJGQsSLEW4t5bHWPw" +BINARY_PATH="$HOME/.avalanchego/plugins/$VMID" +echo "Building SAE EVM at $BINARY_PATH" +go build -o "$BINARY_PATH" "./rpc/"*.go +echo "Built SAE EVM at $BINARY_PATH" diff --git a/vm.go b/vm.go index 0d5f902..206385b 100644 --- a/vm.go +++ b/vm.go @@ -27,6 +27,8 @@ import ( "github.com/ava-labs/strevm/queue" ) +var VMID = ids.ID{'s', 't', 'r', 'e', 'v', 'm'} + // VM implements Streaming Asynchronous Execution (SAE) of EVM blocks. It // implements all [adaptor.ChainVM] methods except for `Initialize()`, which // MUST be handled by a harness implementation that provides the final @@ -187,8 +189,8 @@ func (vm *VM) Version(context.Context) (string, error) { } const ( - HTTPHandlerKey = "sae_http" - WSHandlerKey = "sae_ws" + HTTPHandlerKey = "/sae/http" + WSHandlerKey = "/sae/ws" ) func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { From 068d02a577a83c131f725f178df5e1e03d788eb1 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 9 Jun 2025 13:36:12 -0400 Subject: [PATCH 03/42] Make things work --- block.go | 4 ++-- builder.go | 27 ++++++++++++++++++++++----- exec.go | 4 ++-- mempool.go | 6 +++--- rpc/rpc.go | 23 ++++++++++++++++++++++- validity.go | 2 +- vm.go | 2 +- 7 files changed, 53 insertions(+), 15 deletions(-) diff --git a/block.go b/block.go index 2adb5ff..cf196a6 100644 --- a/block.go +++ b/block.go @@ -75,7 +75,7 @@ func (vm *VM) AcceptBlock(ctx context.Context, b *Block) error { vm.last.settled.Store(b.lastSettled) vm.last.accepted.Store(b) - vm.logger().Debug( + vm.logger().Info( "Accepted block", zap.Uint64("height", b.Height()), zap.Stringer("hash", b.Hash()), @@ -86,7 +86,7 @@ func (vm *VM) AcceptBlock(ctx context.Context, b *Block) error { // GC! prune := func(b *Block) { delete(bm, b.Hash()) - vm.logger().Debug( + vm.logger().Info( "Pruning settled block", zap.Stringer("hash", b.Hash()), zap.Uint64("number", b.NumberU64()), diff --git a/builder.go b/builder.go index d4a02b1..7c44304 100644 --- a/builder.go +++ b/builder.go @@ -8,6 +8,7 @@ import ( "slices" "github.com/arr4n/sink" + snowcommon "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/state" @@ -23,13 +24,26 @@ func (vm *VM) buildBlock(ctx context.Context, timestamp uint64, parent *Block) ( block, err := sink.FromPriorityMutex( ctx, vm.mempool, sink.MaxPriority, func(_ <-chan sink.Priority, pool *queue.Priority[*pendingTx]) (*Block, error) { - return vm.buildBlockWithCandidateTxs(timestamp, parent, pool) + block, err := vm.buildBlockWithCandidateTxs(timestamp, parent, pool) + + // TODO: This shouldn't be done immediately, there should be some + // retry delay if block building failed. + if pool.Len() > 0 { + select { + case vm.toEngine <- snowcommon.PendingTxs: + default: + p := snowcommon.PendingTxs + vm.logger().Info(fmt.Sprintf("%T(%s) dropped", p, p)) + } + } + + return block, err }, ) if err != nil { return nil, err } - vm.logger().Debug( + vm.logger().Info( "Built block", zap.Uint64("timestamp", timestamp), zap.Uint64("height", block.Height()), @@ -54,7 +68,7 @@ func (vm *VM) buildBlockWithCandidateTxs(timestamp uint64, parent *Block, candid ) return nil, fmt.Errorf("%w: parent %#x at time %d", errWaitingForExecution, parent.Hash(), timestamp) } - vm.logger().Debug( + vm.logger().Info( "Settlement candidate", zap.Uint64("timestamp", timestamp), zap.Stringer("parent", parent.Hash()), @@ -174,6 +188,9 @@ TxLoop: validity, err := checker.addTxToQueue(candidate.txAndSender) if err != nil { + // TODO: It is not acceptable to return an error here, as all + // transactions that have been removed from the mempool will be + // dropped and never included. return nil, 0, err } @@ -189,7 +206,7 @@ TxLoop: case delayTx, queueFull: delayed = append(delayed, candidate) - vm.logger().Debug( + vm.logger().Info( "Delaying transaction until later block-building", zap.Stringer("hash", tx.Hash()), ) @@ -198,7 +215,7 @@ TxLoop: } case discardTx: - vm.logger().Debug( + vm.logger().Info( "Discarding transaction", zap.Stringer("hash", tx.Hash()), ) diff --git a/exec.go b/exec.go index f896fcd..71efd7f 100644 --- a/exec.go +++ b/exec.go @@ -274,7 +274,7 @@ func (e *executor) execute(ctx context.Context, b *Block) error { header := types.CopyHeader(b.Header()) header.BaseFee = e.gasClock.params.baseFee().ToBig() - e.log.Debug( + e.log.Info( "Executing accepted block", zap.Uint64("height", b.Height()), zap.Uint64("timestamp", header.Time), @@ -343,7 +343,7 @@ func (e *executor) execute(ctx context.Context, b *Block) error { b.executed.Store(true) e.lastExecuted.Store(b) - e.log.Debug( + e.log.Info( "Block execution complete", zap.Uint64("height", b.Height()), zap.Time("gas_time", e.gasClock.asTime()), diff --git a/mempool.go b/mempool.go index 9da22f8..53c8bf7 100644 --- a/mempool.go +++ b/mempool.go @@ -50,7 +50,7 @@ func (vm *VM) receiveTxs(preempt <-chan sink.Priority, pool *queue.Priority[*pen from, err := types.Sender(vm.currSigner(), tx) if err != nil { - vm.logger().Debug( + vm.logger().Info( "Dropped tx due to failed sender recovery", zap.Stringer("hash", tx.Hash()), zap.Error(err), @@ -64,7 +64,7 @@ func (vm *VM) receiveTxs(preempt <-chan sink.Priority, pool *queue.Priority[*pen }, timePriority: time.Now(), }) - vm.logger().Debug( + vm.logger().Info( "New tx in mempool", zap.Stringer("hash", tx.Hash()), zap.Stringer("from", from), @@ -75,7 +75,7 @@ func (vm *VM) receiveTxs(preempt <-chan sink.Priority, pool *queue.Priority[*pen case vm.toEngine <- snowcommon.PendingTxs: default: p := snowcommon.PendingTxs - vm.logger().Debug(fmt.Sprintf("%T(%s) dropped", p, p)) + vm.logger().Info(fmt.Sprintf("%T(%s) dropped", p, p)) } } } diff --git a/rpc/rpc.go b/rpc/rpc.go index 3c80128..8ee0479 100644 --- a/rpc/rpc.go +++ b/rpc/rpc.go @@ -5,13 +5,34 @@ import ( "fmt" "os" + "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/rpcchainvm" + "github.com/ava-labs/libevm/core/types" sae "github.com/ava-labs/strevm" "github.com/ava-labs/strevm/adaptor" ) +const ( + TargetGasPerSecond = 1_000_000 + GasCapacityPerSecond = 2 * TargetGasPerSecond + ExcessConversionConstant = 87 * TargetGasPerSecond +) + +type hooks struct{} + +func (h *hooks) UpdateGasParams(parent *types.Block, p *sae.GasParams) { + *p = sae.GasParams{ + T: TargetGasPerSecond, + R: GasCapacityPerSecond, + Price: gas.CalculatePrice(1, p.Excess, ExcessConversionConstant), + Excess: p.Excess, + } +} + func main() { - vm := adaptor.Convert(new(sae.SinceGenesis)) + vm := adaptor.Convert(&sae.SinceGenesis{ + Hooks: &hooks{}, + }) if err := rpcchainvm.Serve(context.Background(), vm); err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/validity.go b/validity.go index 4fbac2f..d3bd536 100644 --- a/validity.go +++ b/validity.go @@ -58,7 +58,7 @@ func (vc *validityChecker) addTxToQueue(t txAndSender) (txValidity, error) { vc.rules.IsShanghai, // EIP-3869 ) if err != nil { - vc.log.Debug( + vc.log.Info( "Unable to determine intrinsic gas", zap.Stringer("tx_hash", t.tx.Hash()), zap.Error(err), diff --git a/vm.go b/vm.go index 206385b..cd8edc0 100644 --- a/vm.go +++ b/vm.go @@ -176,7 +176,7 @@ func (vm *VM) SetState(ctx context.Context, state snow.State) error { } func (vm *VM) Shutdown(ctx context.Context) error { - vm.logger().Debug("Shutting down VM") + vm.logger().Info("Shutting down VM") close(vm.quit) vm.blocks.Close() From c531b1f12382ed004b9b75c8e75acec62132957a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 9 Jun 2025 13:56:22 -0400 Subject: [PATCH 04/42] more fixes --- builder.go | 10 +++++++++- exec.go | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/builder.go b/builder.go index 7c44304..1adc6a8 100644 --- a/builder.go +++ b/builder.go @@ -52,7 +52,10 @@ func (vm *VM) buildBlock(ctx context.Context, timestamp uint64, parent *Block) ( return block, nil } -var errWaitingForExecution = errors.New("waiting for execution when building block") +var ( + errWaitingForExecution = errors.New("waiting for execution when building block") + errBlockWithoutChanges = errors.New("block contains no changes") +) func (vm *VM) buildBlockWithCandidateTxs(timestamp uint64, parent *Block, candidateTxs queue.Queue[*pendingTx]) (*Block, error) { if timestamp < parent.Time() { @@ -96,6 +99,11 @@ func (vm *VM) buildBlockWithCandidateTxs(timestamp uint64, parent *Block, candid return nil, err } + if gasUsed == 0 && len(txs) == 0 { + vm.logger().Info("Blocks must either settle or include transactions") + return nil, fmt.Errorf("%w: parent %#x at time %d", errBlockWithoutChanges, parent.Hash(), timestamp) + } + b := vm.newBlock(types.NewBlock( &types.Header{ ParentHash: parent.Hash(), diff --git a/exec.go b/exec.go index 71efd7f..23b8b14 100644 --- a/exec.go +++ b/exec.go @@ -256,7 +256,7 @@ func (c *gasClock) isAfter(timestamp uint64) bool { } func (c *gasClock) asTime() time.Time { - nsec, _ /*remainder*/ := intmath.MulDiv(c.consumed, c.params.R, 1e9) + nsec, _ /*remainder*/ := intmath.MulDiv(c.consumed, 1e9, c.params.R) return time.Unix(int64(c.time), int64(nsec)) } From 3323052f3c8ad15a2b8c9b6dcf07264178ebbcda Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 9 Jun 2025 14:54:10 -0400 Subject: [PATCH 05/42] nit --- block.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/block.go b/block.go index cf196a6..ede6e8b 100644 --- a/block.go +++ b/block.go @@ -61,7 +61,7 @@ func (vm *VM) AcceptBlock(ctx context.Context, b *Block) error { } } if err := b.writeLastSettledNumber(batch); err != nil { - return nil + return err } if err := batch.Write(); err != nil { return err From 8c13c2798073fa8084e4f7f8b294c9b8153daa50 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 9 Jun 2025 15:42:46 -0400 Subject: [PATCH 06/42] fix FATAL in bootstrapping --- block.go | 12 ++++++++++++ exec.go | 16 ++++++++++++++++ vm.go | 9 ++++++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/block.go b/block.go index ede6e8b..cc5dca9 100644 --- a/block.go +++ b/block.go @@ -9,6 +9,7 @@ import ( "time" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" @@ -47,6 +48,17 @@ func (vm *VM) AcceptBlock(ctx context.Context, b *Block) error { return err } + // When the chain is bootstrapping, avalanchego expects to be able to call + // `Verify` and `Accept` in a loop over blocks. Reporting an error during + // either `Verify` or `Accept` is considered FATAL during this process. + // Therefore, we must ensure that avalanchego does not get too far ahead of + // the execution thread and FATAL during block Verification. + if vm.consensusState.Get() == snow.Bootstrapping { + if err := vm.exec.awaitEmpty(ctx); err != nil { + return err + } + } + batch := vm.db.NewBatch() rawdb.WriteCanonicalHash(batch, b.Hash(), b.NumberU64()) rawdb.WriteTxLookupEntriesByBlock(batch, b.Block) // i.e. canonical tx inclusion diff --git a/exec.go b/exec.go index 23b8b14..e3f482f 100644 --- a/exec.go +++ b/exec.go @@ -161,9 +161,25 @@ func (e *executor) processQueue() { ) return } + + // This is a hack to signal anyone blocked on awaitEmpty + _ = e.queue.UseThenSignal(ctx, func(*queue.FIFO[*Block]) error { return nil }) } } +func (e *executor) awaitEmpty(ctx context.Context) error { + // This isn't implemented correctly, but I don't know how it is supposed to + // work. We should have a mutex with 2 conditions, one where the queue is + // empty (and the processing thread is not executing), the other where the + // queue isn't empty. + return e.queue.Wait(ctx, + func(q *queue.FIFO[*Block]) bool { + return q.Len() == 0 + }, + func(*queue.FIFO[*Block]) error { return nil }, + ) +} + type executionScratchSpace struct { snaps *snapshot.Tree statedb *state.StateDB diff --git a/vm.go b/vm.go index cd8edc0..16a37d5 100644 --- a/vm.go +++ b/vm.go @@ -15,6 +15,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" snowcommon "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/version" "github.com/ava-labs/libevm/common" @@ -40,9 +41,10 @@ type VM struct { hooks Hooks now func() time.Time - blocks sink.Mutex[blockMap] - preference atomic.Pointer[Block] - last last + consensusState utils.Atomic[snow.State] + blocks sink.Mutex[blockMap] + preference atomic.Pointer[Block] + last last db ethdb.Database @@ -172,6 +174,7 @@ func (vm *VM) Disconnected(ctx context.Context, nodeID ids.NodeID) error { } func (vm *VM) SetState(ctx context.Context, state snow.State) error { + vm.consensusState.Set(state) return nil } From 9c068d5a7b0cc627a3e39c5abf56fc8b45e55cf1 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 11 Jun 2025 16:33:24 -0400 Subject: [PATCH 07/42] Do not expose unexecuted txs over the RPC --- rpc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc.go b/rpc.go index 38465b6..e43bae9 100644 --- a/rpc.go +++ b/rpc.go @@ -220,7 +220,7 @@ func (b *ethAPIBackend) GetReceipts(ctx context.Context, hash common.Hash) (type func (b *ethAPIBackend) GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error) { tx, blockHash, blockNum, index := rawdb.ReadTransaction(b.vm.db, txHash) - if tx == nil { + if tx == nil || blockNum > b.vm.last.executed.Load().NumberU64() { return false, nil, common.Hash{}, 0, 0, nil } return true, tx, blockHash, blockNum, index, nil From 203fa4d8607425f62f7d1bd743d10668516414bd Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 12 Jun 2025 13:19:26 -0400 Subject: [PATCH 08/42] Remove unused block deletion --- block.go | 1 - 1 file changed, 1 deletion(-) diff --git a/block.go b/block.go index 05d7593..a86bbf7 100644 --- a/block.go +++ b/block.go @@ -94,7 +94,6 @@ func (vm *VM) AcceptBlock(ctx context.Context, b *Block) error { } func (vm *VM) RejectBlock(ctx context.Context, b *Block) error { - rawdb.DeleteBlock(vm.db, b.Hash(), b.NumberU64()) // TODO(arr4n) add the transactions back to the mempool if necessary. return nil } From f34d354628e35c4b430721b982b64d936f5a7358 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 17 Jun 2025 12:49:28 -0400 Subject: [PATCH 09/42] Add block context verification --- adaptor/adaptor.go | 10 +++++++++- block.go | 34 ++++++++++++++++++++++++++++++++++ hook/hook.go | 13 +++++++++++++ integration_test.go | 9 +++++++++ plugin/plugin.go | 9 +++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) diff --git a/adaptor/adaptor.go b/adaptor/adaptor.go index b808016..dfb0e12 100644 --- a/adaptor/adaptor.go +++ b/adaptor/adaptor.go @@ -23,7 +23,9 @@ type ChainVM[BP BlockProperties] interface { ParseBlock(context.Context, []byte) (BP, error) BuildBlock(context.Context) (BP, error) - // Transferred from [snowman.Block]. + // Transferred from [snowman.Block] and [block.WithVerifyContext]. + ShouldVerifyBlockWithContext(context.Context, BP) (bool, error) + VerifyBlockWithContext(context.Context, *block.Context, BP) error VerifyBlock(context.Context, BP) error AcceptBlock(context.Context, BP) error RejectBlock(context.Context, BP) error @@ -83,6 +85,12 @@ func (vm adaptor[BP]) BuildBlock(ctx context.Context) (snowman.Block, error) { return vm.newBlock(vm.ChainVM.BuildBlock(ctx)) } +func (b Block[BP]) ShouldVerifyWithContext(ctx context.Context) (bool, error) { + return b.vm.ShouldVerifyBlockWithContext(ctx, b.b) +} +func (b Block[BP]) VerifyWithContext(ctx context.Context, blockContext *block.Context) error { + return b.vm.VerifyBlockWithContext(ctx, blockContext, b.b) +} func (b Block[BP]) Verify(ctx context.Context) error { return b.vm.VerifyBlock(ctx, b.b) } func (b Block[BP]) Accept(ctx context.Context) error { return b.vm.AcceptBlock(ctx, b.b) } func (b Block[BP]) Reject(ctx context.Context) error { return b.vm.RejectBlock(ctx, b.b) } diff --git a/block.go b/block.go index 5abbc8b..eee33a5 100644 --- a/block.go +++ b/block.go @@ -7,6 +7,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" @@ -103,6 +104,39 @@ func (vm *VM) RejectBlock(ctx context.Context, b *blocks.Block) error { return nil } +func (vm *VM) ShouldVerifyBlockWithContext(ctx context.Context, b *blocks.Block) (bool, error) { + return vm.hooks.ShouldVerifyBlockContext(ctx, b.Block) +} + +func (vm *VM) VerifyBlockWithContext(ctx context.Context, blockContext *block.Context, b *blocks.Block) error { + // Verify that the block is valid within the provided context. This must be + // called even if the block was previously verified because the context may + // be different. + if err := vm.hooks.VerifyBlockContext(ctx, blockContext, b.Block); err != nil { + return err + } + + blockHash := b.Hash() + var previouslyVerified bool + // TODO(StephenButtolph): In the concurrency model this VM is implementing, + // is this usage of the block map a logical race? The consensus engine + // currently only ever calls VerifyWithContext and VerifyBlock on a single + // thread, so the actual behavior seems correct. + err := vm.blocks.Use(ctx, func(bm blockMap) error { + _, previouslyVerified = bm[blockHash] + return nil + }) + if err != nil { + return err + } + // If [VM.VerifyBlock] has already returned nil, we do not need to re-verify + // the block. + if previouslyVerified { + return nil + } + return vm.VerifyBlock(ctx, b) +} + func (vm *VM) VerifyBlock(ctx context.Context, b *blocks.Block) error { parent, err := vm.GetBlock(ctx, ids.ID(b.ParentHash())) if err != nil { diff --git a/hook/hook.go b/hook/hook.go index 6d549d6..ed54f35 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -5,6 +5,9 @@ package hook import ( + "context" + + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/strevm/gastime" @@ -13,6 +16,16 @@ import ( // Points define user-injected hook points. type Points interface { GasTarget(parent *types.Block) gas.Gas + // ShouldVerifyBlockContext reports whether this block is only valid against + // a subset of proposervm block contexts. If there are contexts where this + // block could be invalid, this function must return true. + // + // This function must be deterministic for a given block. + ShouldVerifyBlockContext(ctx context.Context, block *types.Block) (bool, error) + // VerifyBlockContext verifies that the block is valid within the provided + // block context. This is not expected to fully verify the block, only that + // the block is not invalid with the provided context. + VerifyBlockContext(ctx context.Context, blockContext *block.Context, block *types.Block) error } // BeforeBlock is intended to be called before processing a block, with the gas diff --git a/integration_test.go b/integration_test.go index 230dbd0..ba452e8 100644 --- a/integration_test.go +++ b/integration_test.go @@ -17,6 +17,7 @@ import ( "time" "github.com/arr4n/sink" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/vms/components/gas" ethereum "github.com/ava-labs/libevm" @@ -50,6 +51,14 @@ func (h *stubHooks) GasTarget(parent *types.Block) gas.Gas { return h.T } +func (*stubHooks) ShouldVerifyBlockContext(context.Context, *types.Block) (bool, error) { + return false, nil +} + +func (*stubHooks) VerifyBlockContext(context.Context, *block.Context, *types.Block) error { + return nil +} + func TestIntegrationWrapAVAX(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() diff --git a/plugin/plugin.go b/plugin/plugin.go index 6c65c37..3981be9 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -5,6 +5,7 @@ import ( "fmt" "os" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/rpcchainvm" "github.com/ava-labs/libevm/core/types" @@ -22,6 +23,14 @@ func (*hooks) GasTarget(parent *types.Block) gas.Gas { return TargetGasPerSecond } +func (*hooks) ShouldVerifyBlockContext(context.Context, *types.Block) (bool, error) { + return false, nil +} + +func (*hooks) VerifyBlockContext(context.Context, *block.Context, *types.Block) error { + return nil +} + func main() { vm := adaptor.Convert(&sae.SinceGenesis{ Hooks: &hooks{}, From 3bc28f34017826a8f96ea66dbbb845a9d8cbac60 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 17 Jun 2025 13:24:13 -0400 Subject: [PATCH 10/42] Add block building stub --- adaptor/adaptor.go | 11 +++++++++++ vm.go | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/adaptor/adaptor.go b/adaptor/adaptor.go index dfb0e12..0a3aabf 100644 --- a/adaptor/adaptor.go +++ b/adaptor/adaptor.go @@ -13,6 +13,12 @@ import ( "github.com/ava-labs/avalanchego/snow/engine/snowman/block" ) +// Enforce optional interfaces +var ( + _ block.BuildBlockWithContextChainVM = adaptor[BlockProperties]{} + _ block.WithVerifyContext = Block[BlockProperties]{} +) + // ChainVM defines the functionality required in order to be converted into a // Snowman VM. See the respective methods on [block.ChainVM] and [snowman.Block] // for detailed documentation. @@ -21,6 +27,7 @@ type ChainVM[BP BlockProperties] interface { GetBlock(context.Context, ids.ID) (BP, error) ParseBlock(context.Context, []byte) (BP, error) + BuildBlockWithContext(context.Context, *block.Context) (BP, error) BuildBlock(context.Context) (BP, error) // Transferred from [snowman.Block] and [block.WithVerifyContext]. @@ -81,6 +88,10 @@ func (vm adaptor[BP]) ParseBlock(ctx context.Context, blockBytes []byte) (snowma return vm.newBlock(vm.ChainVM.ParseBlock(ctx, blockBytes)) } +func (vm adaptor[BP]) BuildBlockWithContext(ctx context.Context, blockContext *block.Context) (snowman.Block, error) { + return vm.newBlock(vm.ChainVM.BuildBlockWithContext(ctx, blockContext)) +} + func (vm adaptor[BP]) BuildBlock(ctx context.Context) (snowman.Block, error) { return vm.newBlock(vm.ChainVM.BuildBlock(ctx)) } diff --git a/vm.go b/vm.go index 845407c..96977e4 100644 --- a/vm.go +++ b/vm.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" snowcommon "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/version" @@ -230,6 +231,14 @@ func (vm *VM) ParseBlock(ctx context.Context, blockBytes []byte) (*blocks.Block, return vm.newBlock(b, nil, nil) } +func (vm *VM) BuildBlockWithContext(ctx context.Context, _ *block.Context) (*blocks.Block, error) { + // TODO(StephenButtolph): Figure out how we want to support block building + // hooks. Contextual block building is needed to support Warp messages. + // + // We additionally need to include atomic txs in the block for the C-chain. + return vm.BuildBlock(ctx) +} + func (vm *VM) BuildBlock(ctx context.Context) (*blocks.Block, error) { return vm.buildBlock(ctx, uint64(vm.now().Unix()), vm.preference.Load()) } From 64741e230525ca70e3e77a5c4f79d732cf71243f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 17 Jun 2025 14:52:32 -0400 Subject: [PATCH 11/42] Add VerifyBlockAncestors hook --- block.go | 30 ++++++++++++++++++++++++++++++ hook/hook.go | 15 +++++++++++++++ integration_test.go | 5 +++++ plugin/plugin.go | 5 +++++ 4 files changed, 55 insertions(+) diff --git a/block.go b/block.go index eee33a5..a6a5a26 100644 --- a/block.go +++ b/block.go @@ -12,6 +12,7 @@ import ( "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/strevm/blocks" + "github.com/ava-labs/strevm/hook" "github.com/ava-labs/strevm/queue" "go.uber.org/zap" ) @@ -143,6 +144,11 @@ func (vm *VM) VerifyBlock(ctx context.Context, b *blocks.Block) error { return fmt.Errorf("block parent %#x not found (presumed height %d)", b.ParentHash(), b.Height()-1) } + ancestors := iterateUntilSettled(parent) + if err := vm.hooks.VerifyBlockAncestors(ctx, b.Block, ancestors); err != nil { + return err + } + signer := vm.signer(b.NumberU64(), b.Time()) txs := b.Transactions() // This starts a concurrent, background pre-computation of the results of @@ -181,3 +187,27 @@ func (vm *VM) VerifyBlock(ctx context.Context, b *blocks.Block) error { return nil }) } + +// iterateUntilSettled returns an iterator which starts at the provided block +// and iterates up to but not including the most recently settled block. +// +// If the provided block is settled, then the returned iterator is empty. +func iterateUntilSettled(from *blocks.Block) hook.BlockIterator { + return func(yield func(*types.Block) bool) { + for { + next := from.ParentBlock() + // If the next block is nil, then the current block is settled. + if next == nil { + return + } + + // If the person iterating over this iterator broke out of the loop, + // we must not call yield again. + if !yield(from.Block) { + return + } + + from = next + } + } +} diff --git a/hook/hook.go b/hook/hook.go index ed54f35..f25286f 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -13,6 +13,16 @@ import ( "github.com/ava-labs/strevm/gastime" ) +// BlockIterator enables iteration over blocks. +// +// Example usage is: +// +// var it BlockIterator +// for block := range it { +// ... +// } +type BlockIterator func(yield func(*types.Block) bool) + // Points define user-injected hook points. type Points interface { GasTarget(parent *types.Block) gas.Gas @@ -26,6 +36,11 @@ type Points interface { // block context. This is not expected to fully verify the block, only that // the block is not invalid with the provided context. VerifyBlockContext(ctx context.Context, blockContext *block.Context, block *types.Block) error + // VerifyBlockAncestors verifies that the block has a valid chain of + // ancestors. This is not expected to fully verify the block, only that the + // block's ancestors are compatible. The ancestor iterator iterates from the + // parent of block up to but not including the most recently settled block. + VerifyBlockAncestors(ctx context.Context, block *types.Block, ancestors BlockIterator) error } // BeforeBlock is intended to be called before processing a block, with the gas diff --git a/integration_test.go b/integration_test.go index ba452e8..b7c51de 100644 --- a/integration_test.go +++ b/integration_test.go @@ -29,6 +29,7 @@ import ( "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/rpc" "github.com/ava-labs/strevm/blocks" + "github.com/ava-labs/strevm/hook" "github.com/ava-labs/strevm/queue" "github.com/ava-labs/strevm/weth" "github.com/google/go-cmp/cmp" @@ -59,6 +60,10 @@ func (*stubHooks) VerifyBlockContext(context.Context, *block.Context, *types.Blo return nil } +func (*stubHooks) VerifyBlockAncestors(context.Context, *types.Block, hook.BlockIterator) error { + return nil +} + func TestIntegrationWrapAVAX(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() diff --git a/plugin/plugin.go b/plugin/plugin.go index 3981be9..8526785 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -11,6 +11,7 @@ import ( "github.com/ava-labs/libevm/core/types" sae "github.com/ava-labs/strevm" "github.com/ava-labs/strevm/adaptor" + "github.com/ava-labs/strevm/hook" ) const ( @@ -31,6 +32,10 @@ func (*hooks) VerifyBlockContext(context.Context, *block.Context, *types.Block) return nil } +func (*hooks) VerifyBlockAncestors(context.Context, *types.Block, hook.BlockIterator) error { + return nil +} + func main() { vm := adaptor.Convert(&sae.SinceGenesis{ Hooks: &hooks{}, From 25242cc2db2fe57e19de5dd58081259fd581789a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 17 Jun 2025 14:54:39 -0400 Subject: [PATCH 12/42] Cleanup the blocks map on reject --- block.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/block.go b/block.go index a6a5a26..1ce0103 100644 --- a/block.go +++ b/block.go @@ -102,7 +102,10 @@ func (vm *VM) AcceptBlock(ctx context.Context, b *blocks.Block) error { func (vm *VM) RejectBlock(ctx context.Context, b *blocks.Block) error { // TODO(arr4n) add the transactions back to the mempool if necessary. - return nil + return vm.blocks.Use(ctx, func(bm blockMap) error { + delete(bm, b.Hash()) + return nil + }) } func (vm *VM) ShouldVerifyBlockWithContext(ctx context.Context, b *blocks.Block) (bool, error) { From ecff93893a690fe8f07d10233c7c14cc2a24374d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 17 Jun 2025 15:02:49 -0400 Subject: [PATCH 13/42] Fix multiple iterations --- block.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/block.go b/block.go index 1ce0103..d2f2b69 100644 --- a/block.go +++ b/block.go @@ -197,8 +197,10 @@ func (vm *VM) VerifyBlock(ctx context.Context, b *blocks.Block) error { // If the provided block is settled, then the returned iterator is empty. func iterateUntilSettled(from *blocks.Block) hook.BlockIterator { return func(yield func(*types.Block) bool) { + // Do not modify the `from` variable to support multiple iterations. + current := from for { - next := from.ParentBlock() + next := current.ParentBlock() // If the next block is nil, then the current block is settled. if next == nil { return @@ -206,11 +208,11 @@ func iterateUntilSettled(from *blocks.Block) hook.BlockIterator { // If the person iterating over this iterator broke out of the loop, // we must not call yield again. - if !yield(from.Block) { + if !yield(current.Block) { return } - from = next + current = next } } } From d5397bc81369022ab4109ccbd7a8b6a683e8ffd8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 17 Jun 2025 15:51:14 -0400 Subject: [PATCH 14/42] Use stdlib rather than custom type --- block.go | 4 ++-- hook/hook.go | 13 ++----------- integration_test.go | 4 ++-- plugin/plugin.go | 4 ++-- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/block.go b/block.go index d2f2b69..011b657 100644 --- a/block.go +++ b/block.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "iter" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" @@ -12,7 +13,6 @@ import ( "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/strevm/blocks" - "github.com/ava-labs/strevm/hook" "github.com/ava-labs/strevm/queue" "go.uber.org/zap" ) @@ -195,7 +195,7 @@ func (vm *VM) VerifyBlock(ctx context.Context, b *blocks.Block) error { // and iterates up to but not including the most recently settled block. // // If the provided block is settled, then the returned iterator is empty. -func iterateUntilSettled(from *blocks.Block) hook.BlockIterator { +func iterateUntilSettled(from *blocks.Block) iter.Seq[*types.Block] { return func(yield func(*types.Block) bool) { // Do not modify the `from` variable to support multiple iterations. current := from diff --git a/hook/hook.go b/hook/hook.go index f25286f..af4cfbf 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -6,6 +6,7 @@ package hook import ( "context" + "iter" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/vms/components/gas" @@ -13,16 +14,6 @@ import ( "github.com/ava-labs/strevm/gastime" ) -// BlockIterator enables iteration over blocks. -// -// Example usage is: -// -// var it BlockIterator -// for block := range it { -// ... -// } -type BlockIterator func(yield func(*types.Block) bool) - // Points define user-injected hook points. type Points interface { GasTarget(parent *types.Block) gas.Gas @@ -40,7 +31,7 @@ type Points interface { // ancestors. This is not expected to fully verify the block, only that the // block's ancestors are compatible. The ancestor iterator iterates from the // parent of block up to but not including the most recently settled block. - VerifyBlockAncestors(ctx context.Context, block *types.Block, ancestors BlockIterator) error + VerifyBlockAncestors(ctx context.Context, block *types.Block, ancestors iter.Seq[*types.Block]) error } // BeforeBlock is intended to be called before processing a block, with the gas diff --git a/integration_test.go b/integration_test.go index b7c51de..485bd50 100644 --- a/integration_test.go +++ b/integration_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "flag" + "iter" "math" "math/big" "net/http/httptest" @@ -29,7 +30,6 @@ import ( "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/rpc" "github.com/ava-labs/strevm/blocks" - "github.com/ava-labs/strevm/hook" "github.com/ava-labs/strevm/queue" "github.com/ava-labs/strevm/weth" "github.com/google/go-cmp/cmp" @@ -60,7 +60,7 @@ func (*stubHooks) VerifyBlockContext(context.Context, *block.Context, *types.Blo return nil } -func (*stubHooks) VerifyBlockAncestors(context.Context, *types.Block, hook.BlockIterator) error { +func (*stubHooks) VerifyBlockAncestors(context.Context, *types.Block, iter.Seq[*types.Block]) error { return nil } diff --git a/plugin/plugin.go b/plugin/plugin.go index 8526785..37d7901 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "iter" "os" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" @@ -11,7 +12,6 @@ import ( "github.com/ava-labs/libevm/core/types" sae "github.com/ava-labs/strevm" "github.com/ava-labs/strevm/adaptor" - "github.com/ava-labs/strevm/hook" ) const ( @@ -32,7 +32,7 @@ func (*hooks) VerifyBlockContext(context.Context, *block.Context, *types.Block) return nil } -func (*hooks) VerifyBlockAncestors(context.Context, *types.Block, hook.BlockIterator) error { +func (*hooks) VerifyBlockAncestors(context.Context, *types.Block, iter.Seq[*types.Block]) error { return nil } From 4a82f047bad67ccf25d3eb9994a788405e1bf4ef Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 24 Jun 2025 14:01:57 -0400 Subject: [PATCH 15/42] wip --- block.go | 73 ++---- builder.go | 160 +++++++++---- go.mod | 4 - go.sum | 16 -- hook/hook.go | 63 ++++-- integration_test.go | 27 ++- plugin/plugin.go | 28 ++- vm.go | 10 +- worstcase/state.go | 213 ++++++++++++++++++ .../{txinclusion_test.go => state_test.go} | 4 +- worstcase/txinclusion.go | 185 --------------- 11 files changed, 439 insertions(+), 344 deletions(-) create mode 100644 worstcase/state.go rename worstcase/{txinclusion_test.go => state_test.go} (96%) delete mode 100644 worstcase/txinclusion.go diff --git a/block.go b/block.go index 102e6e8..dee1c6f 100644 --- a/block.go +++ b/block.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "iter" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" @@ -111,49 +110,24 @@ func (vm *VM) RejectBlock(ctx context.Context, b *blocks.Block) error { } func (vm *VM) ShouldVerifyBlockWithContext(ctx context.Context, b *blocks.Block) (bool, error) { - return vm.hooks.ShouldVerifyBlockContext(ctx, b.Block) + return true, nil } func (vm *VM) VerifyBlockWithContext(ctx context.Context, blockContext *block.Context, b *blocks.Block) error { - // Verify that the block is valid within the provided context. This must be - // called even if the block was previously verified because the context may - // be different. - if err := vm.hooks.VerifyBlockContext(ctx, blockContext, b.Block); err != nil { - return err - } - - blockHash := b.Hash() - var previouslyVerified bool - // TODO(StephenButtolph): In the concurrency model this VM is implementing, - // is this usage of the block map a logical race? The consensus engine - // currently only ever calls VerifyWithContext and VerifyBlock on a single - // thread, so the actual behavior seems correct. - err := vm.blocks.Use(ctx, func(bm blockMap) error { - _, previouslyVerified = bm[blockHash] - return nil - }) - if err != nil { - return err - } - // If [VM.VerifyBlock] has already returned nil, we do not need to re-verify - // the block. - if previouslyVerified { - return nil - } - return vm.VerifyBlock(ctx, b) + // TODO: This could be optimized to only verify this block once. + return vm.verifyBlock(ctx, blockContext, b) } func (vm *VM) VerifyBlock(ctx context.Context, b *blocks.Block) error { + return vm.verifyBlock(ctx, nil, b) +} + +func (vm *VM) verifyBlock(ctx context.Context, blockContext *block.Context, b *blocks.Block) error { parent, err := vm.GetBlock(ctx, ids.ID(b.ParentHash())) if err != nil { return fmt.Errorf("block parent %#x not found (presumed height %d)", b.ParentHash(), b.Height()-1) } - ancestors := iterateUntilSettled(parent) - if err := vm.hooks.VerifyBlockAncestors(ctx, b.Block, ancestors); err != nil { - return err - } - signer := vm.signer(b.NumberU64(), b.Time()) txs := b.Transactions() // This starts a concurrent, background pre-computation of the results of @@ -175,7 +149,12 @@ func (vm *VM) VerifyBlock(ctx context.Context, b *blocks.Block) error { }) } - bb, err := vm.buildBlockWithCandidateTxs(b.Time(), parent, candidates) + constructBlock, err := vm.hooks.ConstructBlockFromBlock(ctx, b.Block) + if err != nil { + return err + } + + bb, err := vm.buildBlockWithCandidateTxs(b.Time(), parent, candidates, blockContext, constructBlock) if err != nil { return err } @@ -192,29 +171,3 @@ func (vm *VM) VerifyBlock(ctx context.Context, b *blocks.Block) error { return nil }) } - -// iterateUntilSettled returns an iterator which starts at the provided block -// and iterates up to but not including the most recently settled block. -// -// If the provided block is settled, then the returned iterator is empty. -func iterateUntilSettled(from *blocks.Block) iter.Seq[*types.Block] { - return func(yield func(*types.Block) bool) { - // Do not modify the `from` variable to support multiple iterations. - current := from - for { - next := current.ParentBlock() - // If the next block is nil, then the current block is settled. - if next == nil { - return - } - - // If the person iterating over this iterator broke out of the loop, - // we must not call yield again. - if !yield(current.Block) { - return - } - - current = next - } - } -} diff --git a/builder.go b/builder.go index b7e2547..a1af5bc 100644 --- a/builder.go +++ b/builder.go @@ -4,26 +4,29 @@ import ( "context" "errors" "fmt" + "iter" "math/big" "slices" "github.com/arr4n/sink" snowcommon "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/libevm/core/state" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/strevm/blocks" + "github.com/ava-labs/strevm/hook" "github.com/ava-labs/strevm/intmath" "github.com/ava-labs/strevm/queue" "github.com/ava-labs/strevm/worstcase" "go.uber.org/zap" ) -func (vm *VM) buildBlock(ctx context.Context, timestamp uint64, parent *blocks.Block) (*blocks.Block, error) { +func (vm *VM) buildBlock(ctx context.Context, blockContext *block.Context, timestamp uint64, parent *blocks.Block) (*blocks.Block, error) { block, err := sink.FromPriorityMutex( ctx, vm.mempool, sink.MaxPriority, func(_ <-chan sink.Priority, pool *queue.Priority[*pendingTx]) (*blocks.Block, error) { - block, err := vm.buildBlockWithCandidateTxs(timestamp, parent, pool) + block, err := vm.buildBlockWithCandidateTxs(timestamp, parent, pool, blockContext, vm.hooks.ConstructBlock) // TODO: This shouldn't be done immediately, there should be some // retry delay if block building failed. @@ -57,7 +60,13 @@ var ( errNoopBlock = errors.New("block does not settle state nor include transactions") ) -func (vm *VM) buildBlockWithCandidateTxs(timestamp uint64, parent *blocks.Block, candidateTxs queue.Queue[*pendingTx]) (*blocks.Block, error) { +func (vm *VM) buildBlockWithCandidateTxs( + timestamp uint64, + parent *blocks.Block, + candidateTxs queue.Queue[*pendingTx], + blockContext *block.Context, + constructBlock hook.ConstructBlock, +) (*blocks.Block, error) { if timestamp < parent.Time() { return nil, fmt.Errorf("block at time %d before parent at %d", timestamp, parent.Time()) } @@ -80,48 +89,35 @@ func (vm *VM) buildBlockWithCandidateTxs(timestamp uint64, parent *blocks.Block, zap.Uint64("block_time", toSettle.Time()), ) - var ( - receipts []types.Receipts - gasUsed uint64 + ethB, err := vm.buildBlockOnHistory( + toSettle, + parent, + timestamp, + candidateTxs, + blockContext, + constructBlock, ) - // We can never concurrently build and accept a block on the same parent, - // which guarantees that `parent` won't be settled, so the [Block] invariant - // means that `parent.lastSettled != nil`. - for _, b := range parent.IfChildSettles(toSettle) { - brs := b.Receipts() - receipts = append(receipts, brs) - for _, r := range brs { - gasUsed += r.GasUsed - } - } - - txs, gasLimit, err := vm.buildBlockOnHistory(toSettle, parent, timestamp, candidateTxs) if err != nil { return nil, err } - if gasUsed == 0 && len(txs) == 0 { + + // TODO: Check if the block contains transactions (potentially atomic) + if ethB.GasUsed() == 0 { vm.logger().Info("Blocks must either settle or include transactions") return nil, fmt.Errorf("%w: parent %#x at time %d", errNoopBlock, parent.Hash(), timestamp) } - ethB := types.NewBlock( - &types.Header{ - ParentHash: parent.Hash(), - Root: toSettle.PostExecutionStateRoot(), - Number: new(big.Int).Add(parent.Number(), big.NewInt(1)), - GasLimit: uint64(gasLimit), - GasUsed: gasUsed, - Time: timestamp, - BaseFee: nil, // TODO(arr4n) - }, - txs, nil, /*uncles*/ - slices.Concat(receipts...), - trieHasher(), - ) return blocks.New(ethB, parent, toSettle, vm.logger()) } -func (vm *VM) buildBlockOnHistory(lastSettled, parent *blocks.Block, timestamp uint64, candidateTxs queue.Queue[*pendingTx]) (_ types.Transactions, _ gas.Gas, retErr error) { +func (vm *VM) buildBlockOnHistory( + lastSettled, + parent *blocks.Block, + timestamp uint64, + candidateTxs queue.Queue[*pendingTx], + blockContext *block.Context, + constructBlock hook.ConstructBlock, +) (_ *types.Block, retErr error) { var history []*blocks.Block for b := parent; b.ID() != lastSettled.ID(); b = b.ParentBlock() { history = append(history, b) @@ -130,7 +126,7 @@ func (vm *VM) buildBlockOnHistory(lastSettled, parent *blocks.Block, timestamp u sdb, err := state.New(lastSettled.PostExecutionStateRoot(), vm.exec.StateCache(), nil) if err != nil { - return nil, 0, err + return nil, err } checker := worstcase.NewTxIncluder( @@ -142,14 +138,35 @@ func (vm *VM) buildBlockOnHistory(lastSettled, parent *blocks.Block, timestamp u for _, b := range history { checker.StartBlock(b.Header(), vm.hooks.GasTarget(b.ParentBlock().Block)) for _, tx := range b.Transactions() { - if err := checker.Include(tx); err != nil { + if err := checker.ApplyTx(tx); err != nil { vm.logger().Error( "Transaction not included when replaying history", zap.Stringer("block", b.Hash()), zap.Stringer("tx", tx.Hash()), zap.Error(err), ) - return nil, 0, err + return nil, err + } + } + + extraOps, err := vm.hooks.ExtraBlockOperations(context.TODO(), b.Block) + if err != nil { + vm.logger().Error( + "Unable to extract extra block operations when replaying history", + zap.Stringer("block", b.Hash()), + zap.Error(err), + ) + return nil, err + } + for i, op := range extraOps { + if err := checker.Apply(op); err != nil { + vm.logger().Error( + "Operation not applied when replaying history", + zap.Stringer("block", b.Hash()), + zap.Int("index", i), + zap.Error(err), + ) + return nil, err } } } @@ -179,7 +196,7 @@ func (vm *VM) buildBlockOnHistory(lastSettled, parent *blocks.Block, timestamp u candidate := candidateTxs.Pop() tx := candidate.tx - switch err := checker.Include(tx); { + switch err := checker.ApplyTx(tx); { case err == nil: include = append(include, candidate) @@ -198,7 +215,22 @@ func (vm *VM) buildBlockOnHistory(lastSettled, parent *blocks.Block, timestamp u // TODO: It is not acceptable to return an error here, as all // transactions that have been removed from the mempool will be // dropped and never included. - return nil, 0, err + return nil, err + } + } + + var ( + receipts []types.Receipts + gasUsed uint64 + ) + // We can never concurrently build and accept a block on the same parent, + // which guarantees that `parent` won't be settled, so the [Block] invariant + // means that `parent.lastSettled != nil`. + for _, b := range parent.IfChildSettles(lastSettled) { + brs := b.Receipts() + receipts = append(receipts, brs) + for _, r := range brs { + gasUsed += r.GasUsed } } @@ -213,10 +245,26 @@ func (vm *VM) buildBlockOnHistory(lastSettled, parent *blocks.Block, timestamp u gasLimit += gas.Gas(tx.tx.Gas()) } - // TODO(arr4n) return the base fee too, available from the [gastime.Time] in - // `checker`. - - return txs, gasLimit, nil + header := &types.Header{ + ParentHash: parent.Hash(), + Root: lastSettled.PostExecutionStateRoot(), + Number: new(big.Int).Add(parent.Number(), big.NewInt(1)), + GasLimit: uint64(gasLimit), + GasUsed: gasUsed, + Time: timestamp, + BaseFee: nil, // TODO(arr4n) + } + ancestors := iterateUntilSettled(parent) + return constructBlock( + context.TODO(), + blockContext, + header, + parent.Block.Header(), + ancestors, + checker, + txs, + slices.Concat(receipts...), + ) } func errIsOneOf(err error, targets ...error) bool { @@ -232,3 +280,29 @@ func (vm *VM) lastBlockToSettleAt(timestamp uint64, parent *blocks.Block) (*bloc settleAt := intmath.BoundedSubtract(timestamp, stateRootDelaySeconds, vm.last.synchronous.time) return blocks.LastToSettleAt(settleAt, parent) } + +// iterateUntilSettled returns an iterator which starts at the provided block +// and iterates up to but not including the most recently settled block. +// +// If the provided block is settled, then the returned iterator is empty. +func iterateUntilSettled(from *blocks.Block) iter.Seq[*types.Block] { + return func(yield func(*types.Block) bool) { + // Do not modify the `from` variable to support multiple iterations. + current := from + for { + next := current.ParentBlock() + // If the next block is nil, then the current block is settled. + if next == nil { + return + } + + // If the person iterating over this iterator broke out of the loop, + // we must not call yield again. + if !yield(current.Block) { + return + } + + current = next + } + } +} diff --git a/go.mod b/go.mod index 03ed74c..98ab725 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,6 @@ require ( github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/ethereum/c-kzg-4844 v0.4.0 // indirect - github.com/fatih/structtag v1.2.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect @@ -60,7 +59,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/huin/goupnp v1.3.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/klauspost/compress v1.15.15 // indirect github.com/kr/pretty v0.3.1 // indirect @@ -79,8 +77,6 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/status-im/keycard-go v0.2.0 // indirect github.com/supranational/blst v0.3.13 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect diff --git a/go.sum b/go.sum index b5eae6c..b122439 100644 --- a/go.sum +++ b/go.sum @@ -11,10 +11,6 @@ github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKz github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= -github.com/StephenButtolph/canoto v0.17.0 h1:Ssih5oQNUoJJsoq6jmwoOamZN0KOePerZsmZN7rVCUM= -github.com/StephenButtolph/canoto v0.17.0/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE= -github.com/StephenButtolph/canoto v0.17.1-0.20250609162735-d2c4c1bd598d h1:KoDPYYUNS4jcpgKh7oHE1NVKReayvqYoCLoRLZFaeCI= -github.com/StephenButtolph/canoto v0.17.1-0.20250609162735-d2c4c1bd598d/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE= github.com/StephenButtolph/canoto v0.17.1 h1:WnN5czIHHALq7pwc+Z2F1sCsKJCDhxlq0zL0YK1etHc= github.com/StephenButtolph/canoto v0.17.1/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE= github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= @@ -23,10 +19,6 @@ github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/arr4n/sink v0.0.0-20250524105858-43bc1efdcbc4 h1:OtC5U4Tk6niBhEO9JwSqOH209YEPSROZTP9EC+2P3nk= -github.com/arr4n/sink v0.0.0-20250524105858-43bc1efdcbc4/go.mod h1:TFbsruhH4SB/VO/ONKgNrgBeTLDkpr+uydstjIVyFFQ= -github.com/arr4n/sink v0.0.0-20250610112132-99ad5a7d3b48 h1:D/iNtHC2X5jTVP7YVQPSui5Rzzm5Smf+RLAJ2Q8rQOE= -github.com/arr4n/sink v0.0.0-20250610112132-99ad5a7d3b48/go.mod h1:TFbsruhH4SB/VO/ONKgNrgBeTLDkpr+uydstjIVyFFQ= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa h1:7d3Bkbr8pwxrPnK7AbJzI7Qi0DmLAHIgXmPT26D186w= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa/go.mod h1:TFbsruhH4SB/VO/ONKgNrgBeTLDkpr+uydstjIVyFFQ= github.com/ava-labs/avalanchego v1.13.0 h1:bUWMWLShC0ACa5bQgZdBEJf5lMn1lJIvFrolj3iqTZk= @@ -109,8 +101,6 @@ github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= -github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= @@ -224,8 +214,6 @@ github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:q github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= @@ -365,12 +353,8 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= diff --git a/hook/hook.go b/hook/hook.go index af4cfbf..bcc9768 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -10,28 +10,61 @@ import ( "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/strevm/gastime" + "github.com/holiman/uint256" ) +type ConstructBlock func( + ctx context.Context, + blockContext *block.Context, + header *types.Header, + parent *types.Header, + ancestors iter.Seq[*types.Block], + state State, + txs []*types.Transaction, + receipts []*types.Receipt, +) (*types.Block, error) + +type Account struct { + Nonce uint64 + Amount uint256.Int +} + +type Op struct { + // Gas is the amount of gas consumed by this operation + Gas gas.Gas + // GasPrice is the largest gas price 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]Account + // 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 +} + +type State interface { + Apply(o Op) error +} + // Points define user-injected hook points. type Points interface { GasTarget(parent *types.Block) gas.Gas - // ShouldVerifyBlockContext reports whether this block is only valid against - // a subset of proposervm block contexts. If there are contexts where this - // block could be invalid, this function must return true. - // - // This function must be deterministic for a given block. - ShouldVerifyBlockContext(ctx context.Context, block *types.Block) (bool, error) - // VerifyBlockContext verifies that the block is valid within the provided - // block context. This is not expected to fully verify the block, only that - // the block is not invalid with the provided context. - VerifyBlockContext(ctx context.Context, blockContext *block.Context, block *types.Block) error - // VerifyBlockAncestors verifies that the block has a valid chain of - // ancestors. This is not expected to fully verify the block, only that the - // block's ancestors are compatible. The ancestor iterator iterates from the - // parent of block up to but not including the most recently settled block. - VerifyBlockAncestors(ctx context.Context, block *types.Block, ancestors iter.Seq[*types.Block]) error + ExtraBlockOperations(ctx context.Context, block *types.Block) ([]Op, error) + ConstructBlock( + ctx context.Context, + blockContext *block.Context, + header *types.Header, + parent *types.Header, + ancestors iter.Seq[*types.Block], + state State, + txs []*types.Transaction, + receipts []*types.Receipt, + ) (*types.Block, error) + ConstructBlockFromBlock(ctx context.Context, block *types.Block) (ConstructBlock, error) } // BeforeBlock is intended to be called before processing a block, with the gas diff --git a/integration_test.go b/integration_test.go index 485bd50..83b60ed 100644 --- a/integration_test.go +++ b/integration_test.go @@ -30,6 +30,7 @@ import ( "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/rpc" "github.com/ava-labs/strevm/blocks" + "github.com/ava-labs/strevm/hook" "github.com/ava-labs/strevm/queue" "github.com/ava-labs/strevm/weth" "github.com/google/go-cmp/cmp" @@ -52,16 +53,30 @@ func (h *stubHooks) GasTarget(parent *types.Block) gas.Gas { return h.T } -func (*stubHooks) ShouldVerifyBlockContext(context.Context, *types.Block) (bool, error) { - return false, nil +func (*stubHooks) ExtraBlockOperations(ctx context.Context, block *types.Block) ([]hook.Op, error) { + return nil, nil } -func (*stubHooks) VerifyBlockContext(context.Context, *block.Context, *types.Block) error { - return nil +func (h *stubHooks) ConstructBlock( + ctx context.Context, + blockContext *block.Context, + header *types.Header, + parent *types.Header, + ancestors iter.Seq[*types.Block], + state hook.State, + txs []*types.Transaction, + receipts []*types.Receipt, +) (*types.Block, error) { + return types.NewBlock( + header, + txs, nil, /*uncles*/ + receipts, + trieHasher(), + ), nil } -func (*stubHooks) VerifyBlockAncestors(context.Context, *types.Block, iter.Seq[*types.Block]) error { - return nil +func (h *stubHooks) ConstructBlockFromBlock(ctx context.Context, block *types.Block) (hook.ConstructBlock, error) { + return h.ConstructBlock, nil } func TestIntegrationWrapAVAX(t *testing.T) { diff --git a/plugin/plugin.go b/plugin/plugin.go index 37d7901..b31d3eb 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -10,8 +10,10 @@ import ( "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/rpcchainvm" "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/trie" sae "github.com/ava-labs/strevm" "github.com/ava-labs/strevm/adaptor" + "github.com/ava-labs/strevm/hook" ) const ( @@ -24,16 +26,30 @@ func (*hooks) GasTarget(parent *types.Block) gas.Gas { return TargetGasPerSecond } -func (*hooks) ShouldVerifyBlockContext(context.Context, *types.Block) (bool, error) { - return false, nil +func (*hooks) ExtraBlockOperations(ctx context.Context, block *types.Block) ([]hook.Op, error) { + return nil, nil } -func (*hooks) VerifyBlockContext(context.Context, *block.Context, *types.Block) error { - return nil +func (h *hooks) ConstructBlock( + ctx context.Context, + blockContext *block.Context, + header *types.Header, + parent *types.Header, + ancestors iter.Seq[*types.Block], + state hook.State, + txs []*types.Transaction, + receipts []*types.Receipt, +) (*types.Block, error) { + return types.NewBlock( + header, + txs, nil, /*uncles*/ + receipts, + trie.NewStackTrie(nil), + ), nil } -func (*hooks) VerifyBlockAncestors(context.Context, *types.Block, iter.Seq[*types.Block]) error { - return nil +func (h *hooks) ConstructBlockFromBlock(ctx context.Context, block *types.Block) (hook.ConstructBlock, error) { + return h.ConstructBlock, nil } func main() { diff --git a/vm.go b/vm.go index 882ff96..004528f 100644 --- a/vm.go +++ b/vm.go @@ -231,15 +231,11 @@ func (vm *VM) ParseBlock(ctx context.Context, blockBytes []byte) (*blocks.Block, return vm.newBlock(b, nil, nil) } -func (vm *VM) BuildBlockWithContext(ctx context.Context, _ *block.Context) (*blocks.Block, error) { - // TODO(StephenButtolph): Figure out how we want to support block building - // hooks. Contextual block building is needed to support Warp messages. - // - // We additionally need to include atomic txs in the block for the C-chain. - return vm.BuildBlock(ctx) +func (vm *VM) BuildBlock(ctx context.Context) (*blocks.Block, error) { + return vm.BuildBlockWithContext(ctx, nil) } -func (vm *VM) BuildBlock(ctx context.Context) (*blocks.Block, error) { +func (vm *VM) BuildBlockWithContext(ctx context.Context, _ *block.Context) (*blocks.Block, error) { return vm.buildBlock(ctx, uint64(vm.now().Unix()), vm.preference.Load()) } diff --git a/worstcase/state.go b/worstcase/state.go new file mode 100644 index 0000000..7ccd250 --- /dev/null +++ b/worstcase/state.go @@ -0,0 +1,213 @@ +// Package worstcase is a pessimist, always seeing the glass as half empty. But +// where others see full glasses and opportunities, package worstcase sees DoS +// vulnerabilities. +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/ava-labs/strevm/gastime" + "github.com/ava-labs/strevm/hook" + "github.com/holiman/uint256" +) + +// A State assumes that every transaction will consume its stated +// gas limit, tracking worst-case gas costs under this assumption. +type State struct { + db *state.StateDB + + curr *types.Header + config *params.ChainConfig + rules params.Rules + signer types.Signer + + clock *gastime.Time + + maxQSeconds, maxBlockSeconds uint64 + qLength, maxQLength, blockSize, maxBlockSize gas.Gas +} + +// NewTxIncluder constructs a new includer. +// +// The [state.StateDB] MUST be opened at the state immediately following the +// last-executed block upon which the includer is building. 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. +// +// [State.StartBlock] MUST be called before the first call to +// [State.Include]. +func NewTxIncluder( + db *state.StateDB, + config *params.ChainConfig, + fromExecTime *gastime.Time, + maxQueueSeconds, maxBlockSeconds uint64, +) *State { + s := &State{ + db: db, + config: config, + clock: fromExecTime, + maxQSeconds: maxQueueSeconds, + maxBlockSeconds: maxBlockSeconds, + } + s.setMaxSizes() + return s +} + +func (s *State) setMaxSizes() { + s.maxQLength = s.clock.Rate() * gas.Gas(s.maxQSeconds) + s.maxBlockSize = s.clock.Rate() * gas.Gas(s.maxBlockSeconds) +} + +var errNonConsecutiveBlocks = errors.New("non-consecutive block numbers") + +// StartBlock calls [State.FinishBlock] and then fast-forwards the +// includer's [gastime.Time] to the new block's timestamp before updating the +// gas target. Only the block number and timestamp are required to be set in the +// header. +func (s *State) StartBlock(hdr *types.Header, target gas.Gas) 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.FinishBlock() + hook.BeforeBlock(s.clock, hdr, target) + s.setMaxSizes() + s.curr = types.CopyHeader(hdr) + s.curr.GasLimit = uint64(min(s.maxQLength, s.maxBlockSize)) + + // 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 +} + +// FinishBlock advances the includer's [gastime.Time] to account for all +// included transactions since the last call to FinishBlock. In the absence of +// intervening calls to [State.Include], calls to FinishBlock are +// idempotent. +// +// There is no need to call FinishBlock before a call to +// [State.StartBlock]. +func (s *State) FinishBlock() { + hook.AfterBlock(s.clock, s.blockSize) + s.blockSize = 0 +} + +// ErrQueueTooFull and ErrBlockTooFull are returned by +// [State.Include] if inclusion of the transaction would have +// caused the queue or block, respectively, to exceed their maximum allowed gas +// length. +var ( + ErrQueueTooFull = errors.New("queue too full") + ErrBlockTooFull = errors.New("block too full") +) + +// 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. +func (s *State) ApplyTx(tx *types.Transaction) error { + opts := &txpool.ValidationOptions{ + Config: s.config, + Accept: 0 | + 1< s.maxQLength-s.qLength: + return ErrQueueTooFull + case o.Gas > s.maxBlockSize-s.blockSize: + return ErrBlockTooFull + } + + // ----- GasPrice ----- + if min := s.clock.BaseFee(); o.GasPrice.Cmp(min) < 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); bal.Cmp(&ad.Amount) < 0 { + return core.ErrInsufficientFunds + } + } + + // ----- Inclusion ----- + s.qLength += o.Gas + s.blockSize += o.Gas + + for from, ad := range o.From { + s.db.SetNonce(from, ad.Nonce+1) + s.db.SubBalance(from, &ad.Amount) + } + + for to, amount := range o.To { + s.db.AddBalance(to, &amount) + } + return nil +} diff --git a/worstcase/txinclusion_test.go b/worstcase/state_test.go similarity index 96% rename from worstcase/txinclusion_test.go rename to worstcase/state_test.go index 83f9e44..4557f26 100644 --- a/worstcase/txinclusion_test.go +++ b/worstcase/state_test.go @@ -23,7 +23,7 @@ func newDB(tb testing.TB) *state.StateDB { return db } -func newTxIncluder(tb testing.TB) (*TransactionIncluder, *state.StateDB) { +func newTxIncluder(tb testing.TB) (*State, *state.StateDB) { tb.Helper() db := newDB(tb) return NewTxIncluder( @@ -151,7 +151,7 @@ func TestNonContextualTransactionRejection(t *testing.T) { tt.stateSetup(db) } tx := types.MustSignNewTx(key, types.NewCancunSigner(inc.config.ChainID), tt.tx) - require.ErrorIs(t, inc.Include(tx), tt.wantErrIs) + require.ErrorIs(t, inc.ApplyTx(tx), tt.wantErrIs) }) } } diff --git a/worstcase/txinclusion.go b/worstcase/txinclusion.go deleted file mode 100644 index c2cffa9..0000000 --- a/worstcase/txinclusion.go +++ /dev/null @@ -1,185 +0,0 @@ -// Package worstcase is a pessimist, always seeing the glass as half empty. But -// where others see full glasses and opportunities, package worstcase sees DoS -// vulnerabilities. -package worstcase - -import ( - "errors" - "fmt" - "math" - "math/big" - - "github.com/ava-labs/avalanchego/vms/components/gas" - "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/ava-labs/strevm/gastime" - "github.com/ava-labs/strevm/hook" - "github.com/holiman/uint256" -) - -// A TransactionIncluder assumes that every transaction will consume its stated -// gas limit, tracking worst-case gas costs under this assumption. -type TransactionIncluder struct { - db *state.StateDB - - curr *types.Header - config *params.ChainConfig - rules params.Rules - signer types.Signer - - clock *gastime.Time - - maxQSeconds, maxBlockSeconds uint64 - qLength, maxQLength, blockSize, maxBlockSize gas.Gas -} - -// NewTxIncluder constructs a new includer. -// -// The [state.StateDB] MUST be opened at the state immediately following the -// last-executed block upon which the includer is building. 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. -// -// [TransactionIncluder.StartBlock] MUST be called before the first call to -// [TransactionIncluder.Include]. -func NewTxIncluder( - db *state.StateDB, - config *params.ChainConfig, - fromExecTime *gastime.Time, - maxQueueSeconds, maxBlockSeconds uint64, -) *TransactionIncluder { - inc := &TransactionIncluder{ - db: db, - config: config, - clock: fromExecTime, - maxQSeconds: maxQueueSeconds, - maxBlockSeconds: maxBlockSeconds, - } - inc.setMaxSizes() - return inc -} - -func (inc *TransactionIncluder) setMaxSizes() { - inc.maxQLength = inc.clock.Rate() * gas.Gas(inc.maxQSeconds) - inc.maxBlockSize = inc.clock.Rate() * gas.Gas(inc.maxBlockSeconds) -} - -var errNonConsecutiveBlocks = errors.New("non-consecutive block numbers") - -// StartBlock calls [TransactionIncluder.FinishBlock] and then fast-forwards the -// includer's [gastime.Time] to the new block's timestamp before updating the -// gas target. Only the block number and timestamp are required to be set in the -// header. -func (inc *TransactionIncluder) StartBlock(hdr *types.Header, target gas.Gas) error { - if c := inc.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) - } - } - - inc.FinishBlock() - hook.BeforeBlock(inc.clock, hdr, target) - inc.setMaxSizes() - inc.curr = types.CopyHeader(hdr) - inc.curr.GasLimit = uint64(min(inc.maxQLength, inc.maxBlockSize)) - - // 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. - inc.rules = inc.config.Rules(hdr.Number, true, hdr.Time) - inc.signer = types.MakeSigner(inc.config, hdr.Number, hdr.Time) - - return nil -} - -// FinishBlock advances the includer's [gastime.Time] to account for all -// included transactions since the last call to FinishBlock. In the absence of -// intervening calls to [TransactionIncluder.Include], calls to FinishBlock are -// idempotent. -// -// There is no need to call FinishBlock before a call to -// [TransactionIncluder.StartBlock]. -func (inc *TransactionIncluder) FinishBlock() { - hook.AfterBlock(inc.clock, inc.blockSize) - inc.blockSize = 0 -} - -// ErrQueueTooFull and ErrBlockTooFull are returned by -// [TransactionIncluder.Include] if inclusion of the transaction would have -// caused the queue or block, respectively, to exceed their maximum allowed gas -// length. -var ( - ErrQueueTooFull = errors.New("queue too full") - ErrBlockTooFull = errors.New("block too full") -) - -// Include validates the transaction both intrinsically and in the context of -// worst-case gas assumptions of all previous calls to Include. This provides an -// upper bound on the total cost of the transaction such that a nil error -// returned by Include guarantees that the sender of the transaction will have -// sufficient balance to cover its costs if consensus accepts the same -// transaction set (and order) as was passed to Include. -// -// The TransactionIncluder's internal state is updated to reflect inclusion of -// the transaction i.f.f. a nil error is returned by Include. -func (inc *TransactionIncluder) Include(tx *types.Transaction) error { - opts := &txpool.ValidationOptions{ - Config: inc.config, - Accept: 0 | - 1< inc.maxQLength-inc.qLength: - return ErrQueueTooFull - case g > inc.maxBlockSize-inc.blockSize: - return ErrBlockTooFull - } - - from, err := types.Sender(inc.signer, tx) - if err != nil { - return fmt.Errorf("determining sender: %w", err) - } - - // [txpool.ValidateTransactionWithState] is not fit for our purpose so we - // implement our own checks. Although it could be massaged into working - // properly, that would make the code hard to understand. - - // ----- Nonce ----- - switch nonce, next := tx.Nonce(), inc.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+1 < next: - return core.ErrNonceMax - } - - // ----- Balance covers worst-case gas cost + tx value ----- - if cap, min := tx.GasFeeCap(), inc.clock.BaseFee().ToBig(); cap.Cmp(min) < 0 { - return core.ErrFeeCapTooLow - } - txCost := uint256.MustFromBig(tx.Cost()) - if bal := inc.db.GetBalance(from); bal.Cmp(txCost) < 0 { - return core.ErrInsufficientFunds - } - - // ----- Inclusion ----- - g := gas.Gas(tx.Gas()) - inc.qLength += g - inc.blockSize += g - - inc.db.SetNonce(from, inc.db.GetNonce(from)+1) - inc.db.SubBalance(from, txCost) - return nil -} From 9c48d50719a151dcbf7d52d9c810edd92bf4015d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 24 Jun 2025 14:02:43 -0400 Subject: [PATCH 16/42] wip --- vm.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vm.go b/vm.go index 004528f..da3cf5d 100644 --- a/vm.go +++ b/vm.go @@ -235,8 +235,8 @@ func (vm *VM) BuildBlock(ctx context.Context) (*blocks.Block, error) { return vm.BuildBlockWithContext(ctx, nil) } -func (vm *VM) BuildBlockWithContext(ctx context.Context, _ *block.Context) (*blocks.Block, error) { - return vm.buildBlock(ctx, uint64(vm.now().Unix()), vm.preference.Load()) +func (vm *VM) BuildBlockWithContext(ctx context.Context, blockContext *block.Context) (*blocks.Block, error) { + return vm.buildBlock(ctx, blockContext, uint64(vm.now().Unix()), vm.preference.Load()) } func (vm *VM) signer(blockNum, timestamp uint64) types.Signer { From 797da31008947308adbdc02d753b9cdfcccc6823 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 24 Jun 2025 14:45:42 -0400 Subject: [PATCH 17/42] comments --- hook/hook.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hook/hook.go b/hook/hook.go index bcc9768..feec029 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -53,10 +53,14 @@ type State interface { // Points define user-injected hook points. type Points interface { GasTarget(parent *types.Block) gas.Gas + + // Called during historical worst case tracking + execution ExtraBlockOperations(ctx context.Context, block *types.Block) ([]Op, error) + + // Called during build ConstructBlock( ctx context.Context, - blockContext *block.Context, + blockContext *block.Context, // May be nil header *types.Header, parent *types.Header, ancestors iter.Seq[*types.Block], @@ -64,6 +68,8 @@ type Points interface { txs []*types.Transaction, receipts []*types.Receipt, ) (*types.Block, error) + + // Called during verify ConstructBlockFromBlock(ctx context.Context, block *types.Block) (ConstructBlock, error) } From b10488539edb2805d817b9ca0301059662f49641 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 24 Jun 2025 16:01:51 -0400 Subject: [PATCH 18/42] more hooks --- blocks/execution.go | 14 ++++++++++- blocks/settlement_test.go | 3 ++- db.go | 2 +- hook/hook.go | 9 ++++--- hook/hooktest/hook.go | 51 +++++++++++++++++++++++++++++++++++++++ integration_test.go | 40 ++---------------------------- plugin/plugin.go | 47 ++++-------------------------------- saexec/execution.go | 18 +++++++++++++- 8 files changed, 97 insertions(+), 87 deletions(-) create mode 100644 hook/hooktest/hook.go diff --git a/blocks/execution.go b/blocks/execution.go index 8e05f52..142383c 100644 --- a/blocks/execution.go +++ b/blocks/execution.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/libevm/ethdb" "github.com/ava-labs/libevm/trie" "github.com/ava-labs/strevm/gastime" + "github.com/ava-labs/strevm/hook" ) //go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE @@ -57,7 +58,14 @@ func (e *executionResults) Equal(f *executionResults) bool { // // This function MUST NOT be called more than once. The wall-clock [time.Time] // is for metrics only. -func (b *Block) MarkExecuted(db ethdb.Database, byGas *gastime.Time, byWall time.Time, receipts types.Receipts, stateRootPost common.Hash) error { +func (b *Block) MarkExecuted( + db ethdb.Database, + byGas *gastime.Time, + byWall time.Time, + receipts types.Receipts, + stateRootPost common.Hash, + hooks hook.Points, +) error { var used gas.Gas for _, r := range receipts { used += gas.Gas(r.GasUsed) @@ -83,6 +91,10 @@ func (b *Block) MarkExecuted(db ethdb.Database, byGas *gastime.Time, byWall time return err } + if err := hooks.BlockExecuted(context.TODO(), b.Block); err != nil { + return err + } + return b.markExecuted(e) } diff --git a/blocks/settlement_test.go b/blocks/settlement_test.go index 6ace10f..0190aa7 100644 --- a/blocks/settlement_test.go +++ b/blocks/settlement_test.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/ethdb" "github.com/ava-labs/strevm/gastime" + "github.com/ava-labs/strevm/hook/hooktest" "github.com/ava-labs/strevm/proxytime" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -210,7 +211,7 @@ func TestSettles(t *testing.T) { func (b *Block) markExecutedForTests(tb testing.TB, db ethdb.Database, tm *gastime.Time) { tb.Helper() - require.NoError(tb, b.MarkExecuted(db, tm, time.Time{}, nil, common.Hash{}), "MarkExecuted()") + require.NoError(tb, b.MarkExecuted(db, tm, time.Time{}, nil, common.Hash{}, hooktest.Simple{}), "MarkExecuted()") } func TestLastToSettleAt(t *testing.T) { diff --git a/db.go b/db.go index 2feb684..e9b9bae 100644 --- a/db.go +++ b/db.go @@ -54,7 +54,7 @@ func (vm *VM) upgradeLastSynchronousBlock(hash common.Hash) error { 1e6, 0, ) receipts := rawdb.ReadRawReceipts(vm.db, hash, block.Height()) - if err := block.MarkExecuted(vm.db, clock, block.Timestamp(), receipts, block.Root()); err != nil { + if err := block.MarkExecuted(vm.db, clock, block.Timestamp(), receipts, block.Root(), vm.hooks); err != nil { return err } if err := block.WriteLastSettledNumber(vm.db); err != nil { diff --git a/hook/hook.go b/hook/hook.go index feec029..b4c523b 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -54,9 +54,6 @@ type State interface { type Points interface { GasTarget(parent *types.Block) gas.Gas - // Called during historical worst case tracking + execution - ExtraBlockOperations(ctx context.Context, block *types.Block) ([]Op, error) - // Called during build ConstructBlock( ctx context.Context, @@ -71,6 +68,12 @@ type Points interface { // Called during verify ConstructBlockFromBlock(ctx context.Context, block *types.Block) (ConstructBlock, error) + + // Called during historical worst case tracking + execution + ExtraBlockOperations(ctx context.Context, block *types.Block) ([]Op, error) + + // Called after the block has been executed by the node. + BlockExecuted(ctx context.Context, block *types.Block) error } // BeforeBlock is intended to be called before processing a block, with the gas diff --git a/hook/hooktest/hook.go b/hook/hooktest/hook.go new file mode 100644 index 0000000..de164d6 --- /dev/null +++ b/hook/hooktest/hook.go @@ -0,0 +1,51 @@ +package hooktest + +import ( + "context" + "iter" + + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/trie" + "github.com/ava-labs/strevm/hook" +) + +type Simple struct { + T gas.Gas +} + +func (s Simple) GasTarget(parent *types.Block) gas.Gas { + return s.T +} + +func (Simple) ConstructBlock( + ctx context.Context, + blockContext *block.Context, + header *types.Header, + parent *types.Header, + ancestors iter.Seq[*types.Block], + state hook.State, + txs []*types.Transaction, + receipts []*types.Receipt, +) (*types.Block, error) { + return types.NewBlock( + header, + txs, + nil, /*uncles*/ + receipts, + trie.NewStackTrie(nil), + ), nil +} + +func (s Simple) ConstructBlockFromBlock(ctx context.Context, block *types.Block) (hook.ConstructBlock, error) { + return s.ConstructBlock, nil +} + +func (Simple) ExtraBlockOperations(ctx context.Context, block *types.Block) ([]hook.Op, error) { + return nil, nil +} + +func (Simple) BlockExecuted(ctx context.Context, block *types.Block) error { + return nil +} diff --git a/integration_test.go b/integration_test.go index 83b60ed..d1ec684 100644 --- a/integration_test.go +++ b/integration_test.go @@ -4,7 +4,6 @@ import ( "context" "errors" "flag" - "iter" "math" "math/big" "net/http/httptest" @@ -18,7 +17,6 @@ import ( "time" "github.com/arr4n/sink" - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/vms/components/gas" ethereum "github.com/ava-labs/libevm" @@ -30,7 +28,7 @@ import ( "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/rpc" "github.com/ava-labs/strevm/blocks" - "github.com/ava-labs/strevm/hook" + "github.com/ava-labs/strevm/hook/hooktest" "github.com/ava-labs/strevm/queue" "github.com/ava-labs/strevm/weth" "github.com/google/go-cmp/cmp" @@ -45,40 +43,6 @@ var ( cpuProfileDest = flag.String("cpu_profile_out", "", "If non-empty, file to which pprof CPU profile is written") ) -type stubHooks struct { - T gas.Gas -} - -func (h *stubHooks) GasTarget(parent *types.Block) gas.Gas { - return h.T -} - -func (*stubHooks) ExtraBlockOperations(ctx context.Context, block *types.Block) ([]hook.Op, error) { - return nil, nil -} - -func (h *stubHooks) ConstructBlock( - ctx context.Context, - blockContext *block.Context, - header *types.Header, - parent *types.Header, - ancestors iter.Seq[*types.Block], - state hook.State, - txs []*types.Transaction, - receipts []*types.Receipt, -) (*types.Block, error) { - return types.NewBlock( - header, - txs, nil, /*uncles*/ - receipts, - trieHasher(), - ), nil -} - -func (h *stubHooks) ConstructBlockFromBlock(ctx context.Context, block *types.Block) (hook.ConstructBlock, error) { - return h.ConstructBlock, nil -} - func TestIntegrationWrapAVAX(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -96,7 +60,7 @@ func TestIntegrationWrapAVAX(t *testing.T) { func() time.Time { return now }, - &stubHooks{ + hooktest.Simple{ T: 2e6, }, tbLogger{tb: t, level: logging.Debug + 1}, diff --git a/plugin/plugin.go b/plugin/plugin.go index b31d3eb..7896eac 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -3,58 +3,21 @@ package main import ( "context" "fmt" - "iter" "os" - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/rpcchainvm" - "github.com/ava-labs/libevm/core/types" - "github.com/ava-labs/libevm/trie" sae "github.com/ava-labs/strevm" "github.com/ava-labs/strevm/adaptor" - "github.com/ava-labs/strevm/hook" + "github.com/ava-labs/strevm/hook/hooktest" ) -const ( - TargetGasPerSecond = 1_000_000 -) - -type hooks struct{} - -func (*hooks) GasTarget(parent *types.Block) gas.Gas { - return TargetGasPerSecond -} - -func (*hooks) ExtraBlockOperations(ctx context.Context, block *types.Block) ([]hook.Op, error) { - return nil, nil -} - -func (h *hooks) ConstructBlock( - ctx context.Context, - blockContext *block.Context, - header *types.Header, - parent *types.Header, - ancestors iter.Seq[*types.Block], - state hook.State, - txs []*types.Transaction, - receipts []*types.Receipt, -) (*types.Block, error) { - return types.NewBlock( - header, - txs, nil, /*uncles*/ - receipts, - trie.NewStackTrie(nil), - ), nil -} - -func (h *hooks) ConstructBlockFromBlock(ctx context.Context, block *types.Block) (hook.ConstructBlock, error) { - return h.ConstructBlock, nil -} +const TargetGasPerSecond = 1_000_000 func main() { vm := adaptor.Convert(&sae.SinceGenesis{ - Hooks: &hooks{}, + Hooks: hooktest.Simple{ + T: TargetGasPerSecond, + }, }) if err := rpcchainvm.Serve(context.Background(), vm); err != nil { diff --git a/saexec/execution.go b/saexec/execution.go index ce2276f..e5820cd 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -122,6 +122,22 @@ func (e *Executor) execute(ctx context.Context, b *blocks.Block) error { // to access them before the end of the block. receipts[ti] = receipt } + + extraOps, err := e.hooks.ExtraBlockOperations(ctx, b.Block) + if err != nil { + return err + } + for _, op := range extraOps { + blockGasConsumed += op.Gas + for from, ad := range op.From { + x.statedb.SetNonce(from, ad.Nonce+1) + x.statedb.SubBalance(from, &ad.Amount) + } + for to, amount := range op.To { + x.statedb.AddBalance(to, &amount) + } + } + endTime := time.Now() hook.AfterBlock(&e.gasClock, blockGasConsumed) @@ -135,7 +151,7 @@ func (e *Executor) execute(ctx context.Context, b *blocks.Block) error { // 1. [blocks.Block.MarkExecuted] guarantees disk then in-memory changes. // 2. Internal indicator of last executed MUST follow in-memory change. // 3. External indicator of last executed MUST follow internal indicator. - if err := b.MarkExecuted(e.db, e.gasClock.Clone(), endTime, receipts, root); err != nil { + if err := b.MarkExecuted(e.db, e.gasClock.Clone(), endTime, receipts, root, e.hooks); err != nil { return err } e.lastExecuted.Store(b) // (2) From 06ce124db27883bfecf199e6d87c5211b1aeb2b5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 24 Jun 2025 16:17:48 -0400 Subject: [PATCH 19/42] Include receipts --- blocks/execution.go | 2 +- hook/hook.go | 6 +++++- hook/hooktest/hook.go | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/blocks/execution.go b/blocks/execution.go index 142383c..8b6d58b 100644 --- a/blocks/execution.go +++ b/blocks/execution.go @@ -91,7 +91,7 @@ func (b *Block) MarkExecuted( return err } - if err := hooks.BlockExecuted(context.TODO(), b.Block); err != nil { + if err := hooks.BlockExecuted(context.TODO(), b.Block, receipts); err != nil { return err } diff --git a/hook/hook.go b/hook/hook.go index b4c523b..ce2b55f 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -73,7 +73,11 @@ type Points interface { ExtraBlockOperations(ctx context.Context, block *types.Block) ([]Op, error) // Called after the block has been executed by the node. - BlockExecuted(ctx context.Context, block *types.Block) error + BlockExecuted( + ctx context.Context, + block *types.Block, + receipts types.Receipts, + ) error } // BeforeBlock is intended to be called before processing a block, with the gas diff --git a/hook/hooktest/hook.go b/hook/hooktest/hook.go index de164d6..b2cbc66 100644 --- a/hook/hooktest/hook.go +++ b/hook/hooktest/hook.go @@ -46,6 +46,6 @@ func (Simple) ExtraBlockOperations(ctx context.Context, block *types.Block) ([]h return nil, nil } -func (Simple) BlockExecuted(ctx context.Context, block *types.Block) error { +func (Simple) BlockExecuted(ctx context.Context, block *types.Block, receipts types.Receipts) error { return nil } From b6e1701cfedffdf88105e4a53bd7b8250de7c0aa Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 24 Jun 2025 17:04:04 -0400 Subject: [PATCH 20/42] wip --- integration_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integration_test.go b/integration_test.go index c486a9b..fe53d26 100644 --- a/integration_test.go +++ b/integration_test.go @@ -44,6 +44,10 @@ var ( enableMinGasConsumption = flag.Bool("enable_min_gas_consumption", true, "Enforce lambda lower bound on gas consumption in integration test") ) +func init() { + flag.Var(&txsInIntegrationTest, "wrap_avax_tx_count", "Number of transactions to use in TestIntegrationWrapAVAX (comma-separated)") +} + func TestIntegrationWrapAVAX(t *testing.T) { for _, n := range txsInIntegrationTest { t.Run(fmt.Sprint(n), func(t *testing.T) { From 3ce2e9cf31254f7d990afc3338a875778057b418 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 24 Jun 2025 17:07:44 -0400 Subject: [PATCH 21/42] fix --- go.mod | 4 ++++ go.sum | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/go.mod b/go.mod index d1f8707..adc55c6 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/ethereum/c-kzg-4844 v0.4.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect @@ -62,6 +63,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/huin/goupnp v1.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/klauspost/compress v1.15.15 // indirect github.com/kr/pretty v0.3.1 // indirect @@ -80,6 +82,8 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/status-im/keycard-go v0.2.0 // indirect github.com/supranational/blst v0.3.14 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect diff --git a/go.sum b/go.sum index 2297fc4..eec6f00 100644 --- a/go.sum +++ b/go.sum @@ -101,6 +101,8 @@ github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= @@ -214,6 +216,8 @@ github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:q github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= @@ -353,8 +357,12 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= From d9b9474a1174af1591eebc3c95a0a72eac27fa81 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 24 Jun 2025 17:23:52 -0400 Subject: [PATCH 22/42] nits --- scripts/build.sh | 2 +- vm.go | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 218ff88..a2dc81c 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -7,5 +7,5 @@ set -o pipefail VMID="sr96zN6VeXJ4y5fY5EFziQrPSiy4LJPUMJGQsSLEW4t5bHWPw" BINARY_PATH="$HOME/.avalanchego/plugins/$VMID" echo "Building SAE EVM at $BINARY_PATH" -go build -o "$BINARY_PATH" "./rpc/"*.go +go build -o "$BINARY_PATH" "./plugin/"*.go echo "Built SAE EVM at $BINARY_PATH" diff --git a/vm.go b/vm.go index ab72733..9eb90d0 100644 --- a/vm.go +++ b/vm.go @@ -69,7 +69,17 @@ type ( ) type Config struct { - Hooks hook.Points + Hooks hook.Points + // LastExecutedBlockHeight should be >= the LastSynchronousBlock height. + // + // TODO(StephenButtolph): This allows coreth to specify what atomic txs + // (and warp receipts) have been applied. This is needed because the DB that + // is written to with Hooks.BlockExecuted is not atomically managed with the + // rest of SAE's state. We must ensure that Hooks.BlockExecuted is called + // consecutively starting with the block with height + // LastExecutedBlockHeight+1. + LastExecutedBlockHeight uint64 + ChainConfig *params.ChainConfig DB ethdb.Database // At the point of upgrade from synchronous to asynchronous execution, the From 5ae6bb3810fb3ac08ce5f3dae0684c023951e035 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:10:33 +0100 Subject: [PATCH 23/42] chore: repo setup incl. CI, license, etc. (#3) The `go.yml` and `lint.yml` CI workflows come from `libevm` as reasonable first approximations of what we want for this repo. The failures on the Go workflow are all to be expected because there aren't any Go files to test, generate, nor lint. What's important is that all workflows are running. --- .github/workflows/go.yml | 59 +++++++++++++++++ .github/workflows/lint.yml | 65 +++++++++++++++++++ .gitignore | 1 + .golangci.yml | 128 +++++++++++++++++++++++++++++++++++++ .license-header | 2 + .yamllint.yml | 9 +++ LICENSE | 66 +++++++++++++++++++ README.md | 3 +- go.mod | 3 + 9 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/go.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .license-header create mode 100644 .yamllint.yml create mode 100644 LICENSE create mode 100644 go.mod diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..af0ac76 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,59 @@ +name: Go + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + # If adding a new job, add it to the `needs` list of the `go` job as this is + # what gates PRs. + go: + runs-on: ubuntu-latest + needs: [go_test, go_generate, go_tidy] + steps: + - run: echo "Dependencies successful" + + go_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - run: go test ./... + + go_generate: + env: + EXCLUDE_REGEX: "ava-labs/libevm/(accounts/usbwallet/trezor)$" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + + - name: Run `go generate` + run: go list ./... | grep -Pv "${EXCLUDE_REGEX}" | xargs go generate; + + - name: git diff + run: git diff --exit-code + + go_tidy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - run: go mod tidy + - run: git diff --exit-code diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..0eba8a9 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,65 @@ +name: lint + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + # Required: allow read access to the content for analysis. + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + # If adding a new linter: (a) create a new job; and (b) add it to the `needs` + # list of the `lint` job as this is what gates PRs. + lint: + runs-on: ubuntu-latest + needs: [golangci-lint, yamllint, shellcheck] + steps: + - run: echo "Dependencies successful" + + golangci-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for go-header check https://github.com/golangci/golangci-lint/issues/2470#issuecomment-1473658471 + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: goheader + if: ${{ github.event_name == 'pull_request' }} + # The goheader linter is only enabled in the CI so that it runs only on modified or new files + # (see only-new-issues: true). It is disabled in .golangci.yml because + # golangci-lint running locally is not aware of new/modified files compared to the base + # commit of a pull request, and we want to avoid reporting invalid goheader errors. + uses: golangci/golangci-lint-action@v6 + with: + version: v1.60 + only-new-issues: true + args: --enable-only goheader + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.60 + + yamllint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: yamllint -c .yamllint.yml . + + shellcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@2.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722d5e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..9eff0e8 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,128 @@ +# This file configures github.com/golangci/golangci-lint. + +run: + timeout: 20m + tests: true + +linters: + enable: + # Every available linter at the time of writing was considered (quickly) and + # inclusion was liberal. Linters are good at detecting code smells, but if + # we find that a particular one causes too many false positives then we can + # configure it better or, as a last resort, remove it. + - containedctx + - errcheck + - forcetypeassert + - gci + - gocheckcompilerdirectives + - gofmt + # goheader is disabled but it is enabled in the CI with a flag. + # Please see .github/workflows/golangci-lint.yml which explains why. + # - goheader + - goimports + - gomodguard + - gosec + - govet + - ineffassign + # TODO(arr4n): investigate ireturn + - misspell + - nakedret + - nestif + - nilerr + - nolintlint + - reassign + - revive + - sloglint + - staticcheck + - tagliatelle + - testableexamples + - testifylint + - thelper + - tparallel + - unconvert + - usestdlibvars + - unused + - whitespace + +linters-settings: + gci: + custom-order: true + sections: + - standard + - default + - localmodule + # The rest of these break developer expections, in increasing order of + # divergence, so are at the end to increase the chance of being seen. + - alias + - dot + - blank + goheader: + template-path: .license-header + + gomodguard: + blocked: + modules: + - github.com/ethereum/go-ethereum: + reason: "Use ava-labs/libevm instead" + - github.com/ava-labs/avalanchego: + reason: "Avoid dependency loop" + - github.com/ava-labs/coreth: + reason: "Avoid dependency loop" + - github.com/ava-labs/subnet-evm: + reason: "Avoid dependency loop" + revive: + rules: + - name: unused-parameter + # Method parameters may be required by interfaces and forcing them to be + # named _ is of questionable benefit. + disabled: true + +issues: + exclude-dirs-use-default: false + exclude-rules: + - path-except: libevm + linters: + # If any issue is flagged in a non-libevm file, add the linter here + # because the problem isn't under our control. + - containedctx + - forcetypeassert + - errcheck + - gci + - gofmt + - goheader + - goimports + - gosec + - gosimple + - govet + - nakedret + - nestif + - nilerr + - nolintlint + - revive + - staticcheck + - tagliatelle + - testableexamples + - testifylint + - thelper + - tparallel + - typecheck + - usestdlibvars + - varnamelen + - wastedassign + - whitespace + include: + # Many of the default exclusions are because, verbatim "Annoying issue", + # which defeats the point of a linter. + - EXC0002 + - EXC0004 + - EXC0005 + - EXC0006 + - EXC0007 + - EXC0008 + - EXC0009 + - EXC0010 + - EXC0011 + - EXC0012 + - EXC0013 + - EXC0014 + - EXC0015 diff --git a/.license-header b/.license-header new file mode 100644 index 0000000..4bbc82b --- /dev/null +++ b/.license-header @@ -0,0 +1,2 @@ +Copyright (C) {{ MOD-YEAR-RANGE }}, Ava Labs, Inc. All rights reserved. +See the file LICENSE for licensing terms. diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..84a8b96 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,9 @@ +extends: default + +rules: + document-start: disable + line-length: disable + comments: + min-spaces-from-content: 1 + truthy: + check-keys: false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..236b1c8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,66 @@ +Copyright (C) 2025, Ava Labs, Inc. All rights reserved. + +Ecosystem License +Version: 1.1 + +Subject to the terms herein, Ava Labs, Inc. (**"Ava Labs"**) hereby grants you +a limited, royalty-free, worldwide, non-sublicensable, non-transferable, +non-exclusive license to use, copy, modify, create derivative works based on, +and redistribute the Software, in source code, binary, or any other form, +including any modifications or derivative works of the Software (collectively, +**"Licensed Software"**), in each case subject to this Ecosystem License +(**"License"**). + +This License applies to all copies, modifications, derivative works, and any +other form or usage of the Licensed Software. You will include and display +this License, without modification, with all uses of the Licensed Software, +regardless of form. + +You will use the Licensed Software solely (i) in connection with the Avalanche +Public Blockchain platform, having a NetworkID of 1 (Mainnet) or 5 (Fuji), and +associated blockchains, comprised exclusively of the Avalanche X-Chain, +C-Chain, P-Chain and any subnets linked to the P-Chain ("Avalanche Authorized +Platform") or (ii) for non-production, testing or research purposes within the +Avalanche ecosystem, in each case, without any commercial application +("Non-Commercial Use"); provided that this License does not permit use of the +Licensed Software in connection with (a) any forks of the Avalanche Authorized +Platform or (b) in any manner not operationally connected to the Avalanche +Authorized Platform other than, for the avoidance of doubt, the limited +exception for Non-Commercial Use. Ava Labs may publicly announce changes or +additions to the Avalanche Authorized Platform, which may expand or modify +usage of the Licensed Software. Upon such announcement, the Avalanche +Authorized Platform will be deemed to be the then-current iteration of such +platform. + +You hereby acknowledge and agree to the terms set forth at +www.avalabs.org/important-notice. + +If you use the Licensed Software in violation of this License, this License +will automatically terminate and Ava Labs reserves all rights to seek any +remedy for such violation. + +Except for uses explicitly permitted in this License, Ava Labs retains all +rights in the Licensed Software, including without limitation the ability to +modify it. + +Except as required or explicitly permitted by this License, you will not use +any Ava Labs names, logos, or trademarks without Ava Labs’ prior written +consent. + +You may use this License for software other than the "Licensed Software" +specified above, as long as the only change to this License is the definition +of the term "Licensed Software." + +The Licensed Software may reference third party components. You acknowledge +and agree that these third party components may be governed by a separate +license or terms and that you will comply with them. + +**TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE LICENSED SOFTWARE IS PROVIDED +ON AN "AS IS" BASIS, AND AVA LABS EXPRESSLY DISCLAIMS AND EXCLUDES ALL +REPRESENTATIONS, WARRANTIES AND OTHER TERMS AND CONDITIONS, WHETHER EXPRESS OR +IMPLIED, INCLUDING WITHOUT LIMITATION BY OPERATION OF LAW OR BY CUSTOM, +STATUTE OR OTHERWISE, AND INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED WARRANTY, +TERM, OR CONDITION OF NON-INFRINGEMENT, MERCHANTABILITY, TITLE, OR FITNESS FOR +PARTICULAR PURPOSE. YOU USE THE LICENSED SOFTWARE AT YOUR OWN RISK. AVA LABS +EXPRESSLY DISCLAIMS ALL LIABILITY (INCLUDING FOR ALL DIRECT, CONSEQUENTIAL OR +OTHER DAMAGES OR LOSSES) RELATED TO ANY USE OF THE LICENSED SOFTWARE.** \ No newline at end of file diff --git a/README.md b/README.md index 0294a82..24a8348 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # strevm -`strevm` is the reference implementation of Streaming Asynchronous Execution (SAE) of EVM blocks, as described in [ACP](https://github.com/avalanche-foundation/ACPs) 194. +`strevm` is the reference implementation of Streaming Asynchronous Execution (SAE) of EVM blocks, as described in [ACP-194](https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution). +It is under active development and there are currently no guarantees about the stability of its Go APIs. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..146877c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/ava-labs/strevm + +go 1.23.7 From 03d6c0ab47d96048d9183ef485526d7d4a89c04e Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:22:39 +0100 Subject: [PATCH 24/42] fix: remove `golangci-lint` config only needed by `libevm` (#4) `goheader` only checks new issues in `libevm` because `MOD-YEAR-RANGE` breaks otherwise (we can reintroduce this next year if we haven't moved the code into `avalanchego` yet). The other deleted config is what `libevm` uses to avoid linting `geth` code. Go workflows are still expected to fail, as described in #3, and all that matters is that they run (and `golangci-lint` _attempts_ to lint but finds no files). --- .github/workflows/lint.yml | 13 ------------- .golangci.yml | 36 +----------------------------------- 2 files changed, 1 insertion(+), 48 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0eba8a9..336d033 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,22 +30,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Required for go-header check https://github.com/golangci/golangci-lint/issues/2470#issuecomment-1473658471 - uses: actions/setup-go@v5 with: go-version-file: "go.mod" - - name: goheader - if: ${{ github.event_name == 'pull_request' }} - # The goheader linter is only enabled in the CI so that it runs only on modified or new files - # (see only-new-issues: true). It is disabled in .golangci.yml because - # golangci-lint running locally is not aware of new/modified files compared to the base - # commit of a pull request, and we want to avoid reporting invalid goheader errors. - uses: golangci/golangci-lint-action@v6 - with: - version: v1.60 - only-new-issues: true - args: --enable-only goheader - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: diff --git a/.golangci.yml b/.golangci.yml index 9eff0e8..31ae008 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,9 +16,7 @@ linters: - gci - gocheckcompilerdirectives - gofmt - # goheader is disabled but it is enabled in the CI with a flag. - # Please see .github/workflows/golangci-lint.yml which explains why. - # - goheader + - goheader - goimports - gomodguard - gosec @@ -78,38 +76,6 @@ linters-settings: disabled: true issues: - exclude-dirs-use-default: false - exclude-rules: - - path-except: libevm - linters: - # If any issue is flagged in a non-libevm file, add the linter here - # because the problem isn't under our control. - - containedctx - - forcetypeassert - - errcheck - - gci - - gofmt - - goheader - - goimports - - gosec - - gosimple - - govet - - nakedret - - nestif - - nilerr - - nolintlint - - revive - - staticcheck - - tagliatelle - - testableexamples - - testifylint - - thelper - - tparallel - - typecheck - - usestdlibvars - - varnamelen - - wastedassign - - whitespace include: # Many of the default exclusions are because, verbatim "Annoying issue", # which defeats the point of a linter. From b586245f8291d6242b4e17d0fd03d9aab288b36f Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:58:05 +0100 Subject: [PATCH 25/42] fix: `golangci-lint` enforces comments on exported identifiers (#6) As it says on the tin. --- .golangci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 31ae008..07424c8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -74,6 +74,15 @@ linters-settings: # Method parameters may be required by interfaces and forcing them to be # named _ is of questionable benefit. disabled: true + - name: exported + severity: error + disabled: false + exclude: [""] + arguments: + - "sayRepetitiveInsteadOfStutters" + - name: package-comments + severity: warning + disabled: false issues: include: From 07807e2cd7930d85b5f74562b74a7782d446b7c9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 25 Jun 2025 10:37:49 -0400 Subject: [PATCH 26/42] wip --- adaptor/adaptor.go | 2 +- builder.go | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/adaptor/adaptor.go b/adaptor/adaptor.go index 0a3aabf..8016939 100644 --- a/adaptor/adaptor.go +++ b/adaptor/adaptor.go @@ -55,7 +55,7 @@ type BlockProperties interface { // Convert transforms a generic [ChainVM] into a standard [block.ChainVM]. All // [snowman.Block] values returned by methods of the returned chain will be of // the concrete type [Block] with type parameter `BP`. -func Convert[BP BlockProperties](vm ChainVM[BP]) block.ChainVM { +func Convert[BP BlockProperties](vm ChainVM[BP]) *adaptor[BP] { return &adaptor[BP]{vm} } diff --git a/builder.go b/builder.go index 5e5b49f..30522de 100644 --- a/builder.go +++ b/builder.go @@ -17,8 +17,8 @@ import ( "github.com/ava-labs/strevm/blocks" "github.com/ava-labs/strevm/hook" "github.com/ava-labs/strevm/intmath" - "github.com/ava-labs/strevm/queue" "github.com/ava-labs/strevm/params" + "github.com/ava-labs/strevm/queue" "github.com/ava-labs/strevm/worstcase" "go.uber.org/zap" ) @@ -101,13 +101,6 @@ func (vm *VM) buildBlockWithCandidateTxs( if err != nil { return nil, err } - - // TODO: Check if the block contains transactions (potentially atomic) - if ethB.GasUsed() == 0 { - vm.logger().Info("Blocks must either settle or include transactions") - return nil, fmt.Errorf("%w: parent %#x at time %d", errNoopBlock, parent.Hash(), timestamp) - } - return blocks.New(ethB, parent, toSettle, vm.logger()) } From 74ccf472d75de1345015d624a4621f21e322d3f6 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Fri, 27 Jun 2025 18:10:49 +0100 Subject: [PATCH 27/42] feat: `intmath` package (#5) Useful abstractions for later code: 1. `BoundedSubtract(a, b, floor)` returns `max(a-b, floor)` without underflow. 2. `MulDiv(a, b, denominator)` returns `(a*b)/denominator` without overflow. 3. `CeilDiv(num, denom)` returns `num/denom` rounded _up_. --- go.mod | 2 + go.sum | 2 + intmath/intmath.go | 49 +++++++++++++++++++++ intmath/intmath_test.go | 98 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 go.sum create mode 100644 intmath/intmath.go create mode 100644 intmath/intmath_test.go diff --git a/go.mod b/go.mod index 146877c..dd1d866 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/ava-labs/strevm go 1.23.7 + +require golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7794c76 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= diff --git a/intmath/intmath.go b/intmath/intmath.go new file mode 100644 index 0000000..e83cf23 --- /dev/null +++ b/intmath/intmath.go @@ -0,0 +1,49 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package intmath provides special-case integer arithmetic. +package intmath + +import ( + "errors" + "math/bits" + + "golang.org/x/exp/constraints" +) + +// BoundedSubtract returns `max(a-b,floor)` without underflow. +func BoundedSubtract[T constraints.Unsigned](a, b, floor T) T { + // If `floor + b` overflows then it's impossible for `a` to ever be large + // enough for the subtraction to not be bounded. + minA := floor + b + if overflow := minA < b; overflow || a <= minA { + return floor + } + return a - b +} + +// ErrOverflow is returned if a return value would have overflowed its type. +var ErrOverflow = errors.New("overflow") + +// MulDiv returns the quotient and remainder of `(a*b)/den` without overflow in +// the event that `a*b>=2^64`. However, if the quotient were to overflow then +// [ErrOverflow] is returned. +func MulDiv[T ~uint64](a, b, den T) (quo, rem T, err error) { + hi, lo := bits.Mul64(uint64(a), uint64(b)) + if uint64(den) <= hi { + return 0, 0, ErrOverflow + } + q, r := bits.Div64(hi, lo, uint64(den)) + return T(q), T(r), nil +} + +// CeilDiv returns `ceil(num/den)`, i.e. the rounded-up quotient. +func CeilDiv[T ~uint64](num, den T) T { + lo, hi := bits.Add64(uint64(num), uint64(den)-1, 0) + // [bits.Div64] panics if the denominator is zero (expected behaviour) or if + // `den <= hi`. The latter is impossible because `hi` is a carry bit (i.e. + // can only be 0 or 1) and even if `num==MaxUint64` then `den` would have to + // be `>=2` for `hi` to be non-zero. + quo, _ := bits.Div64(hi, lo, uint64(den)) + return T(quo) +} diff --git a/intmath/intmath_test.go b/intmath/intmath_test.go new file mode 100644 index 0000000..c2b0508 --- /dev/null +++ b/intmath/intmath_test.go @@ -0,0 +1,98 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package intmath + +import ( + "errors" + "math" + "math/rand/v2" + "testing" +) + +const max = math.MaxUint64 + +func TestBoundedSubtract(t *testing.T) { + tests := []struct { + a, b, floor, want uint64 + }{ + {a: 1, b: 2, floor: 0, want: 0}, // a < b + {a: 2, b: 1, floor: 0, want: 1}, // not bounded + {a: 2, b: 1, floor: 1, want: 1}, // a - b == floor + {a: 2, b: 2, floor: 1, want: 1}, // bounded + {a: 3, b: 1, floor: 1, want: 2}, + {a: max, b: 10, floor: max - 9, want: max - 9}, // `a` threshold (`max+1`) would overflow uint64 + {a: max, b: 10, floor: max - 11, want: max - 10}, + } + + for _, tt := range tests { + if got := BoundedSubtract(tt.a, tt.b, tt.floor); got != tt.want { + t.Errorf("BoundedSubtract[%T](%[1]d, %d, %d) got %d; want %d", tt.a, tt.b, tt.floor, got, tt.want) + } + } +} + +func TestMulDiv(t *testing.T) { + tests := []struct { + a, b, div, wantQuo, wantRem uint64 + }{ + { + a: 5, b: 2, div: 3, // 10/3 + wantQuo: 3, wantRem: 1, + }, + { + a: 5, b: 3, div: 3, // 15/3 + wantQuo: 5, wantRem: 0, + }, + { + a: max, b: 4, div: 8, // must avoid overflow + wantQuo: max / 2, wantRem: 4, + }, + } + + for _, tt := range tests { + if gotQuo, gotRem, err := MulDiv(tt.a, tt.b, tt.div); err != nil || gotQuo != tt.wantQuo || gotRem != tt.wantRem { + t.Errorf("MulDiv[%T](%[1]d, %d, %d) got (%d, %d, %v); want (%d, %d, nil)", tt.a, tt.b, tt.div, gotQuo, gotRem, err, tt.wantQuo, tt.wantRem) + } + } + + if _, _, err := MulDiv[uint64](max, 2, 1); !errors.Is(err, ErrOverflow) { + t.Errorf("MulDiv[uint64]([max uint64], 2, 1) got error %v; want %v", err, ErrOverflow) + } +} + +func TestCeilDiv(t *testing.T) { + type test struct { + num, den, want uint64 + } + + tests := []test{ + {num: 4, den: 2, want: 2}, + {num: 4, den: 1, want: 4}, + {num: 4, den: 3, want: 2}, + {num: 10, den: 3, want: 4}, + {num: max, den: 2, want: 1 << 63}, // must not overflow + } + + rng := rand.New(rand.NewPCG(0, 0)) //nolint:gosec // Reproducibility is valuable for tests + for range 50 { + l := uint64(rng.Uint32()) + r := uint64(rng.Uint32()) + + tests = append(tests, []test{ + {num: l*r + 1, den: l, want: r + 1}, + {num: l*r + 0, den: l, want: r}, + {num: l*r - 1, den: l, want: r}, + // l <-> r + {num: l*r + 1, den: r, want: l + 1}, + {num: l*r + 0, den: r, want: l}, + {num: l*r - 1, den: r, want: l}, + }...) + } + + for _, tt := range tests { + if got := CeilDiv(tt.num, tt.den); got != tt.want { + t.Errorf("CeilDiv[%T](%[1]d, %d) got %d; want %d", tt.num, tt.den, got, tt.want) + } + } +} From 04d5104183147c2aa7bedc5889f0d0f235b97dfc Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Fri, 27 Jun 2025 19:58:18 +0100 Subject: [PATCH 28/42] feat: `adaptor` package (#7) Allows block implementations to be read-only and not have to be aware of the VM's that created them, which is typically necessary when calling `Block.Verify()`, `Accept()`, or `Reject()`. These methods instead exist on the VM as `{Verify,Accept,Reject}Block()`. There are no explicit tests for this package as it's exercised heavily via the actual SAE tests that will follow. --- .golangci.yml | 2 - adaptor/adaptor.go | 111 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 52 ++++++++++++++++++++- go.sum | 110 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 adaptor/adaptor.go diff --git a/.golangci.yml b/.golangci.yml index 07424c8..8694165 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -62,8 +62,6 @@ linters-settings: modules: - github.com/ethereum/go-ethereum: reason: "Use ava-labs/libevm instead" - - github.com/ava-labs/avalanchego: - reason: "Avoid dependency loop" - github.com/ava-labs/coreth: reason: "Avoid dependency loop" - github.com/ava-labs/subnet-evm: diff --git a/adaptor/adaptor.go b/adaptor/adaptor.go new file mode 100644 index 0000000..ed10f69 --- /dev/null +++ b/adaptor/adaptor.go @@ -0,0 +1,111 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package adaptor provides a generic alternative to the Snowman [block.ChainVM] +// interface, which doesn't require the block to be aware of the VM +// implementation. +package adaptor + +import ( + "context" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" +) + +// ChainVM defines the functionality required in order to be converted into a +// Snowman VM. See the respective methods on [block.ChainVM] and [snowman.Block] +// for detailed documentation. +type ChainVM[BP BlockProperties] interface { + common.VM + + GetBlock(context.Context, ids.ID) (BP, error) + ParseBlock(context.Context, []byte) (BP, error) + BuildBlock(context.Context) (BP, error) + + // Transferred from [snowman.Block]. + VerifyBlock(context.Context, BP) error + AcceptBlock(context.Context, BP) error + RejectBlock(context.Context, BP) error + + SetPreference(context.Context, ids.ID) error + LastAccepted(context.Context) (ids.ID, error) + GetBlockIDAtHeight(context.Context, uint64) (ids.ID, error) +} + +// BlockProperties is a read-only subset of [snowman.Block]. The state-modifying +// methods required by Snowman consensus are, instead, present on [ChainVM]. +type BlockProperties interface { + ID() ids.ID + Parent() ids.ID + Bytes() []byte + Height() uint64 + Timestamp() time.Time +} + +// Convert transforms a generic [ChainVM] into a standard [block.ChainVM]. All +// [snowman.Block] values returned by methods of the returned chain will be of +// the concrete type [Block] with type parameter `BP`. +func Convert[BP BlockProperties](vm ChainVM[BP]) block.ChainVM { + return &adaptor[BP]{vm} +} + +type adaptor[BP BlockProperties] struct { + ChainVM[BP] +} + +// Block is an implementation of [snowman.Block], used by chains returned by +// [Convert]. The [BlockProperties] can be accessed with [Block.Unwrap]. +type Block[BP BlockProperties] struct { + b BP + vm ChainVM[BP] +} + +// Unwrap returns the [BlockProperties] carried by b. +func (b Block[BP]) Unwrap() BP { return b.b } + +func (vm adaptor[BP]) newBlock(b BP, err error) (snowman.Block, error) { + if err != nil { + return nil, err + } + return Block[BP]{b, vm.ChainVM}, nil +} + +func (vm adaptor[BP]) GetBlock(ctx context.Context, blkID ids.ID) (snowman.Block, error) { + return vm.newBlock(vm.ChainVM.GetBlock(ctx, blkID)) +} + +func (vm adaptor[BP]) ParseBlock(ctx context.Context, blockBytes []byte) (snowman.Block, error) { + return vm.newBlock(vm.ChainVM.ParseBlock(ctx, blockBytes)) +} + +func (vm adaptor[BP]) BuildBlock(ctx context.Context) (snowman.Block, error) { + return vm.newBlock(vm.ChainVM.BuildBlock(ctx)) +} + +// Verify calls VerifyBlock(b) on the [ChainVM] that created b. +func (b Block[BP]) Verify(ctx context.Context) error { return b.vm.VerifyBlock(ctx, b.b) } + +// Accept calls AcceptBlock(b) on the [ChainVM] that created b. +func (b Block[BP]) Accept(ctx context.Context) error { return b.vm.AcceptBlock(ctx, b.b) } + +// Reject calls RejectBlock(b) on the [ChainVM] that created b. +func (b Block[BP]) Reject(ctx context.Context) error { return b.vm.RejectBlock(ctx, b.b) } + +// ID propagates the respective method from the [BlockProperties] carried by b. +func (b Block[BP]) ID() ids.ID { return b.b.ID() } + +// Parent propagates the respective method from the [BlockProperties] carried by b. +func (b Block[BP]) Parent() ids.ID { return b.b.Parent() } + +// Bytes propagates the respective method from the [BlockProperties] carried by b. +func (b Block[BP]) Bytes() []byte { return b.b.Bytes() } + +// Height propagates the respective method from the [BlockProperties] carried by b. +func (b Block[BP]) Height() uint64 { return b.b.Height() } + +// Timestamp propagates the respective method from the [BlockProperties] carried by b. +func (b Block[BP]) Timestamp() time.Time { return b.b.Timestamp() } diff --git a/go.mod b/go.mod index dd1d866..701da80 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,53 @@ module github.com/ava-labs/strevm -go 1.23.7 +go 1.23.9 -require golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b +toolchain go1.23.10 + +require ( + github.com/ava-labs/avalanchego v1.13.2 + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/DataDog/zstd v1.5.2 // indirect + github.com/StephenButtolph/canoto v0.15.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/renameio/v2 v2.0.0 // indirect + github.com/gorilla/rpc v1.2.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/prometheus/client_golang v1.16.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect + github.com/supranational/blst v0.3.14 // indirect + go.opentelemetry.io/otel v1.22.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect + go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/otel/sdk v1.22.0 // indirect + go.opentelemetry.io/otel/trace v1.22.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + gonum.org/v1/gonum v0.11.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/grpc v1.66.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect +) diff --git a/go.sum b/go.sum index 7794c76..e007a7d 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,112 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= +github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/StephenButtolph/canoto v0.15.0 h1:3iGdyTSQZ7/y09WaJCe0O/HIi53ZyTrnmVzfCqt64mM= +github.com/StephenButtolph/canoto v0.15.0/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE= +github.com/ava-labs/avalanchego v1.13.2 h1:Kx/T2a8vqLlgHde3DWT5zMF5yBIh1rqEd6nJQMMzV/Y= +github.com/ava-labs/avalanchego v1.13.2/go.mod h1:s7W/kim5L6hiD2PB1v/Ozy1ZZyoLQ4H6mxVO0aMnxng= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= +github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= +github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= +github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= +github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/sanity-io/litter v1.5.1 h1:dwnrSypP6q56o3lFxTU+t2fwQ9A+U5qrXVO4Qg9KwVU= +github.com/sanity-io/litter v1.5.1/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= +github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/thepudds/fzgen v0.4.3 h1:srUP/34BulQaEwPP/uHZkdjUcUjIzL7Jkf4CBVryiP8= +github.com/thepudds/fzgen v0.4.3/go.mod h1:BhhwtRhzgvLWAjjcHDJ9pEiLD2Z9hrVIFjBCHJ//zJ4= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 h1:H2JFgRcGiyHg7H7bwcwaQJYrNFqCqrbTQ8K4p1OvDu8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0/go.mod h1:WfCWp1bGoYK8MeULtI15MmQVczfR+bFkk0DF3h06QmQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU= +google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From bfe8d91f049d2c0b820f21190301b02ecf512d34 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 8 Jul 2025 10:55:02 -0400 Subject: [PATCH 29/42] Remove ToEngine channel --- blocks/settlement.go | 2 +- builder.go | 14 ++------------ go.mod | 2 +- go.sum | 4 ++-- harness.go | 6 ++---- mempool.go | 16 +++++++++------- sae_test.go | 2 +- saedev/dev.go | 24 +++++++++++++----------- vm.go | 22 +++++++++++----------- 9 files changed, 42 insertions(+), 50 deletions(-) diff --git a/blocks/settlement.go b/blocks/settlement.go index a382977..4155dd0 100644 --- a/blocks/settlement.go +++ b/blocks/settlement.go @@ -56,7 +56,7 @@ func (b *Block) ParentBlock() *Block { if a := b.ancestry.Load(); a != nil { return a.parent } - b.log.Error(getParentOfSettledMsg) + b.log.Debug(getParentOfSettledMsg) return nil } diff --git a/builder.go b/builder.go index 30522de..97fcdb7 100644 --- a/builder.go +++ b/builder.go @@ -9,7 +9,6 @@ import ( "slices" "github.com/arr4n/sink" - snowcommon "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/libevm/core/state" @@ -28,18 +27,9 @@ func (vm *VM) buildBlock(ctx context.Context, blockContext *block.Context, times ctx, vm.mempool, sink.MaxPriority, func(_ <-chan sink.Priority, pool *queue.Priority[*pendingTx]) (*blocks.Block, error) { block, err := vm.buildBlockWithCandidateTxs(timestamp, parent, pool, blockContext, vm.hooks.ConstructBlock) - - // TODO: This shouldn't be done immediately, there should be some - // retry delay if block building failed. - if pool.Len() > 0 { - select { - case vm.toEngine <- snowcommon.PendingTxs: - default: - p := snowcommon.PendingTxs - vm.logger().Info(fmt.Sprintf("%T(%s) dropped", p, p)) - } + if pool.Len() == 0 { + vm.mempoolHasTxs.Block() } - return block, err }, ) diff --git a/go.mod b/go.mod index adc55c6..89f606b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.23.10 require ( github.com/StephenButtolph/canoto v0.17.1 github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa - github.com/ava-labs/avalanchego v1.13.2-rc.1 + github.com/ava-labs/avalanchego v1.13.3-0.20250707201933-507e6bbb5e7d github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1 github.com/dustin/go-humanize v1.0.0 github.com/google/go-cmp v0.7.0 diff --git a/go.sum b/go.sum index eec6f00..64e2acd 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa h1:7d3Bkbr8pwxrPnK7AbJzI7Qi0DmLAHIgXmPT26D186w= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa/go.mod h1:TFbsruhH4SB/VO/ONKgNrgBeTLDkpr+uydstjIVyFFQ= -github.com/ava-labs/avalanchego v1.13.2-rc.1 h1:TaUB0g8L1uRILvJFdOKwjo7h04rGM/u+MZEvHmh/Y6E= -github.com/ava-labs/avalanchego v1.13.2-rc.1/go.mod h1:s7W/kim5L6hiD2PB1v/Ozy1ZZyoLQ4H6mxVO0aMnxng= +github.com/ava-labs/avalanchego v1.13.3-0.20250707201933-507e6bbb5e7d h1:+IK0mMgrXj2yOxh23ShACLeJlG+XMb6ZDeXda5IFgb0= +github.com/ava-labs/avalanchego v1.13.3-0.20250707201933-507e6bbb5e7d/go.mod h1:QmwzzCZtKaam5OY45kuzq3UinsyRXH75LkP85W7713M= github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1 h1:vBMYo+Iazw0rGTr+cwjkBdh5eadLPlv4ywI4lKye3CA= github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1/go.mod h1:+Iol+sVQ1KyoBsHf3veyrBmHCXr3xXRWq6ZXkgVfNLU= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= diff --git a/harness.go b/harness.go index e8501c6..e01ac6f 100644 --- a/harness.go +++ b/harness.go @@ -54,7 +54,6 @@ func (s *SinceGenesis) Initialize( genesisBytes []byte, _ []byte, _ []byte, - toEngine chan<- snowcommon.Message, _ []*snowcommon.Fx, _ snowcommon.AppSender, ) error { @@ -89,9 +88,8 @@ func (s *SinceGenesis) Initialize( Target: genesisBlockGasTarget, ExcessAfter: 0, }, - ToEngine: toEngine, - SnowCtx: chainCtx, - Now: s.Now, + SnowCtx: chainCtx, + Now: s.Now, }, ) if err != nil { diff --git a/mempool.go b/mempool.go index 53c8bf7..dc2a6c9 100644 --- a/mempool.go +++ b/mempool.go @@ -36,6 +36,14 @@ func (vm *VM) startMempool() { } } +func (vm *VM) WaitForEvent(ctx context.Context) (snowcommon.Message, error) { + // TODO: there should be maximum frequency of block building enforced here. + if err := vm.mempoolHasTxs.Wait(ctx); err != nil { + return 0, err + } + return snowcommon.PendingTxs, nil +} + func (vm *VM) receiveTxs(preempt <-chan sink.Priority, pool *queue.Priority[*pendingTx]) error { for { select { @@ -70,13 +78,7 @@ func (vm *VM) receiveTxs(preempt <-chan sink.Priority, pool *queue.Priority[*pen zap.Stringer("from", from), zap.Uint64("nonce", tx.Nonce()), ) - - select { - case vm.toEngine <- snowcommon.PendingTxs: - default: - p := snowcommon.PendingTxs - vm.logger().Info(fmt.Sprintf("%T(%s) dropped", p, p)) - } + vm.mempoolHasTxs.Open() } } } diff --git a/sae_test.go b/sae_test.go index 78a78bc..3913cd1 100644 --- a/sae_test.go +++ b/sae_test.go @@ -67,7 +67,7 @@ func newVM(ctx context.Context, tb testing.TB, now func() time.Time, hooks hook. snowCtx.Log = logger require.NoErrorf(tb, snow.Initialize( ctx, snowCtx, - nil, genesis, nil, nil, nil, nil, nil, + nil, genesis, nil, nil, nil, nil, ), "%T.Initialize()", snow) handlers, err := snow.CreateHandlers(ctx) diff --git a/saedev/dev.go b/saedev/dev.go index 60b8df0..46f440b 100644 --- a/saedev/dev.go +++ b/saedev/dev.go @@ -6,6 +6,7 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "log" "math/big" @@ -72,26 +73,27 @@ func run(ctx context.Context) error { }), )) - msgs := make(chan snowcommon.Message) - quit := make(chan struct{}) - - if err := vm.Initialize(ctx, snowCtx, nil, genJSON, nil, nil, msgs, nil, nil); err != nil { + if err := vm.Initialize(ctx, snowCtx, nil, genJSON, nil, nil, nil, nil); err != nil { return err } + + quitContext, quit := context.WithCancel(context.Background()) defer func() { - close(quit) + quit() vm.Shutdown(ctx) }() go func() { BuildLoop: for { - select { - case <-quit: + msg, err := vm.WaitForEvent(quitContext) + if errors.Is(err, context.Canceled) { return - case msg := <-msgs: - if msg != snowcommon.PendingTxs { - continue BuildLoop - } + } + if err != nil { + log.Fatalf("%T.WaitForEvent(): %v", vm, err) + } + if msg != snowcommon.PendingTxs { + continue BuildLoop } b, err := vm.BuildBlock(ctx) diff --git a/vm.go b/vm.go index 9eb90d0..5243da8 100644 --- a/vm.go +++ b/vm.go @@ -39,9 +39,8 @@ var VMID = ids.ID{'s', 't', 'r', 'e', 'v', 'm'} type VM struct { snowCtx *snow.Context snowcommon.AppHandler - toEngine chan<- snowcommon.Message - hooks hook.Points - now func() time.Time + hooks hook.Points + now func() time.Time consensusState utils.Atomic[snow.State] @@ -51,8 +50,9 @@ type VM struct { db ethdb.Database - newTxs chan *types.Transaction - mempool sink.PriorityMutex[*queue.Priority[*pendingTx]] + newTxs chan *types.Transaction + mempool sink.PriorityMutex[*queue.Priority[*pendingTx]] + mempoolHasTxs sink.Gate exec *saexec.Executor @@ -90,8 +90,7 @@ type Config struct { // event of a node restart. LastSynchronousBlock LastSynchronousBlock - ToEngine chan<- snowcommon.Message - SnowCtx *snow.Context + SnowCtx *snow.Context // Now is optional, defaulting to [time.Now] if nil. Now func() time.Time @@ -109,19 +108,20 @@ func New(ctx context.Context, c Config) (*VM, error) { // VM snowCtx: c.SnowCtx, db: c.DB, - toEngine: c.ToEngine, hooks: c.Hooks, AppHandler: snowcommon.NewNoOpAppHandler(logging.NoLog{}), now: c.Now, blocks: sink.NewMutex(make(blockMap)), // Block building - newTxs: make(chan *types.Transaction, 10), // TODO(arr4n) make the buffer configurable - mempool: sink.NewPriorityMutex(new(queue.Priority[*pendingTx])), - quit: quit, // both mempool and executor + newTxs: make(chan *types.Transaction, 10), // TODO(arr4n) make the buffer configurable + mempool: sink.NewPriorityMutex(new(queue.Priority[*pendingTx])), + mempoolHasTxs: sink.NewGate(), + quit: quit, // both mempool and executor } if vm.now == nil { vm.now = time.Now } + vm.mempoolHasTxs.Block() // The mempool is initially empty. if err := vm.upgradeLastSynchronousBlock(c.LastSynchronousBlock); err != nil { return nil, err From db02f1071b5013e25a3c0a1eb839afdfd8163ce4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 11 Jul 2025 13:24:11 -0400 Subject: [PATCH 30/42] wip --- go.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.mod b/go.mod index 89f606b..c52df59 100644 --- a/go.mod +++ b/go.mod @@ -116,3 +116,5 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) + +replace github.com/ava-labs/libevm => /Users/stephen/go/src/github.com/ava-labs/libevm From bbd44625b2a8075eab798d7a4c07a73169ca4ff3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 17 Jul 2025 15:51:32 -0400 Subject: [PATCH 31/42] update avalanchego dep --- go.mod | 2 +- go.sum | 6 ++---- vm.go | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index c52df59..7b1d60b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.23.10 require ( github.com/StephenButtolph/canoto v0.17.1 github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa - github.com/ava-labs/avalanchego v1.13.3-0.20250707201933-507e6bbb5e7d + github.com/ava-labs/avalanchego v1.13.3-rc.2 github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1 github.com/dustin/go-humanize v1.0.0 github.com/google/go-cmp v0.7.0 diff --git a/go.sum b/go.sum index 64e2acd..ae831a2 100644 --- a/go.sum +++ b/go.sum @@ -21,10 +21,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa h1:7d3Bkbr8pwxrPnK7AbJzI7Qi0DmLAHIgXmPT26D186w= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa/go.mod h1:TFbsruhH4SB/VO/ONKgNrgBeTLDkpr+uydstjIVyFFQ= -github.com/ava-labs/avalanchego v1.13.3-0.20250707201933-507e6bbb5e7d h1:+IK0mMgrXj2yOxh23ShACLeJlG+XMb6ZDeXda5IFgb0= -github.com/ava-labs/avalanchego v1.13.3-0.20250707201933-507e6bbb5e7d/go.mod h1:QmwzzCZtKaam5OY45kuzq3UinsyRXH75LkP85W7713M= -github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1 h1:vBMYo+Iazw0rGTr+cwjkBdh5eadLPlv4ywI4lKye3CA= -github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1/go.mod h1:+Iol+sVQ1KyoBsHf3veyrBmHCXr3xXRWq6ZXkgVfNLU= +github.com/ava-labs/avalanchego v1.13.3-rc.2 h1:P+XSQqfAuhNPq+RG/dwDQ07o3sMh9ZsyeSgH/OV4y5s= +github.com/ava-labs/avalanchego v1.13.3-rc.2/go.mod h1:dXVZK6Sw3ZPIQ9sLjto0VMWeehExppNHFTWFwyUv5tk= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= diff --git a/vm.go b/vm.go index 5243da8..9fd5f82 100644 --- a/vm.go +++ b/vm.go @@ -219,8 +219,8 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { }, nil } -func (vm *VM) CreateHTTP2Handler(context.Context) (http.Handler, error) { - return nil, errUnimplemented +func (vm *VM) NewHTTPHandler(context.Context) (http.Handler, error) { + return nil, nil } func (vm *VM) GetBlock(ctx context.Context, blkID ids.ID) (*blocks.Block, error) { From 3b70781e198ec5c1794dd58acd3ec125a62de519 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Wed, 23 Jul 2025 01:19:13 +0100 Subject: [PATCH 32/42] feat: `proxytime` package (#8) This package forms the basis of the gas clock, which will extend a `proxytime.Time[gas.Gas]` in a future PR. The `Time.SetRateInvariants()` functionality will be used for changing gas target without affecting gas price, by scaling rate, target, and excess proportionally. All other functionality is necessary for the execution thread. Although the `CmpOpt()` configuration option might feel like overkill, it's necessary for the more extensive, system-wide tests that come later. --------- Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Co-authored-by: Stephen Buttolph --- .github/workflows/lint.yml | 2 +- cmputils/cmputils.go | 30 ++++ go.mod | 11 +- go.sum | 23 ++- proxytime/cmpopt.go | 56 ++++++ proxytime/cmpopt_test.go | 95 ++++++++++ proxytime/proxytime.canoto.go | 225 ++++++++++++++++++++++++ proxytime/proxytime.go | 210 ++++++++++++++++++++++ proxytime/proxytime_test.go | 321 ++++++++++++++++++++++++++++++++++ tools.go | 11 ++ 10 files changed, 980 insertions(+), 4 deletions(-) create mode 100644 cmputils/cmputils.go create mode 100644 proxytime/cmpopt.go create mode 100644 proxytime/cmpopt_test.go create mode 100644 proxytime/proxytime.canoto.go create mode 100644 proxytime/proxytime.go create mode 100644 proxytime/proxytime_test.go create mode 100644 tools.go diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 336d033..473c709 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,7 +36,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.60 + version: v1.63.3 yamllint: runs-on: ubuntu-latest diff --git a/cmputils/cmputils.go b/cmputils/cmputils.go new file mode 100644 index 0000000..6ecedc5 --- /dev/null +++ b/cmputils/cmputils.go @@ -0,0 +1,30 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +//go:build !prod && !nocmpopts + +// Package cmputils provides [cmp] options and utilities for their creation. +package cmputils + +import ( + "reflect" + + "github.com/google/go-cmp/cmp" +) + +// IfIn returns a filtered equivalent of `opt` such that it is only evaluated if +// the [cmp.Path] includes at least one `T`. This is typically used for struct +// fields (and sub-fields). +func IfIn[T any](opt cmp.Option) cmp.Option { + return cmp.FilterPath(pathIncludes[T], opt) +} + +func pathIncludes[T any](p cmp.Path) bool { + t := reflect.TypeFor[T]() + for _, step := range p { + if step.Type() == t { + return true + } + } + return false +} diff --git a/go.mod b/go.mod index 701da80..4c39611 100644 --- a/go.mod +++ b/go.mod @@ -6,28 +6,36 @@ toolchain go1.23.10 require ( github.com/ava-labs/avalanchego v1.13.2 + github.com/google/go-cmp v0.6.0 + github.com/stretchr/testify v1.10.0 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b ) require ( github.com/BurntSushi/toml v1.5.0 // indirect github.com/DataDog/zstd v1.5.2 // indirect - github.com/StephenButtolph/canoto v0.15.0 // indirect + github.com/StephenButtolph/canoto v0.17.1 github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/structtag v1.2.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/gorilla/rpc v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mr-tron/base58 v1.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/supranational/blst v0.3.14 // indirect go.opentelemetry.io/otel v1.22.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 // indirect @@ -50,4 +58,5 @@ require ( google.golang.org/grpc v1.66.0 // indirect google.golang.org/protobuf v1.35.2 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e007a7d..05c8171 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/StephenButtolph/canoto v0.15.0 h1:3iGdyTSQZ7/y09WaJCe0O/HIi53ZyTrnmVzfCqt64mM= -github.com/StephenButtolph/canoto v0.15.0/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE= +github.com/StephenButtolph/canoto v0.17.1 h1:WnN5czIHHALq7pwc+Z2F1sCsKJCDhxlq0zL0YK1etHc= +github.com/StephenButtolph/canoto v0.17.1/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE= github.com/ava-labs/avalanchego v1.13.2 h1:Kx/T2a8vqLlgHde3DWT5zMF5yBIh1rqEd6nJQMMzV/Y= github.com/ava-labs/avalanchego v1.13.2/go.mod h1:s7W/kim5L6hiD2PB1v/Ozy1ZZyoLQ4H6mxVO0aMnxng= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -12,8 +12,11 @@ github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqy github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -33,6 +36,12 @@ github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= @@ -47,8 +56,15 @@ github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.1 h1:dwnrSypP6q56o3lFxTU+t2fwQ9A+U5qrXVO4Qg9KwVU= github.com/sanity-io/litter v1.5.1/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= @@ -104,6 +120,9 @@ google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/proxytime/cmpopt.go b/proxytime/cmpopt.go new file mode 100644 index 0000000..8bb7d24 --- /dev/null +++ b/proxytime/cmpopt.go @@ -0,0 +1,56 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +//go:build !prod && !nocmpopts + +package proxytime + +import ( + "fmt" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/ava-labs/strevm/cmputils" +) + +// A CmpRateInvariantsBy value configures [CmpOpt] treatment of rate-invariant +// values. +type CmpRateInvariantsBy uint64 + +// Valid [CmpRateInvariantsBy] values. +const ( + CmpRateInvariantsByValue CmpRateInvariantsBy = iota + IgnoreRateInvariants +) + +// CmpOpt returns a configuration for [cmp.Diff] to compare [Time] instances in +// tests. The option will only be applied to the specific [Duration] type. +func CmpOpt[D Duration](invariants CmpRateInvariantsBy) cmp.Option { + return cmp.Options{ + cmp.AllowUnexported(Time[D]{}), + cmpopts.IgnoreTypes(canotoData_Time{}), + invariantsOpt[D](invariants), + } +} + +func invariantsOpt[D Duration](by CmpRateInvariantsBy) (opt cmp.Option) { + defer func() { + opt = cmputils.IfIn[*Time[D]](opt) + }() + + switch by { + case IgnoreRateInvariants: + return cmpopts.IgnoreTypes([]*D{}) + + case CmpRateInvariantsByValue: + return cmp.Transformer("rate_invariants_as_values", func(ptrs []*D) (vals []D) { + for _, x := range ptrs { + vals = append(vals, *x) // [Time.SetRateInvariants] requires that they aren't nil. + } + return vals + }) + } + + panic(fmt.Sprintf("Unsupported %T value: %d", by, by)) +} diff --git a/proxytime/cmpopt_test.go b/proxytime/cmpopt_test.go new file mode 100644 index 0000000..8666e3c --- /dev/null +++ b/proxytime/cmpopt_test.go @@ -0,0 +1,95 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package proxytime + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" +) + +func TestCmpOpt(t *testing.T) { + // There is enough logic in [CmpOpt] treatment of rate invariants that it + // warrants testing the test code. + + defaultOpt := CmpOpt[uint64](CmpRateInvariantsByValue) + + withRateInvariants := func(xs ...*uint64) *Time[uint64] { + tm := New[uint64](42, 1) + tm.SetRateInvariants(xs...) + return tm + } + zeroA := new(uint64) + zeroB := new(uint64) + one := new(uint64) + *one = 1 + + tests := []struct { + name string + a, b *Time[uint64] + opt cmp.Option + wantEq bool + }{ + { + name: "same_time_no_invariants", + a: New[uint64](42, 1), + b: New[uint64](42, 1), + opt: defaultOpt, + wantEq: true, + }, + { + name: "different_rate", + a: New[uint64](42, 1), + b: New[uint64](42, 2), + opt: defaultOpt, + wantEq: false, + }, + { + name: "different_unix_time", + a: New[uint64](42, 1), + b: New[uint64](41, 1), + opt: defaultOpt, + wantEq: false, + }, + { + name: "different_fractional_second", + a: New[uint64](42, 100), + b: func() *Time[uint64] { + tm := New[uint64](42, 100) + tm.Tick(1) + return tm + }(), + opt: defaultOpt, + wantEq: false, + }, + { + name: "different_but_ignored_invariants", + a: withRateInvariants(zeroA), + b: withRateInvariants(one), + opt: CmpOpt[uint64](IgnoreRateInvariants), + wantEq: true, + }, + { + name: "equal_invariants_compared_by_value", + a: withRateInvariants(zeroA), + b: withRateInvariants(zeroB), + opt: CmpOpt[uint64](CmpRateInvariantsByValue), + wantEq: true, + }, + { + name: "unequal_invariants_compared_by_value", + a: withRateInvariants(zeroA), + b: withRateInvariants(one), + opt: CmpOpt[uint64](CmpRateInvariantsByValue), + wantEq: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wantEq, cmp.Equal(tt.a, tt.b, tt.opt)) + }) + } +} diff --git a/proxytime/proxytime.canoto.go b/proxytime/proxytime.canoto.go new file mode 100644 index 0000000..ed4aefa --- /dev/null +++ b/proxytime/proxytime.canoto.go @@ -0,0 +1,225 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.17.1 +// source: proxytime.go + +package proxytime + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that unused imports do not error +var ( + _ atomic.Uint64 + + _ = io.ErrUnexpectedEOF +) + +const ( + canoto__Time__seconds__tag = "\x08" // canoto.Tag(1, canoto.Varint) + canoto__Time__fraction__tag = "\x10" // canoto.Tag(2, canoto.Varint) + canoto__Time__hertz__tag = "\x18" // canoto.Tag(3, canoto.Varint) +) + +type canotoData_Time struct { + size atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*Time[T1]) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero Time[T1] + s := &canoto.Spec{ + Name: "Time", + Fields: []canoto.FieldType{ + { + FieldNumber: 1, + Name: "seconds", + OneOf: "", + TypeUint: canoto.SizeOf(zero.seconds), + }, + { + FieldNumber: 2, + Name: "fraction", + OneOf: "", + TypeUint: canoto.SizeOf(zero.fraction), + }, + { + FieldNumber: 3, + Name: "hertz", + OneOf: "", + TypeUint: canoto.SizeOf(zero.hertz), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*Time[T1]) MakeCanoto() *Time[T1] { + return new(Time[T1]) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *Time[T1]) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *Time[T1]) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = Time[T1]{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.seconds); err != nil { + return err + } + if canoto.IsZero(c.seconds) { + return canoto.ErrZeroValue + } + case 2: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.fraction); err != nil { + return err + } + if canoto.IsZero(c.fraction) { + return canoto.ErrZeroValue + } + case 3: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.hertz); err != nil { + return err + } + if canoto.IsZero(c.hertz) { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *Time[T1]) ValidCanoto() bool { + if c == nil { + return true + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *Time[T1]) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if !canoto.IsZero(c.seconds) { + size += uint64(len(canoto__Time__seconds__tag)) + canoto.SizeUint(c.seconds) + } + if !canoto.IsZero(c.fraction) { + size += uint64(len(canoto__Time__fraction__tag)) + canoto.SizeUint(c.fraction) + } + if !canoto.IsZero(c.hertz) { + size += uint64(len(canoto__Time__hertz__tag)) + canoto.SizeUint(c.hertz) + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *Time[T1]) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *Time[T1]) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *Time[T1]) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if !canoto.IsZero(c.seconds) { + canoto.Append(&w, canoto__Time__seconds__tag) + canoto.AppendUint(&w, c.seconds) + } + if !canoto.IsZero(c.fraction) { + canoto.Append(&w, canoto__Time__fraction__tag) + canoto.AppendUint(&w, c.fraction) + } + if !canoto.IsZero(c.hertz) { + canoto.Append(&w, canoto__Time__hertz__tag) + canoto.AppendUint(&w, c.hertz) + } + return w +} diff --git a/proxytime/proxytime.go b/proxytime/proxytime.go new file mode 100644 index 0000000..5801651 --- /dev/null +++ b/proxytime/proxytime.go @@ -0,0 +1,210 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package proxytime measures the passage of time based on a proxy unit and +// associated unit rate. +package proxytime + +import ( + "cmp" + "fmt" + "math" + "math/bits" + "time" + + "github.com/ava-labs/strevm/intmath" +) + +//go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE + +// A Duration is a type parameter for use as the unit of passage of [Time]. +type Duration interface { + ~uint64 +} + +// Time represents an instant in time, its passage measured by an arbitrary unit +// of duration. It is not thread safe nor is the zero value valid. +type Time[D Duration] struct { + seconds uint64 `canoto:"uint,1"` + // invariant: fraction < hertz + fraction D `canoto:"uint,2"` + hertz D `canoto:"uint,3"` + + rateInvariants []*D + + canotoData canotoData_Time `canoto:"nocopy"` +} + +// IMPORTANT: keep [Time.Clone] next to the struct definition to make it easier +// to check that all fields are copied. + +// Clone returns a copy of the time. Note that it does NOT copy the pointers +// passed to [Time.SetRateInvariants] as this risks coupling the clone with the +// wrong invariants. +func (tm *Time[D]) Clone() *Time[D] { + return &Time[D]{ + seconds: tm.seconds, + fraction: tm.fraction, + hertz: tm.hertz, + } +} + +// New returns a new [Time], set from a Unix timestamp. The passage of `hertz` +// units is equivalent to a tick of 1 second. +func New[D Duration](unixSeconds uint64, hertz D) *Time[D] { + return &Time[D]{ + seconds: unixSeconds, + hertz: hertz, + } +} + +// Unix returns tm as a Unix timestamp. +func (tm *Time[D]) Unix() uint64 { + return tm.seconds +} + +// A FractionalSecond represents a sub-second duration of time. The numerator is +// equivalent to a value passed to [Time.Tick] when [Time.Rate] is the +// denominator. +type FractionalSecond[D Duration] struct { + Numerator, Denominator D +} + +// Fraction returns the fractional-second component of the time, denominated in +// [Time.Rate]. +func (tm *Time[D]) Fraction() FractionalSecond[D] { + return FractionalSecond[D]{tm.fraction, tm.hertz} +} + +// Rate returns the proxy duration required for the passage of one second. +func (tm *Time[D]) Rate() D { + return tm.hertz +} + +// Tick advances the time by `d`. +func (tm *Time[D]) Tick(d D) { + frac, carry := bits.Add64(uint64(tm.fraction), uint64(d), 0) + quo, rem := bits.Div64(carry, frac, uint64(tm.hertz)) + tm.seconds += quo + tm.fraction = D(rem) +} + +// FastForwardTo sets the time to the specified Unix timestamp if it is in the +// future, returning the integer and fraction number of seconds by which the +// time was advanced. +func (tm *Time[D]) FastForwardTo(to uint64) (uint64, FractionalSecond[D]) { + if to <= tm.seconds { + return 0, FractionalSecond[D]{0, tm.hertz} + } + + sec := to - tm.seconds + var frac D + if tm.fraction > 0 { + frac = tm.hertz - tm.fraction + sec-- + } + + tm.seconds = to + tm.fraction = 0 + + return sec, FractionalSecond[D]{frac, tm.hertz} +} + +// SetRate changes the unit rate at which time passes. The requisite integer +// division may result in rounding down of the fractional-second component of +// time, the amount of which is returned. +// +// If no values have been registered with [Time.SetRateInvariants] then SetRate +// will always return a nil error. A non-nil error will only be returned if any +// of the rate-invariant values overflows a uint64 due to the scaling. +func (tm *Time[D]) SetRate(hertz D) (truncated FractionalSecond[D], err error) { + frac, truncated, err := tm.scale(tm.fraction, hertz) + if err != nil { + // If this happens then there is a bug in the implementation. The + // invariant that `tm.fraction < tm.hertz` makes overflow impossible as + // the scaled fraction will be less than the new rate. + return FractionalSecond[D]{}, fmt.Errorf("fractional-second time: %w", err) + } + + // Avoid scaling some but not all rate invariants if one results in an + // error. + scaled := make([]D, len(tm.rateInvariants)) + for i, v := range tm.rateInvariants { + scaled[i], _, err = tm.scale(*v, hertz) + if err != nil { + return FractionalSecond[D]{}, fmt.Errorf("rate invariant [%d]: %w", i, err) + } + } + for i, v := range tm.rateInvariants { + *v = scaled[i] + } + + tm.fraction = frac + tm.hertz = hertz + return truncated, nil +} + +// SetRateInvariants sets units that, whenever [Time.SetRate] is called, will be +// scaled relative to the change in rate. Scaling may be affected by the same +// truncation described for [Time.SetRate]. Truncation aside, the rational +// numbers formed by the invariants divided by the rate will each remain equal +// despite their change in denominator. +// +// The pointers MUST NOT be nil. +func (tm *Time[D]) SetRateInvariants(inv ...*D) { + tm.rateInvariants = inv +} + +// scale returns `val`, scaled from the existing [Time.Rate] to the newly +// specified one. See [Time.SetRate] for details about truncation and overflow +// errors. +func (tm *Time[D]) scale(val, newRate D) (scaled D, truncated FractionalSecond[D], err error) { + scaled, trunc, err := intmath.MulDiv(val, newRate, tm.hertz) + if err != nil { + return 0, FractionalSecond[D]{}, fmt.Errorf("scaling %d from rate of %d to %d: %w", val, tm.hertz, newRate, err) + } + return scaled, FractionalSecond[D]{Numerator: trunc, Denominator: tm.hertz}, nil +} + +// Compare returns +// +// -1 if tm is before u +// 0 if tm and u represent the same instant +// +1 if tm is after u. +// +// Results are undefined if [Time.Rate] is different for the two instants. +func (tm *Time[D]) Compare(u *Time[D]) int { + if c := cmp.Compare(tm.seconds, u.seconds); c != 0 { + return c + } + return cmp.Compare(tm.fraction, u.fraction) +} + +// CompareUnix is equivalent to [Time.Compare] against a zero-fractional-second +// instant in time. Note that it does NOT only compare the seconds and that if +// `tm` has the same [Time.Unix] as `sec` but non-zero [Time.Fraction] then +// CompareUnix will return 1. +func (tm *Time[D]) CompareUnix(sec uint64) int { + return tm.Compare(&Time[D]{seconds: sec}) +} + +// AsTime converts the proxy time to a standard [time.Time] in UTC. AsTime is +// analogous to setting a rate of 1e9 (nanosecond), which might result in +// truncation. The second-range limitations documented on [time.Unix] also apply +// to AsTime. +func (tm *Time[D]) AsTime() time.Time { + if tm.seconds > math.MaxInt64 { // keeps gosec linter happy + return time.Unix(math.MaxInt64, math.MaxInt64) + } + // The error can be ignored as the fraction is always less than the rate and + // therefore the scaled value can never overflow. + nsec, _ /*remainder*/, _ := tm.scale(tm.fraction, 1e9) + return time.Unix(int64(tm.seconds), int64(nsec)).In(time.UTC) +} + +// String returns the time as a human-readable string. It is not intended for +// parsing and its format MAY change. +func (tm *Time[D]) String() string { + f := tm.Fraction() + return fmt.Sprintf("%d+(%d/%d)", tm.Unix(), f.Numerator, f.Denominator) +} diff --git a/proxytime/proxytime_test.go b/proxytime/proxytime_test.go new file mode 100644 index 0000000..25bd71a --- /dev/null +++ b/proxytime/proxytime_test.go @@ -0,0 +1,321 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package proxytime + +import ( + "cmp" + "fmt" + "math" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + gocmp "github.com/google/go-cmp/cmp" +) + +func frac(num, den uint64) FractionalSecond[uint64] { + return FractionalSecond[uint64]{Numerator: num, Denominator: den} +} + +func (tm *Time[D]) assertEq(tb testing.TB, desc string, seconds uint64, fraction FractionalSecond[D]) (equal bool) { + tb.Helper() + want := &Time[D]{ + seconds: seconds, + fraction: fraction.Numerator, + hertz: fraction.Denominator, + } + if diff := gocmp.Diff(want, tm, CmpOpt[D](IgnoreRateInvariants)); diff != "" { + tb.Errorf("%s diff (-want +got):\n%s", desc, diff) + return false + } + return true +} + +func (tm *Time[D]) requireEq(tb testing.TB, desc string, seconds uint64, fraction FractionalSecond[D]) { + tb.Helper() + if !tm.assertEq(tb, desc, seconds, fraction) { + tb.FailNow() + } +} + +func TestTickAndCmp(t *testing.T) { + const rate = 500 + tm := New(0, uint64(500)) + tm.assertEq(t, "New(0, ...)", 0, frac(0, rate)) + + steps := []struct { + tick uint64 + wantSec, wantFrac uint64 + }{ + { + tick: 100, + wantSec: 0, wantFrac: 100, + }, + { + tick: 399, + wantSec: 0, wantFrac: 499, + }, + { + // Although this is a no-op, it's useful to see the fraction for + // understanding the next step. + tick: 0, + wantSec: 0, wantFrac: rate - 1, + }, + { + tick: 1, + wantSec: 1, wantFrac: 0, + }, + { + tick: rate, + wantSec: 2, wantFrac: 0, + }, + { + tick: 400, + wantSec: 2, wantFrac: 400, + }, + { + tick: 200, + wantSec: 3, wantFrac: 100, + }, + { + tick: 3*rate + 100, + wantSec: 6, wantFrac: 200, + }, + { + tick: 299, + wantSec: 6, wantFrac: 499, + }, + { + tick: 2, + wantSec: 7, wantFrac: 1, + }, + { + tick: rate - 1, + wantSec: 8, wantFrac: 0, + }, + { + // Set fraction to anything non-zero so we can test overflow + // prevention with a tick of 2^64-1. + tick: 1, + wantSec: 8, wantFrac: 1, + }, + { + tick: math.MaxUint64, + wantSec: 8 + math.MaxUint64/rate, + wantFrac: 1 + math.MaxUint64%rate, + }, + } + + var ticked uint64 + for _, s := range steps { + old := tm.Clone() + + tm.Tick(s.tick) + ticked += s.tick + tm.requireEq(t, fmt.Sprintf("%+d", ticked), s.wantSec, frac(s.wantFrac, rate)) + + if got, want := tm.Compare(old), cmp.Compare(s.tick, 0); got != want { + t.Errorf("After %T.Tick(%d); ticked.Cmp(original) got %d; want %d", tm, s.tick, got, want) + } + if got, want := old.Compare(tm), cmp.Compare(0, s.tick); got != want { + t.Errorf("After %T.Tick(%d); original.Cmp(ticked) got %d; want %d", tm, s.tick, got, want) + } + } +} + +func TestSetRate(t *testing.T) { + const ( + initSeconds = 42 + divisor = 3 + initRate = uint64(1000 * divisor) + ) + tm := New(initSeconds, initRate) + + const tick = uint64(100 * divisor) + tm.Tick(tick) + tm.requireEq(t, "baseline", initSeconds, frac(tick, initRate)) + + const initInvariant = 200 * divisor + invariant := uint64(initInvariant) + tm.SetRateInvariants(&invariant) + + steps := []struct { + newRate, wantNumerator uint64 + wantTruncated FractionalSecond[uint64] + wantInvariant uint64 + }{ + { + newRate: initRate / divisor, // no rounding + wantNumerator: tick / divisor, + wantTruncated: frac(0, 1), + wantInvariant: invariant / divisor, + }, + { + newRate: initRate * 5, + wantNumerator: tick * 5, + wantTruncated: frac(0, 1), // multiplication never has rounding + wantInvariant: invariant * 5, + }, + { + newRate: 15_000, // same as above, but shows the numbers explicitly + wantNumerator: 1_500, + wantTruncated: frac(0, 1), + wantInvariant: 3_000, + }, + { + newRate: 75, + wantNumerator: 7, // 7.5 + wantTruncated: frac(7_500, 15_000), // rounded down by 0.5, denominated in the old rate + wantInvariant: 15, + }, + } + + for _, s := range steps { + old := tm.Rate() + gotTruncated, err := tm.SetRate(s.newRate) + require.NoErrorf(t, err, "%T.SetRate(%d)", tm, s.newRate) + desc := fmt.Sprintf("rate changed from %d to %d", old, s.newRate) + tm.requireEq(t, desc, initSeconds, frac(s.wantNumerator, s.newRate)) + + if gotTruncated.Numerator == 0 && s.wantTruncated.Numerator == 0 { + assert.NotZerof(t, gotTruncated.Denominator, "truncation %T.Denominator with 0 numerator", gotTruncated) + } else { + assert.Equal(t, s.wantTruncated, gotTruncated, "truncation") + } + assert.Equal(t, s.wantInvariant, invariant) + } +} + +func TestAsTime(t *testing.T) { + stdlib := time.Date(1986, time.October, 1, 0, 0, 0, 0, time.UTC) + + const rate uint64 = 500 + tm := New(uint64(stdlib.Unix()), rate) //nolint:gosec // Known to not overflow + if got, want := tm.AsTime(), stdlib; !got.Equal(want) { + t.Fatalf("%T.AsTime() at construction got %v; want %v", tm, got, want) + } + + tm.Tick(1) + if got, want := tm.AsTime(), stdlib.Add(2*time.Millisecond); !got.Equal(want) { + t.Fatalf("%T.AsTime() after ticking 1/%d got %v; want %v", tm, rate, got, want) + } +} + +func TestCanotoRoundTrip(t *testing.T) { + tests := []struct { + name string + seconds, rate, tick uint64 + }{ + { + name: "non_zero_fields", + seconds: 42, + rate: 10_000, + tick: 1_234, + }, + { + name: "zero_seconds", + rate: 100, + tick: 1, + }, + { + name: "zero_fractional_second", + seconds: 999, + rate: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tm := New(tt.seconds, tt.rate) + tm.Tick(tt.tick) + + got := new(Time[uint64]) + require.NoErrorf(t, got.UnmarshalCanoto(tm.MarshalCanoto()), "%T.UnmarshalCanoto(%[1]T.MarshalCanoto())", got) + got.assertEq(t, fmt.Sprintf("%T.UnmarshalCanoto(%[1]T.MarshalCanoto())", tm), tt.seconds, frac(tt.tick, tt.rate)) + }) + } +} + +func TestFastForward(t *testing.T) { + tm := New(42, uint64(1000)) + + steps := []struct { + tickBefore uint64 + ffTo uint64 + wantSec uint64 + wantFrac FractionalSecond[uint64] + }{ + { + tickBefore: 100, // 42.100 + ffTo: 42, // in the past + wantSec: 0, + wantFrac: frac(0, 1000), + }, + { + tickBefore: 0, // 42.100 + ffTo: 43, + wantSec: 0, + wantFrac: frac(900, 1000), + }, + { + tickBefore: 0, // 43.000 + ffTo: 44, + wantSec: 1, + wantFrac: frac(0, 1000), + }, + { + tickBefore: 200, // 44.200 + ffTo: 50, + wantSec: 5, + wantFrac: frac(800, 1000), + }, + } + + for _, s := range steps { + tm.Tick(s.tickBefore) + gotSec, gotFrac := tm.FastForwardTo(s.ffTo) + assert.Equal(t, s.wantSec, gotSec) + assert.Equal(t, s.wantFrac, gotFrac) + + if t.Failed() { + t.FailNow() + } + } +} + +func TestCmpUnix(t *testing.T) { + tests := []struct { + tm *Time[uint64] + tick uint64 + cmpAgainst uint64 + want int + }{ + { + tm: New[uint64](42, 1e6), + cmpAgainst: 42, + want: 0, + }, + { + tm: New[uint64](42, 1e6), + tick: 1, + cmpAgainst: 42, + want: 1, + }, + { + tm: New[uint64](41, 100), + tick: 99, + cmpAgainst: 42, + want: -1, + }, + } + + for _, tt := range tests { + tt.tm.Tick(tt.tick) + if got := tt.tm.CompareUnix(tt.cmpAgainst); got != tt.want { + t.Errorf("Time{%d + %d/%d}.CmpUnix(%d) got %d; want %d", tt.tm.Unix(), tt.tm.fraction, tt.tm.hertz, tt.cmpAgainst, got, tt.want) + } + } +} diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..dd6e0d7 --- /dev/null +++ b/tools.go @@ -0,0 +1,11 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +//go:build tools + +package strevm + +// Protects indirect dependencies of tools from being pruned by `go mod tidy`. +import ( + _ "github.com/StephenButtolph/canoto/canoto" +) From c07db95b8e9914c94e12439d768463042251f896 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 23 Jul 2025 01:23:24 +0100 Subject: [PATCH 33/42] feat: `gastime` package --- gastime/cmpopt.go | 24 ++++ gastime/gastime.go | 112 +++++++++++++++++ gastime/gastime_test.go | 242 ++++++++++++++++++++++++++++++++++++ gastime/marshal.canoto.go | 252 ++++++++++++++++++++++++++++++++++++++ gastime/marshal.go | 49 ++++++++ go.mod | 1 + go.sum | 2 + 7 files changed, 682 insertions(+) create mode 100644 gastime/cmpopt.go create mode 100644 gastime/gastime.go create mode 100644 gastime/gastime_test.go create mode 100644 gastime/marshal.canoto.go create mode 100644 gastime/marshal.go diff --git a/gastime/cmpopt.go b/gastime/cmpopt.go new file mode 100644 index 0000000..0844bc3 --- /dev/null +++ b/gastime/cmpopt.go @@ -0,0 +1,24 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +//go:build !prod && !nocmpopts + +package gastime + +import ( + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/ava-labs/strevm/proxytime" +) + +// CmpOpt returns a configuration for [cmp.Diff] to compare [Time] instances in +// tests. +func CmpOpt() cmp.Option { + return cmp.Options{ + cmp.AllowUnexported(TimeMarshaler{}), + cmpopts.IgnoreTypes(canotoData_TimeMarshaler{}), + proxytime.CmpOpt[gas.Gas](proxytime.CmpRateInvariantsByValue), + } +} diff --git a/gastime/gastime.go b/gastime/gastime.go new file mode 100644 index 0000000..8a2533a --- /dev/null +++ b/gastime/gastime.go @@ -0,0 +1,112 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package gastime measures time based on the consumption of gas. +package gastime + +import ( + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/holiman/uint256" + + "github.com/ava-labs/strevm/intmath" + "github.com/ava-labs/strevm/proxytime" +) + +// Time represents an instant in time, its passage measured in [gas.Gas] +// consumption. It is not thread safe nor is the zero value valid. +// +// In addition to the passage of time, it also tracks excess consumption above a +// target, as described in [ACP-194] as a "continuous" version of [ACP-176]. +// +// Copying a Time, either directly or by dereferencing a pointer, will result in +// undefined behaviour. Use [Time.Clone] instead as it reestablishes internal +// invariants. +// +// [ACP-176]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/176-dynamic-evm-gas-limit-and-price-discovery-updates +// [ACP-194]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution +type Time struct { + TimeMarshaler +} + +// makeTime is a constructor shared by [New] and [Time.Clone]. +func makeTime(t *proxytime.Time[gas.Gas], target, excess gas.Gas) *Time { + tm := &Time{ + TimeMarshaler: TimeMarshaler{ + Time: t, + target: target, + excess: excess, + }, + } + tm.establishInvariants() + return tm +} + +func (tm *Time) establishInvariants() { + tm.Time.SetRateInvariants(&tm.target, &tm.excess) +} + +// New returns a new [Time], set from a Unix timestamp. The consumption of +// `2*target` units of [gas.Gas] is equivalent to a tick of 1 second. +func New(unixSeconds uint64, target, startingExcess gas.Gas) *Time { + return makeTime(proxytime.New(unixSeconds, 2*target), target, startingExcess) +} + +// Clone returns a deep copy of the time. +func (tm *Time) Clone() *Time { + // [proxytime.Time.Clone] explicitly does NOT clone the rate invariants, so + // we reestablish them as if we were constructing a new instance. + return makeTime(tm.Time.Clone(), tm.target, tm.excess) +} + +// Target returns the `T` parameter of ACP-176. +func (tm *Time) Target() gas.Gas { + return tm.target +} + +// Excess returns the `x` variable of ACP-176. +func (tm *Time) Excess() gas.Gas { + return tm.excess +} + +// Price returns the price of a unit of gas, i.e. the "base fee". +func (tm *Time) Price() gas.Price { + return gas.CalculatePrice(1 /* M */, tm.excess, 87*tm.target /* K */) +} + +// BaseFee is equivalent to [Time.Price], returning the result as a uint256 for +// compatibility with geth/libevm objects. +func (tm *Time) BaseFee() *uint256.Int { + return uint256.NewInt(uint64(tm.Price())) +} + +// SetTarget changes the target gas consumption per second. It is equivalent to +// [proxytime.Time.SetRate] with `2*t`, but is preferred as it avoids +// accidentally setting an odd rate. It returns an error if the scaled +// [Time.Excess] overflows as a result of the scaling. +func (tm *Time) SetTarget(t gas.Gas) error { + _, err := tm.SetRate(2 * t) // also updates target as it was passed to [proxytime.Time.SetRateInvariants] + return err +} + +// Tick is equivalent to [proxytime.Time.Tick] except that it also updates the +// gas excess. +func (tm *Time) Tick(g gas.Gas) { + tm.Time.Tick(g) + + R, T := tm.Rate(), tm.Target() + quo, _, _ := intmath.MulDiv(g, R-T, R) // overflow is impossible as (R-T)/R < 1 + tm.excess += quo +} + +// FastForwardTo is equivalent to [proxytime.Time.FastForwardTo] except that it +// may also update the gas excess. +func (tm *Time) FastForwardTo(to uint64) { + sec, frac := tm.Time.FastForwardTo(to) + if sec == 0 && frac.Numerator == 0 { + return + } + + R, T := tm.Rate(), tm.Target() + quo, _, _ := intmath.MulDiv(R*gas.Gas(sec)+frac.Numerator, T, R) // overflow is impossible as T/R < 1 + tm.excess = intmath.BoundedSubtract(tm.excess, quo, 0) +} diff --git a/gastime/gastime_test.go b/gastime/gastime_test.go new file mode 100644 index 0000000..465a1dc --- /dev/null +++ b/gastime/gastime_test.go @@ -0,0 +1,242 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gastime + +import ( + "testing" + + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/strevm/intmath" + "github.com/ava-labs/strevm/proxytime" +) + +func (tm *Time) cloneViaCanotoRoundTrip(tb testing.TB) *Time { + tb.Helper() + x := new(Time) + require.NoErrorf(tb, x.UnmarshalCanoto(tm.MarshalCanoto()), "%T.UnmarshalCanoto(%[1]T.MarshalCanoto())", tm) + return x +} + +func TestClone(t *testing.T) { + tm := New(42, 1e6, 1e5) + tm.Tick(1) + + if diff := cmp.Diff(tm, tm.Clone(), CmpOpt()); diff != "" { + t.Errorf("%T.Clone() diff (-want +got):\n%s", tm, diff) + } + if diff := cmp.Diff(tm, tm.cloneViaCanotoRoundTrip(t), CmpOpt()); diff != "" { + t.Errorf("%T.UnmarshalCanoto(%[1]T.MarshalCanoto()) diff (-want +got):\n%s", tm, diff) + } +} + +// state captures parameters about a [Time] for assertion in tests. It includes +// both explicit (i.e. struct fields) and derived parameters (e.g. gas price), +// which aid testing of behaviour and invariants in a more fine-grained manner +// than direct comparison of two instances. +type state struct { + UnixTime uint64 + ConsumedThisSecond proxytime.FractionalSecond[gas.Gas] + Rate, Target, Excess gas.Gas + Price gas.Price +} + +func (tm *Time) state() state { + return state{ + UnixTime: tm.Unix(), + ConsumedThisSecond: tm.Fraction(), + Rate: tm.Rate(), + Target: tm.Target(), + Excess: tm.Excess(), + Price: tm.Price(), + } +} + +func (tm *Time) requireState(tb testing.TB, desc string, want state, opts ...cmp.Option) { + tb.Helper() + if diff := cmp.Diff(want, tm.state(), opts...); diff != "" { + tb.Fatalf("%s (-want +got):\n%s", desc, diff) + } +} + +func (tm *Time) mustSetRate(tb testing.TB, rate gas.Gas) { + tb.Helper() + _, err := tm.SetRate(rate) + require.NoError(tb, err, "%T.SetRate(%d)", tm, rate) +} + +func (tm *Time) mustSetTarget(tb testing.TB, target gas.Gas) { + tb.Helper() + require.NoError(tb, tm.SetTarget(target), "%T.SetTarget(%d)", tm, target) +} + +func TestScaling(t *testing.T) { + const initExcess = gas.Gas(1_234_567_890) + tm := New(42, 1.6e6, initExcess) + + // The initial price isn't important in this test; what we care about is + // that it's invariant under scaling of the target etc. + initPrice := tm.Price() + if initPrice == 1 { + t.Fatalf("Bad test setup: increase initial excess to achieve %T > 1", initPrice) + } + + ignore := cmpopts.IgnoreFields(state{}, "UnixTime", "ConsumedThisSecond") + + tm.requireState(t, "initial", state{ + Rate: 3.2e6, + Target: 1.6e6, + Excess: initExcess, + Price: initPrice, + }, ignore) + + tm.mustSetTarget(t, 3.2e6) + tm.requireState(t, "after SetTarget()", state{ + Rate: 6.4e6, + Target: 3.2e6, + Excess: 2 * initExcess, + Price: initPrice, // unchanged + }, ignore) + + // SetRate is identical to setting via the target, as long as the rate is + // even. Although the documentation states that SetTarget is preferred, we + // still need to test SetRate. + tm.mustSetRate(t, 4e6) + want := state{ + Rate: 4e6, + Target: 2e6, + Excess: (func() gas.Gas { + // Scale the _initial_ excess relative to the new and _initial_ + // rates, not the most recent rate before scaling. + x, _, err := intmath.MulDiv(initExcess, 4e6, 3.2e6) + require.NoErrorf(t, err, "intmath.MulDiv(%d, %d, %d)", initExcess, 4e6, 3.2e6) + return x + })(), + Price: initPrice, // unchanged + } + tm.requireState(t, "after SetRate()", want, ignore) + + testPostClone := func(t *testing.T, cloned *Time) { + t.Helper() + want := want + cloned.requireState(t, "unchanged immediately after clone", want, ignore) + + cloned.mustSetRate(t, cloned.Rate()*2) + tm.requireState(t, "original Time unchanged by setting clone's rate", want, ignore) + + want.Rate *= 2 + want.Target *= 2 + want.Excess *= 2 + cloned.requireState(t, "scaling after clone and then SetRate()", want, ignore) + } + + t.Run("clone", func(t *testing.T) { + testPostClone(t, tm.Clone()) + }) + + t.Run("canoto_roundtrip", func(t *testing.T) { + testPostClone(t, tm.cloneViaCanotoRoundTrip(t)) + }) +} + +func TestExcess(t *testing.T) { + const rate = gas.Gas(3.2e6) + tm := New(42, rate/2, 0) + + frac := func(num gas.Gas) (f proxytime.FractionalSecond[gas.Gas]) { + f.Numerator = num + f.Denominator = rate + return f + } + + ignore := cmpopts.IgnoreFields(state{}, "Rate", "Target", "Price") + + tm.requireState(t, "initial", state{ + UnixTime: 42, + ConsumedThisSecond: frac(0), + Excess: 0, + }, ignore) + + // NOTE: when R = 2T, excess increases or decreases by half the passage of + // time, depending on whether time was Tick()ed or FastForward()ed, + // respectively. + + steps := []struct { + desc string + // Only one of fast-forwarding or ticking per step. + ffToBefore uint64 + tickBefore gas.Gas + want state + }{ + { + desc: "initial tick 1/2s", + tickBefore: rate / 2, + want: state{ + UnixTime: 42, + ConsumedThisSecond: frac(rate / 2), + Excess: (rate / 2) / 2, + }, + }, + { + desc: "total tick 3/4s", + tickBefore: rate / 4, + want: state{ + UnixTime: 42, + ConsumedThisSecond: frac(3 * rate / 4), + Excess: 3 * rate / 8, + }, + }, + { + desc: "total tick 5/4s", + tickBefore: rate / 2, + want: state{ + UnixTime: 43, + ConsumedThisSecond: frac(rate / 4), + Excess: 5 * rate / 8, + }, + }, + { + desc: "total tick 11.25s", + tickBefore: 10 * rate, + want: state{ + UnixTime: 53, + ConsumedThisSecond: frac(rate / 4), + Excess: 45 * rate / 8, // (11*4 + 1) quarters of ticking, halved + }, + }, + { + desc: "no op fast forward", + ffToBefore: 53, + want: state{ // unchanged + UnixTime: 53, + ConsumedThisSecond: frac(rate / 4), + Excess: 45 * rate / 8, + }, + }, + { + desc: "fast forward 11.25s to 13s", + ffToBefore: 55, + want: state{ + UnixTime: 55, + ConsumedThisSecond: frac(0), + Excess: 45*rate/8 - 7*rate/8, + }, + }, + } + + for _, s := range steps { + switch ff, tk := s.ffToBefore, s.tickBefore; { + case ff > 0 && tk > 0: + t.Fatalf("Bad test setup (%q) only FastForward() or Tick() before", s.desc) + case ff > 0: + tm.FastForwardTo(ff) + case tk > 0: + tm.Tick(tk) + } + tm.requireState(t, s.desc, s.want, ignore) + } +} diff --git a/gastime/marshal.canoto.go b/gastime/marshal.canoto.go new file mode 100644 index 0000000..25aaa08 --- /dev/null +++ b/gastime/marshal.canoto.go @@ -0,0 +1,252 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.17.1 +// source: marshal.go + +package gastime + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that unused imports do not error +var ( + _ atomic.Uint64 + + _ = io.ErrUnexpectedEOF +) + +const ( + canoto__TimeMarshaler__Time__tag = "\x0a" // canoto.Tag(1, canoto.Len) + canoto__TimeMarshaler__target__tag = "\x10" // canoto.Tag(2, canoto.Varint) + canoto__TimeMarshaler__excess__tag = "\x18" // canoto.Tag(3, canoto.Varint) +) + +type canotoData_TimeMarshaler struct { + size atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*TimeMarshaler) CanotoSpec(types ...reflect.Type) *canoto.Spec { + types = append(types, reflect.TypeOf(TimeMarshaler{})) + var zero TimeMarshaler + s := &canoto.Spec{ + Name: "TimeMarshaler", + Fields: []canoto.FieldType{ + canoto.FieldTypeFromField( + /*type inference:*/ (zero.Time), + /*FieldNumber: */ 1, + /*Name: */ "Time", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*types: */ types, + ), + { + FieldNumber: 2, + Name: "target", + OneOf: "", + TypeUint: canoto.SizeOf(zero.target), + }, + { + FieldNumber: 3, + Name: "excess", + OneOf: "", + TypeUint: canoto.SizeOf(zero.excess), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*TimeMarshaler) MakeCanoto() *TimeMarshaler { + return new(TimeMarshaler) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *TimeMarshaler) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *TimeMarshaler) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = TimeMarshaler{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the bytes for the field. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + return canoto.ErrZeroValue + } + r.Unsafe = originalUnsafe + + // Unmarshal the field from the bytes. + remainingBytes := r.B + r.B = msgBytes + c.Time = canoto.MakePointer(c.Time) + if err := (c.Time).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + case 2: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.target); err != nil { + return err + } + if canoto.IsZero(c.target) { + return canoto.ErrZeroValue + } + case 3: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.excess); err != nil { + return err + } + if canoto.IsZero(c.excess) { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *TimeMarshaler) ValidCanoto() bool { + if c == nil { + return true + } + if c.Time != nil && !(c.Time).ValidCanoto() { + return false + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *TimeMarshaler) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if c.Time != nil { + (c.Time).CalculateCanotoCache() + if fieldSize := (c.Time).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canoto__TimeMarshaler__Time__tag)) + canoto.SizeUint(fieldSize) + fieldSize + } + } + if !canoto.IsZero(c.target) { + size += uint64(len(canoto__TimeMarshaler__target__tag)) + canoto.SizeUint(c.target) + } + if !canoto.IsZero(c.excess) { + size += uint64(len(canoto__TimeMarshaler__excess__tag)) + canoto.SizeUint(c.excess) + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *TimeMarshaler) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TimeMarshaler) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TimeMarshaler) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if c.Time != nil { + if fieldSize := (c.Time).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canoto__TimeMarshaler__Time__tag) + canoto.AppendUint(&w, fieldSize) + w = (c.Time).MarshalCanotoInto(w) + } + } + if !canoto.IsZero(c.target) { + canoto.Append(&w, canoto__TimeMarshaler__target__tag) + canoto.AppendUint(&w, c.target) + } + if !canoto.IsZero(c.excess) { + canoto.Append(&w, canoto__TimeMarshaler__excess__tag) + canoto.AppendUint(&w, c.excess) + } + return w +} diff --git a/gastime/marshal.go b/gastime/marshal.go new file mode 100644 index 0000000..e013a6c --- /dev/null +++ b/gastime/marshal.go @@ -0,0 +1,49 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gastime + +import ( + "github.com/StephenButtolph/canoto" + "github.com/ava-labs/avalanchego/vms/components/gas" + + "github.com/ava-labs/strevm/proxytime" +) + +//go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE + +// A TimeMarshaler can marshal a time to and from canoto. It is of limited use +// by itself and SHOULD only be used via a wrapping [Time]. +type TimeMarshaler struct { //nolint:tagliatelle // TODO(arr4n) submit linter bug report + *proxytime.Time[gas.Gas] `canoto:"pointer,1"` + target gas.Gas `canoto:"uint,2"` + excess gas.Gas `canoto:"uint,3"` + + // The nocopy is important, not only for canoto, but because of the use of + // pointers in [Time.establishInvariants]. See [Time.Clone]. + canotoData canotoData_TimeMarshaler `canoto:"nocopy"` +} + +var _ canoto.Message = (*Time)(nil) + +// MakeCanoto creates a new empty value. +func (*Time) MakeCanoto() *Time { return new(Time) } + +// UnmarshalCanoto unmarshals the bytes into the [TimeMarshaler] and then +// reestablishes invariants. +func (tm *Time) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return tm.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the [TimeMarshaler] from the reader and then +// reestablishes invariants. +func (tm *Time) UnmarshalCanotoFrom(r canoto.Reader) error { + if err := tm.TimeMarshaler.UnmarshalCanotoFrom(r); err != nil { + return err + } + tm.establishInvariants() + return nil +} diff --git a/go.mod b/go.mod index 4c39611..64ce228 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.23.10 require ( github.com/ava-labs/avalanchego v1.13.2 github.com/google/go-cmp v0.6.0 + github.com/holiman/uint256 v1.2.4 github.com/stretchr/testify v1.10.0 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b ) diff --git a/go.sum b/go.sum index 05c8171..4eb8c3a 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= +github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From 4f6ed22a0f93a83e75a5c6e994d6b2250714bd9d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 13 Aug 2025 13:54:34 -0400 Subject: [PATCH 34/42] update dependencies --- go.mod | 25 +++++++++++-------------- go.sum | 44 ++++++++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index 7b1d60b..648fa2c 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ toolchain go1.23.10 require ( github.com/StephenButtolph/canoto v0.17.1 github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa - github.com/ava-labs/avalanchego v1.13.3-rc.2 - github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1 + github.com/ava-labs/avalanchego v1.13.4 + github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6 github.com/dustin/go-humanize v1.0.0 github.com/google/go-cmp v0.7.0 github.com/holiman/uint256 v1.2.4 @@ -38,11 +38,11 @@ require ( github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.1 // indirect github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 // indirect - github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect + github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect - github.com/ethereum/c-kzg-4844 v0.4.0 // indirect + github.com/ethereum/c-kzg-4844 v1.0.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect @@ -53,7 +53,6 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -65,20 +64,20 @@ require ( github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect - github.com/klauspost/compress v1.15.15 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.16.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.10.1 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect @@ -111,10 +110,8 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect google.golang.org/grpc v1.66.0 // indirect - google.golang.org/protobuf v1.35.2 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) - -replace github.com/ava-labs/libevm => /Users/stephen/go/src/github.com/ava-labs/libevm diff --git a/go.sum b/go.sum index ae831a2..aee8c18 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,10 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa h1:7d3Bkbr8pwxrPnK7AbJzI7Qi0DmLAHIgXmPT26D186w= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa/go.mod h1:TFbsruhH4SB/VO/ONKgNrgBeTLDkpr+uydstjIVyFFQ= -github.com/ava-labs/avalanchego v1.13.3-rc.2 h1:P+XSQqfAuhNPq+RG/dwDQ07o3sMh9ZsyeSgH/OV4y5s= -github.com/ava-labs/avalanchego v1.13.3-rc.2/go.mod h1:dXVZK6Sw3ZPIQ9sLjto0VMWeehExppNHFTWFwyUv5tk= +github.com/ava-labs/avalanchego v1.13.4 h1:H7bI1qx9qaOddJ3E2J9KkLvTe7d613K821eST6VGXEU= +github.com/ava-labs/avalanchego v1.13.4/go.mod h1:pMPIH9KeyXFsdxuF6sy06sztq3p2rI4XeePXvGeg9Ew= +github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6 h1:tyM659nDOknwTeU4A0fUVsGNIU7k0v738wYN92nqs/Y= +github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6/go.mod h1:zP/DOcABRWargBmUWv1jXplyWNcfmBy9cxr0lw3LW3g= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -73,8 +75,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lV github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 h1:d28BXYi+wUpz1KBmiF9bWrjEMacUEREV6MBi2ODnrfQ= github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= -github.com/crate-crypto/go-kzg-4844 v0.7.0 h1:C0vgZRk4q4EZ/JgPfzuSoxdCq3C3mOZMBShovmncxvA= -github.com/crate-crypto/go-kzg-4844 v0.7.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= +github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= +github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -95,8 +97,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= -github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY= -github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= +github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= @@ -152,7 +154,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -236,8 +237,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= -github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -270,8 +271,6 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -290,6 +289,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= @@ -320,15 +321,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= -github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -485,7 +486,6 @@ golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -599,8 +599,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 74a0891f635e408b4527ec8d8778ab218ce0a265 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Aug 2025 12:22:05 -0400 Subject: [PATCH 35/42] wip add p2p networking support --- go.mod | 2 +- vm.go | 43 +++++++++++++++++++++++++++++++------------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 648fa2c..efd26d3 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/dustin/go-humanize v1.0.0 github.com/google/go-cmp v0.7.0 github.com/holiman/uint256 v1.2.4 + github.com/prometheus/client_golang v1.22.0 github.com/stretchr/testify v1.10.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.26.0 @@ -74,7 +75,6 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect diff --git a/vm.go b/vm.go index 9fd5f82..36002be 100644 --- a/vm.go +++ b/vm.go @@ -11,6 +11,7 @@ import ( "github.com/arr4n/sink" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p" "github.com/ava-labs/avalanchego/snow" snowcommon "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" @@ -28,6 +29,7 @@ import ( "github.com/ava-labs/strevm/hook" "github.com/ava-labs/strevm/queue" "github.com/ava-labs/strevm/saexec" + "github.com/prometheus/client_golang/prometheus" ) var VMID = ids.ID{'s', 't', 'r', 'e', 'v', 'm'} @@ -37,10 +39,12 @@ var VMID = ids.ID{'s', 't', 'r', 'e', 'v', 'm'} // MUST be handled by a harness implementation that provides the final // synchronous block, which MAY be a standard genesis block. type VM struct { + *p2p.Network + snowCtx *snow.Context - snowcommon.AppHandler - hooks hook.Points - now func() time.Time + hooks hook.Points + now func() time.Time + metrics *prometheus.Registry consensusState utils.Atomic[snow.State] @@ -90,7 +94,8 @@ type Config struct { // event of a node restart. LastSynchronousBlock LastSynchronousBlock - SnowCtx *snow.Context + SnowCtx *snow.Context + AppSender snowcommon.AppSender // Now is optional, defaulting to [time.Now] if nil. Now func() time.Time @@ -102,21 +107,35 @@ type LastSynchronousBlock struct { } func New(ctx context.Context, c Config) (*VM, error) { - quit := make(chan struct{}) + metrics := prometheus.NewRegistry() + if err := c.SnowCtx.Metrics.Register("lib", metrics); err != nil { + return nil, err + } + + network, err := p2p.NewNetwork( + c.SnowCtx.Log, + c.AppSender, + metrics, + "p2p", + ) + if err != nil { + return nil, err + } vm := &VM{ + // Networking + Network: network, // VM - snowCtx: c.SnowCtx, - db: c.DB, - hooks: c.Hooks, - AppHandler: snowcommon.NewNoOpAppHandler(logging.NoLog{}), - now: c.Now, - blocks: sink.NewMutex(make(blockMap)), + snowCtx: c.SnowCtx, + db: c.DB, + hooks: c.Hooks, + now: c.Now, + blocks: sink.NewMutex(make(blockMap)), // Block building newTxs: make(chan *types.Transaction, 10), // TODO(arr4n) make the buffer configurable mempool: sink.NewPriorityMutex(new(queue.Priority[*pendingTx])), mempoolHasTxs: sink.NewGate(), - quit: quit, // both mempool and executor + quit: make(chan struct{}), // both mempool and executor } if vm.now == nil { vm.now = time.Now From d9448d996c6d18d09ccc9a13e6c68d1f265b0dd9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 26 Aug 2025 14:39:51 -0400 Subject: [PATCH 36/42] update avalanchego --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index efd26d3..694caa3 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.23.10 require ( github.com/StephenButtolph/canoto v0.17.1 github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa - github.com/ava-labs/avalanchego v1.13.4 + github.com/ava-labs/avalanchego v1.13.5-rc.4 github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6 github.com/dustin/go-humanize v1.0.0 github.com/google/go-cmp v0.7.0 diff --git a/go.sum b/go.sum index aee8c18..8a872c0 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa h1:7d3Bkbr8pwxrPnK7AbJzI7Qi0DmLAHIgXmPT26D186w= github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa/go.mod h1:TFbsruhH4SB/VO/ONKgNrgBeTLDkpr+uydstjIVyFFQ= -github.com/ava-labs/avalanchego v1.13.4 h1:H7bI1qx9qaOddJ3E2J9KkLvTe7d613K821eST6VGXEU= -github.com/ava-labs/avalanchego v1.13.4/go.mod h1:pMPIH9KeyXFsdxuF6sy06sztq3p2rI4XeePXvGeg9Ew= +github.com/ava-labs/avalanchego v1.13.5-rc.4 h1:5aPlOFQFbKBLvUzsxLgybGhOCqEyi74x1qcgntVtzww= +github.com/ava-labs/avalanchego v1.13.5-rc.4/go.mod h1:6bXxADKsAkU/f9Xme0gFJGRALp3IVzwq8NMDyx6ucRs= github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6 h1:tyM659nDOknwTeU4A0fUVsGNIU7k0v738wYN92nqs/Y= github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6/go.mod h1:zP/DOcABRWargBmUWv1jXplyWNcfmBy9cxr0lw3LW3g= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= From 13ad8fe7bf621981353aef433c07775461deffcf Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Wed, 10 Sep 2025 18:09:25 +0100 Subject: [PATCH 37/42] feat: `gastime` package (#9) Introduces the gas clock, an extension of a `proxytime.Time[gas.Gas]` that also tracks a "continuous" equivalent of ACP-176 gas excess at gas-unit resolution instead of per second. Closes #11 --- gastime/cmpopt.go | 24 +++ gastime/gastime.go | 174 +++++++++++++++++++++ gastime/gastime_test.go | 302 ++++++++++++++++++++++++++++++++++++ gastime/marshal.canoto.go | 252 ++++++++++++++++++++++++++++++ gastime/marshal.go | 49 ++++++ go.mod | 1 + go.sum | 2 + proxytime/proxytime.go | 2 +- proxytime/proxytime_test.go | 11 +- 9 files changed, 811 insertions(+), 6 deletions(-) create mode 100644 gastime/cmpopt.go create mode 100644 gastime/gastime.go create mode 100644 gastime/gastime_test.go create mode 100644 gastime/marshal.canoto.go create mode 100644 gastime/marshal.go diff --git a/gastime/cmpopt.go b/gastime/cmpopt.go new file mode 100644 index 0000000..0844bc3 --- /dev/null +++ b/gastime/cmpopt.go @@ -0,0 +1,24 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +//go:build !prod && !nocmpopts + +package gastime + +import ( + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/ava-labs/strevm/proxytime" +) + +// CmpOpt returns a configuration for [cmp.Diff] to compare [Time] instances in +// tests. +func CmpOpt() cmp.Option { + return cmp.Options{ + cmp.AllowUnexported(TimeMarshaler{}), + cmpopts.IgnoreTypes(canotoData_TimeMarshaler{}), + proxytime.CmpOpt[gas.Gas](proxytime.CmpRateInvariantsByValue), + } +} diff --git a/gastime/gastime.go b/gastime/gastime.go new file mode 100644 index 0000000..cbb2144 --- /dev/null +++ b/gastime/gastime.go @@ -0,0 +1,174 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package gastime measures time based on the consumption of gas. +package gastime + +import ( + "math" + + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/holiman/uint256" + + "github.com/ava-labs/strevm/intmath" + "github.com/ava-labs/strevm/proxytime" +) + +// Time represents an instant in time, its passage measured in [gas.Gas] +// consumption. It is not thread safe nor is the zero value valid. +// +// In addition to the passage of time, it also tracks excess consumption above a +// target, as described in [ACP-194] as a "continuous" version of [ACP-176]. +// +// Copying a Time, either directly or by dereferencing a pointer, will result in +// undefined behaviour. Use [Time.Clone] instead as it reestablishes internal +// invariants. +// +// [ACP-176]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/176-dynamic-evm-gas-limit-and-price-discovery-updates +// [ACP-194]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution +type Time struct { + TimeMarshaler +} + +// makeTime is a constructor shared by [New] and [Time.Clone]. +func makeTime(t *proxytime.Time[gas.Gas], target, excess gas.Gas) *Time { + tm := &Time{ + TimeMarshaler: TimeMarshaler{ + Time: t, + target: target, + excess: excess, + }, + } + tm.establishInvariants() + return tm +} + +func (tm *Time) establishInvariants() { + tm.Time.SetRateInvariants(&tm.target, &tm.excess) +} + +// New returns a new [Time], set from a Unix timestamp. The consumption of +// `target` * [TargetToRate] units of [gas.Gas] is equivalent to a tick of 1 +// second. Targets are clamped to [MaxTarget]. +func New(unixSeconds uint64, target, startingExcess gas.Gas) *Time { + target = clampTarget(target) + return makeTime(proxytime.New(unixSeconds, rateOf(target)), target, startingExcess) +} + +// TargetToRate is the ratio between [Time.Target] and [proxytime.Time.Rate]. +const TargetToRate = 2 + +// MaxTarget is the maximum allowable [Time.Target] to avoid overflows of the +// associated [proxytime.Time.Rate]. Values above this are silently clamped. +const MaxTarget = gas.Gas(math.MaxUint64 / TargetToRate) + +func rateOf(target gas.Gas) gas.Gas { return target * TargetToRate } +func clampTarget(t gas.Gas) gas.Gas { return min(t, MaxTarget) } +func roundRate(r gas.Gas) gas.Gas { return (r / TargetToRate) * TargetToRate } + +// Clone returns a deep copy of the time. +func (tm *Time) Clone() *Time { + // [proxytime.Time.Clone] explicitly does NOT clone the rate invariants, so + // we reestablish them as if we were constructing a new instance. + return makeTime(tm.Time.Clone(), tm.target, tm.excess) +} + +// Target returns the `T` parameter of ACP-176. +func (tm *Time) Target() gas.Gas { + return tm.target +} + +// Excess returns the `x` variable of ACP-176. +func (tm *Time) Excess() gas.Gas { + return tm.excess +} + +// Price returns the price of a unit of gas, i.e. the "base fee". +func (tm *Time) Price() gas.Price { + return gas.CalculatePrice(1 /* M */, tm.excess, tm.excessScalingFactor()) +} + +// excessScalingFactor returns the K variable of ACP-103/176, i.e. 87*T, capped +// at [math.MaxUint64]. +func (tm *Time) excessScalingFactor() gas.Gas { + const ( + targetToK = 87 + overflowThreshold = math.MaxUint64 / targetToK + ) + if tm.target > overflowThreshold { + return math.MaxUint64 + } + return targetToK * tm.target +} + +// BaseFee is equivalent to [Time.Price], returning the result as a uint256 for +// compatibility with geth/libevm objects. +func (tm *Time) BaseFee() *uint256.Int { + return uint256.NewInt(uint64(tm.Price())) +} + +// SetRate changes the gas rate per second, rounding down the argument if it is +// not a multiple of [TargetToRate]. See [Time.SetTarget] re potential error(s). +func (tm *Time) SetRate(r gas.Gas) error { + _, err := tm.TimeMarshaler.SetRate(roundRate(r)) + return err +} + +// SetTarget changes the target gas consumption per second, clamping the +// argument to [MaxTarget]. It returns an error if the scaled [Time.Excess] +// overflows as a result of the scaling. +func (tm *Time) SetTarget(t gas.Gas) error { + _, err := tm.TimeMarshaler.SetRate(rateOf(clampTarget(t))) // also updates [Time.Target] as it was passed to [proxytime.Time.SetRateInvariants] + return err +} + +// Tick is equivalent to [proxytime.Time.Tick] except that it also updates the +// gas excess. +func (tm *Time) Tick(g gas.Gas) { + tm.Time.Tick(g) + + R, T := tm.Rate(), tm.Target() + quo, _, _ := intmath.MulDiv(g, R-T, R) // overflow is impossible as (R-T)/R < 1 + tm.excess += quo +} + +// FastForwardTo is equivalent to [proxytime.Time.FastForwardTo] except that it +// may also update the gas excess. +func (tm *Time) FastForwardTo(to uint64) { + sec, frac := tm.Time.FastForwardTo(to) + if sec == 0 && frac.Numerator == 0 { + return + } + + R, T := tm.Rate(), tm.Target() + + // Excess is reduced by the amount of gas skipped (g), multiplied by T/R. + // However, to avoid overflow, the implementation needs to be a bit more + // complicated. The reduction in excess can be calculated as follows (math + // notation, not code, and ignoring the bounding at zero): + // + // s := seconds fast-forwarded (`sec`) + // f := `frac.Numerator` + // x := excess + // + // dx = -g·T/R + // = -(sR + f)·T/R + // = -sR·T/R - fT/R + // = -sT - fT/R + // + // Note that this is equivalent to the ACP reduction of T·dt because dt is + // equal to s + f/R since `frac.Denominator == R` is a documented invariant. + // Therefore dx = -(s + f/R)·T, but we separate the terms differently for + // our implementation. + + // -sT + if s := gas.Gas(sec); tm.excess/T >= s { // sT <= x; division is safe because T > 0 + tm.excess -= s * T + } else { // sT > x + tm.excess = 0 + } + + // -fT/R + quo, _, _ := intmath.MulDiv(frac.Numerator, T, R) // overflow is impossible as T/R < 1 + tm.excess = intmath.BoundedSubtract(tm.excess, quo, 0) +} diff --git a/gastime/gastime_test.go b/gastime/gastime_test.go new file mode 100644 index 0000000..7ad8380 --- /dev/null +++ b/gastime/gastime_test.go @@ -0,0 +1,302 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gastime + +import ( + "fmt" + "math" + "testing" + + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/strevm/intmath" + "github.com/ava-labs/strevm/proxytime" +) + +func (tm *Time) cloneViaCanotoRoundTrip(tb testing.TB) *Time { + tb.Helper() + x := new(Time) + require.NoErrorf(tb, x.UnmarshalCanoto(tm.MarshalCanoto()), "%T.UnmarshalCanoto(%[1]T.MarshalCanoto())", tm) + return x +} + +func TestClone(t *testing.T) { + tm := New(42, 1e6, 1e5) + tm.Tick(1) + + if diff := cmp.Diff(tm, tm.Clone(), CmpOpt()); diff != "" { + t.Errorf("%T.Clone() diff (-want +got):\n%s", tm, diff) + } + if diff := cmp.Diff(tm, tm.cloneViaCanotoRoundTrip(t), CmpOpt()); diff != "" { + t.Errorf("%T.UnmarshalCanoto(%[1]T.MarshalCanoto()) diff (-want +got):\n%s", tm, diff) + } +} + +// state captures parameters about a [Time] for assertion in tests. It includes +// both explicit (i.e. struct fields) and derived parameters (e.g. gas price), +// which aid testing of behaviour and invariants in a more fine-grained manner +// than direct comparison of two instances. +type state struct { + UnixTime uint64 + ConsumedThisSecond proxytime.FractionalSecond[gas.Gas] + Rate, Target, Excess gas.Gas + Price gas.Price +} + +func (tm *Time) state() state { + return state{ + UnixTime: tm.Unix(), + ConsumedThisSecond: tm.Fraction(), + Rate: tm.Rate(), + Target: tm.Target(), + Excess: tm.Excess(), + Price: tm.Price(), + } +} + +func (tm *Time) requireState(tb testing.TB, desc string, want state, opts ...cmp.Option) { + tb.Helper() + if diff := cmp.Diff(want, tm.state(), opts...); diff != "" { + tb.Fatalf("%s (-want +got):\n%s", desc, diff) + } +} + +func (tm *Time) mustSetRate(tb testing.TB, rate gas.Gas) { + tb.Helper() + require.NoErrorf(tb, tm.SetRate(rate), "%T.%T.SetRate(%d)", tm, TimeMarshaler{}, rate) +} + +func (tm *Time) mustSetTarget(tb testing.TB, target gas.Gas) { + tb.Helper() + require.NoError(tb, tm.SetTarget(target), "%T.SetTarget(%d)", tm, target) +} + +func TestScaling(t *testing.T) { + const initExcess = gas.Gas(1_234_567_890) + tm := New(42, 1.6e6, initExcess) + + // The initial price isn't important in this test; what we care about is + // that it's invariant under scaling of the target etc. + initPrice := tm.Price() + if initPrice == 1 { + t.Fatalf("Bad test setup: increase initial excess to achieve %T > 1", initPrice) + } + + ignore := cmpopts.IgnoreFields(state{}, "UnixTime", "ConsumedThisSecond") + + tm.requireState(t, "initial", state{ + Rate: 3.2e6, + Target: 1.6e6, + Excess: initExcess, + Price: initPrice, + }, ignore) + + tm.mustSetTarget(t, 3.2e6) + tm.requireState(t, "after SetTarget()", state{ + Rate: 6.4e6, + Target: 3.2e6, + Excess: 2 * initExcess, + Price: initPrice, // unchanged + }, ignore) + + // SetRate is identical to setting via the target, as long as the rate is + // even. Although the documentation states that SetTarget is preferred, we + // still need to test SetRate. + const ( + wantTargetViaRate = 2e6 + wantRate = wantTargetViaRate * TargetToRate + ) + want := state{ + Rate: wantRate, + Target: wantTargetViaRate, + Excess: (func() gas.Gas { + // Scale the _initial_ excess relative to the new and _initial_ + // rates, not the most recent rate before scaling. + x, _, err := intmath.MulDiv(initExcess, wantRate, 3.2e6) + require.NoErrorf(t, err, "intmath.MulDiv(%d, %d, %d)", initExcess, 4e6, 3.2e6) + return x + })(), + Price: initPrice, // unchanged + } + for roundingError := range gas.Gas(TargetToRate) { + r := wantRate + roundingError + tm.mustSetRate(t, r) + tm.requireState(t, fmt.Sprintf("after SetRate(%d)", r), want, ignore) + } + + testPostClone := func(t *testing.T, cloned *Time) { + t.Helper() + want := want + cloned.requireState(t, "unchanged immediately after clone", want, ignore) + + cloned.mustSetRate(t, cloned.Rate()*2) + tm.requireState(t, "original Time unchanged by setting clone's rate", want, ignore) + + want.Rate *= 2 + want.Target *= 2 + want.Excess *= 2 + cloned.requireState(t, "scaling after clone and then SetRate()", want, ignore) + } + + t.Run("clone", func(t *testing.T) { + testPostClone(t, tm.Clone()) + }) + + t.Run("canoto_roundtrip", func(t *testing.T) { + testPostClone(t, tm.cloneViaCanotoRoundTrip(t)) + }) +} + +func TestExcess(t *testing.T) { + const rate = gas.Gas(3.2e6) + tm := New(42, rate/2, 0) + + frac := func(num gas.Gas) (f proxytime.FractionalSecond[gas.Gas]) { + f.Numerator = num + f.Denominator = rate + return f + } + + ignore := cmpopts.IgnoreFields(state{}, "Rate", "Target", "Price") + + tm.requireState(t, "initial", state{ + UnixTime: 42, + ConsumedThisSecond: frac(0), + Excess: 0, + }, ignore) + + // NOTE: when R = 2T, excess increases or decreases by half the passage of + // time, depending on whether time was Tick()ed or FastForward()ed, + // respectively. + + steps := []struct { + desc string + // Only one of fast-forwarding or ticking per step. + ffToBefore uint64 + tickBefore gas.Gas + want state + }{ + { + desc: "initial tick 1/2s", + tickBefore: rate / 2, + want: state{ + UnixTime: 42, + ConsumedThisSecond: frac(rate / 2), + Excess: (rate / 2) / 2, + }, + }, + { + desc: "total tick 3/4s", + tickBefore: rate / 4, + want: state{ + UnixTime: 42, + ConsumedThisSecond: frac(3 * rate / 4), + Excess: 3 * rate / 8, + }, + }, + { + desc: "total tick 5/4s", + tickBefore: rate / 2, + want: state{ + UnixTime: 43, + ConsumedThisSecond: frac(rate / 4), + Excess: 5 * rate / 8, + }, + }, + { + desc: "total tick 11.25s", + tickBefore: 10 * rate, + want: state{ + UnixTime: 53, + ConsumedThisSecond: frac(rate / 4), + Excess: 45 * rate / 8, // (11*4 + 1) quarters of ticking, halved + }, + }, + { + desc: "no op fast forward", + ffToBefore: 53, + want: state{ // unchanged + UnixTime: 53, + ConsumedThisSecond: frac(rate / 4), + Excess: 45 * rate / 8, + }, + }, + { + desc: "fast forward 11.25s to 13s", + ffToBefore: 55, + want: state{ + UnixTime: 55, + ConsumedThisSecond: frac(0), + Excess: 45*rate/8 - 7*rate/8, + }, + }, + { + desc: "fast forward causes overflow when seconds multiplied by R", + ffToBefore: math.MaxUint64, + want: state{ + UnixTime: math.MaxUint64, + ConsumedThisSecond: frac(0), + Excess: 0, + }, + }, + } + + for _, s := range steps { + switch ff, tk := s.ffToBefore, s.tickBefore; { + case ff > 0 && tk > 0: + t.Fatalf("Bad test setup (%q) only FastForward() or Tick() before", s.desc) + case ff > 0: + tm.FastForwardTo(ff) + case tk > 0: + tm.Tick(tk) + } + tm.requireState(t, s.desc, s.want, ignore) + } +} + +func TestExcessScalingFactor(t *testing.T) { + const max = math.MaxUint64 + + tests := []struct { + target, want gas.Gas + }{ + {1, 87}, + {2, 174}, + {max / 87, (max / 87) * 87}, + {max/87 - 0, max - 81}, // identical to above, but explicit for clarity + {max/87 - 1, max - 81 - 87}, + {max/87 + 1, max}, // because `max - 81 + 87` would overflow + {max, max}, + } + + tm := New(0, 1, 0) + for _, tt := range tests { + require.NoErrorf(t, tm.SetTarget(tt.target), "%T.SetTarget(%v)", tm, tt.target) + assert.Equalf(t, tt.want, tm.excessScalingFactor(), "T = %d", tt.target) + } +} + +func TestTargetClamping(t *testing.T) { + tm := New(0, MaxTarget+1, 0) + require.Equal(t, MaxTarget, tm.Target(), "tm.Target() clamped by constructor") + + tests := []struct { + setTo, want gas.Gas + }{ + {setTo: 10, want: 10}, + {setTo: MaxTarget + 1, want: MaxTarget}, + {setTo: 20, want: 20}, + {setTo: math.MaxUint64, want: MaxTarget}, + } + + for _, tt := range tests { + require.NoErrorf(t, tm.SetTarget(tt.setTo), "%T.SetTarget(%d)", tm, tt.setTo) + assert.Equalf(t, tt.want, tm.Target(), "%T.Target() after setting to %#x", tm, tt.setTo) + assert.Equalf(t, tm.Target()*TargetToRate, tm.Rate(), "%T.Rate() == %d * %[1]T.Target()", tm, TargetToRate) + } +} diff --git a/gastime/marshal.canoto.go b/gastime/marshal.canoto.go new file mode 100644 index 0000000..25aaa08 --- /dev/null +++ b/gastime/marshal.canoto.go @@ -0,0 +1,252 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.17.1 +// source: marshal.go + +package gastime + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that unused imports do not error +var ( + _ atomic.Uint64 + + _ = io.ErrUnexpectedEOF +) + +const ( + canoto__TimeMarshaler__Time__tag = "\x0a" // canoto.Tag(1, canoto.Len) + canoto__TimeMarshaler__target__tag = "\x10" // canoto.Tag(2, canoto.Varint) + canoto__TimeMarshaler__excess__tag = "\x18" // canoto.Tag(3, canoto.Varint) +) + +type canotoData_TimeMarshaler struct { + size atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*TimeMarshaler) CanotoSpec(types ...reflect.Type) *canoto.Spec { + types = append(types, reflect.TypeOf(TimeMarshaler{})) + var zero TimeMarshaler + s := &canoto.Spec{ + Name: "TimeMarshaler", + Fields: []canoto.FieldType{ + canoto.FieldTypeFromField( + /*type inference:*/ (zero.Time), + /*FieldNumber: */ 1, + /*Name: */ "Time", + /*FixedLength: */ 0, + /*Repeated: */ false, + /*OneOf: */ "", + /*types: */ types, + ), + { + FieldNumber: 2, + Name: "target", + OneOf: "", + TypeUint: canoto.SizeOf(zero.target), + }, + { + FieldNumber: 3, + Name: "excess", + OneOf: "", + TypeUint: canoto.SizeOf(zero.excess), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*TimeMarshaler) MakeCanoto() *TimeMarshaler { + return new(TimeMarshaler) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *TimeMarshaler) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a [canoto.Reader]. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *TimeMarshaler) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = TimeMarshaler{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the bytes for the field. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + return canoto.ErrZeroValue + } + r.Unsafe = originalUnsafe + + // Unmarshal the field from the bytes. + remainingBytes := r.B + r.B = msgBytes + c.Time = canoto.MakePointer(c.Time) + if err := (c.Time).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + case 2: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.target); err != nil { + return err + } + if canoto.IsZero(c.target) { + return canoto.ErrZeroValue + } + case 3: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.excess); err != nil { + return err + } + if canoto.IsZero(c.excess) { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *TimeMarshaler) ValidCanoto() bool { + if c == nil { + return true + } + if c.Time != nil && !(c.Time).ValidCanoto() { + return false + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *TimeMarshaler) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if c.Time != nil { + (c.Time).CalculateCanotoCache() + if fieldSize := (c.Time).CachedCanotoSize(); fieldSize != 0 { + size += uint64(len(canoto__TimeMarshaler__Time__tag)) + canoto.SizeUint(fieldSize) + fieldSize + } + } + if !canoto.IsZero(c.target) { + size += uint64(len(canoto__TimeMarshaler__target__tag)) + canoto.SizeUint(c.target) + } + if !canoto.IsZero(c.excess) { + size += uint64(len(canoto__TimeMarshaler__excess__tag)) + canoto.SizeUint(c.excess) + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *TimeMarshaler) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TimeMarshaler) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a [canoto.Writer] and returns the +// resulting [canoto.Writer]. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TimeMarshaler) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if c.Time != nil { + if fieldSize := (c.Time).CachedCanotoSize(); fieldSize != 0 { + canoto.Append(&w, canoto__TimeMarshaler__Time__tag) + canoto.AppendUint(&w, fieldSize) + w = (c.Time).MarshalCanotoInto(w) + } + } + if !canoto.IsZero(c.target) { + canoto.Append(&w, canoto__TimeMarshaler__target__tag) + canoto.AppendUint(&w, c.target) + } + if !canoto.IsZero(c.excess) { + canoto.Append(&w, canoto__TimeMarshaler__excess__tag) + canoto.AppendUint(&w, c.excess) + } + return w +} diff --git a/gastime/marshal.go b/gastime/marshal.go new file mode 100644 index 0000000..c1573b3 --- /dev/null +++ b/gastime/marshal.go @@ -0,0 +1,49 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gastime + +import ( + "github.com/StephenButtolph/canoto" + "github.com/ava-labs/avalanchego/vms/components/gas" + + "github.com/ava-labs/strevm/proxytime" +) + +//go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE + +// A TimeMarshaler can marshal a time to and from canoto. It is of limited use +// by itself and MUST only be used via a wrapping [Time]. +type TimeMarshaler struct { //nolint:tagliatelle // TODO(arr4n) submit linter bug report + *proxytime.Time[gas.Gas] `canoto:"pointer,1"` + target gas.Gas `canoto:"uint,2"` + excess gas.Gas `canoto:"uint,3"` + + // The nocopy is important, not only for canoto, but because of the use of + // pointers in [Time.establishInvariants]. See [Time.Clone]. + canotoData canotoData_TimeMarshaler `canoto:"nocopy"` +} + +var _ canoto.Message = (*Time)(nil) + +// MakeCanoto creates a new empty value. +func (*Time) MakeCanoto() *Time { return new(Time) } + +// UnmarshalCanoto unmarshals the bytes into the [TimeMarshaler] and then +// reestablishes invariants. +func (tm *Time) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return tm.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the [TimeMarshaler] from the reader and then +// reestablishes invariants. +func (tm *Time) UnmarshalCanotoFrom(r canoto.Reader) error { + if err := tm.TimeMarshaler.UnmarshalCanotoFrom(r); err != nil { + return err + } + tm.establishInvariants() + return nil +} diff --git a/go.mod b/go.mod index 4c39611..64ce228 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.23.10 require ( github.com/ava-labs/avalanchego v1.13.2 github.com/google/go-cmp v0.6.0 + github.com/holiman/uint256 v1.2.4 github.com/stretchr/testify v1.10.0 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b ) diff --git a/go.sum b/go.sum index 05c8171..4eb8c3a 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= +github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/proxytime/proxytime.go b/proxytime/proxytime.go index 5801651..0e78ea4 100644 --- a/proxytime/proxytime.go +++ b/proxytime/proxytime.go @@ -91,7 +91,7 @@ func (tm *Time[D]) Tick(d D) { // FastForwardTo sets the time to the specified Unix timestamp if it is in the // future, returning the integer and fraction number of seconds by which the -// time was advanced. +// time was advanced. The fraction is always denominated in [Time.Rate]. func (tm *Time[D]) FastForwardTo(to uint64) (uint64, FractionalSecond[D]) { if to <= tm.seconds { return 0, FractionalSecond[D]{0, tm.hertz} diff --git a/proxytime/proxytime_test.go b/proxytime/proxytime_test.go index 25bd71a..d8b66cb 100644 --- a/proxytime/proxytime_test.go +++ b/proxytime/proxytime_test.go @@ -240,7 +240,8 @@ func TestCanotoRoundTrip(t *testing.T) { } func TestFastForward(t *testing.T) { - tm := New(42, uint64(1000)) + const rate = uint64(1000) + tm := New(42, rate) steps := []struct { tickBefore uint64 @@ -252,25 +253,25 @@ func TestFastForward(t *testing.T) { tickBefore: 100, // 42.100 ffTo: 42, // in the past wantSec: 0, - wantFrac: frac(0, 1000), + wantFrac: frac(0, rate), }, { tickBefore: 0, // 42.100 ffTo: 43, wantSec: 0, - wantFrac: frac(900, 1000), + wantFrac: frac(900, rate), }, { tickBefore: 0, // 43.000 ffTo: 44, wantSec: 1, - wantFrac: frac(0, 1000), + wantFrac: frac(0, rate), }, { tickBefore: 200, // 44.200 ffTo: 50, wantSec: 5, - wantFrac: frac(800, 1000), + wantFrac: frac(800, rate), }, } From 07f63fde601271c1434d6bf10eaebabadd09a924 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 12 Nov 2025 17:44:40 -0500 Subject: [PATCH 38/42] Add p2p validators --- vm.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/vm.go b/vm.go index 36002be..6b99986 100644 --- a/vm.go +++ b/vm.go @@ -32,6 +32,8 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +const maxValidatorSetStaleness = time.Minute + var VMID = ids.ID{'s', 't', 'r', 'e', 'v', 'm'} // VM implements Streaming Asynchronous Execution (SAE) of EVM blocks. It @@ -40,6 +42,7 @@ var VMID = ids.ID{'s', 't', 'r', 'e', 'v', 'm'} // synchronous block, which MAY be a standard genesis block. type VM struct { *p2p.Network + P2PValidators *p2p.Validators snowCtx *snow.Context hooks hook.Points @@ -112,11 +115,19 @@ func New(ctx context.Context, c Config) (*VM, error) { return nil, err } + p2pValidators := p2p.NewValidators( + c.SnowCtx.Log, + c.SnowCtx.SubnetID, + c.SnowCtx.ValidatorState, + maxValidatorSetStaleness, + ) + network, err := p2p.NewNetwork( c.SnowCtx.Log, c.AppSender, metrics, "p2p", + p2pValidators, ) if err != nil { return nil, err @@ -124,7 +135,8 @@ func New(ctx context.Context, c Config) (*VM, error) { vm := &VM{ // Networking - Network: network, + Network: network, + P2PValidators: p2pValidators, // VM snowCtx: c.SnowCtx, db: c.DB, From 8f95b2a996967739518137a87954686aa4502a81 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 13 Nov 2025 16:26:07 -0500 Subject: [PATCH 39/42] wip --- vm.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vm.go b/vm.go index 6b99986..86d41b8 100644 --- a/vm.go +++ b/vm.go @@ -236,8 +236,8 @@ func (vm *VM) Version(context.Context) (string, error) { } const ( - HTTPHandlerKey = "/sae/http" - WSHandlerKey = "/sae/ws" + HTTPHandlerKey = "/rpc" + WSHandlerKey = "/ws" ) func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { From 249188903424382adfe31139210b8aeafe061b1a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 13 Nov 2025 17:00:39 -0500 Subject: [PATCH 40/42] expose more apis --- go.mod | 22 +++++++++++----------- go.sum | 36 ++++++++++++++++++------------------ rpc.go | 9 ++++++--- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/go.mod b/go.mod index 694caa3..d8daab9 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module github.com/ava-labs/strevm -go 1.23.9 +go 1.24.8 -toolchain go1.23.10 +toolchain go1.24.9 require ( github.com/StephenButtolph/canoto v0.17.1 github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa github.com/ava-labs/avalanchego v1.13.5-rc.4 - github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6 + github.com/ava-labs/libevm v1.13.15-0.20251113215524-d0d2e6e207df github.com/dustin/go-humanize v1.0.0 github.com/google/go-cmp v0.7.0 github.com/holiman/uint256 v1.2.4 @@ -17,7 +17,7 @@ require ( go.uber.org/goleak v1.3.0 go.uber.org/zap v1.26.0 golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e - golang.org/x/term v0.30.0 + golang.org/x/term v0.36.0 ) require ( @@ -99,13 +99,13 @@ require ( go.opentelemetry.io/otel/trace v1.22.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.29.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.38.0 // indirect gonum.org/v1/gonum v0.11.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect diff --git a/go.sum b/go.sum index 8a872c0..200d4e7 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa h1:7d3Bkbr8pwxrPnK7AbJz github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa/go.mod h1:TFbsruhH4SB/VO/ONKgNrgBeTLDkpr+uydstjIVyFFQ= github.com/ava-labs/avalanchego v1.13.5-rc.4 h1:5aPlOFQFbKBLvUzsxLgybGhOCqEyi74x1qcgntVtzww= github.com/ava-labs/avalanchego v1.13.5-rc.4/go.mod h1:6bXxADKsAkU/f9Xme0gFJGRALp3IVzwq8NMDyx6ucRs= -github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6 h1:tyM659nDOknwTeU4A0fUVsGNIU7k0v738wYN92nqs/Y= -github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6/go.mod h1:zP/DOcABRWargBmUWv1jXplyWNcfmBy9cxr0lw3LW3g= +github.com/ava-labs/libevm v1.13.15-0.20251113215524-d0d2e6e207df h1:kljCS+Ya/Ay0UaP/M4UVEJh/a+OOxU3EPfTjv6PV9LM= +github.com/ava-labs/libevm v1.13.15-0.20251113215524-d0d2e6e207df/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -445,8 +445,8 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4= golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= @@ -458,8 +458,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -481,8 +481,8 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -491,8 +491,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -529,20 +529,20 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -560,8 +560,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= -golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/rpc.go b/rpc.go index 465ce8a..e70b4f9 100644 --- a/rpc.go +++ b/rpc.go @@ -30,9 +30,12 @@ func (vm *VM) ethRPCServer() *rpc.Server { b := ðAPIBackend{vm: vm} s := rpc.NewServer() - s.RegisterName("eth", ethapi.NewBlockChainAPI(b)) - s.RegisterName("eth", ethapi.NewTransactionAPI(b, new(ethapi.AddrLocker))) - s.RegisterName("eth", filters.NewFilterAPI( + _ = s.RegisterName("eth", ethapi.NewEthereumAPI(b)) + _ = s.RegisterName("eth", ethapi.NewBlockChainAPI(b)) + _ = s.RegisterName("eth", ethapi.NewTransactionAPI(b, new(ethapi.AddrLocker))) + _ = s.RegisterName("txpool", ethapi.NewTxPoolAPI(b)) + _ = s.RegisterName("debug", ethapi.NewDebugAPI(b)) + _ = s.RegisterName("eth", filters.NewFilterAPI( filters.NewFilterSystem(b, filters.Config{}), false, // lightMode TODO(arr4n) investigate further )) From b363264a860f91315863d37301044bf97df3f9bc Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 14 Nov 2025 15:29:11 -0500 Subject: [PATCH 41/42] update libevm --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d8daab9..9484758 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/StephenButtolph/canoto v0.17.1 github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa github.com/ava-labs/avalanchego v1.13.5-rc.4 - github.com/ava-labs/libevm v1.13.15-0.20251113215524-d0d2e6e207df + github.com/ava-labs/libevm v1.13.15-0.20251114170401-749b6cefda28 github.com/dustin/go-humanize v1.0.0 github.com/google/go-cmp v0.7.0 github.com/holiman/uint256 v1.2.4 diff --git a/go.sum b/go.sum index 200d4e7..6f707fa 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa h1:7d3Bkbr8pwxrPnK7AbJz github.com/arr4n/sink v0.0.0-20250610120507-bd1b0fbb19fa/go.mod h1:TFbsruhH4SB/VO/ONKgNrgBeTLDkpr+uydstjIVyFFQ= github.com/ava-labs/avalanchego v1.13.5-rc.4 h1:5aPlOFQFbKBLvUzsxLgybGhOCqEyi74x1qcgntVtzww= github.com/ava-labs/avalanchego v1.13.5-rc.4/go.mod h1:6bXxADKsAkU/f9Xme0gFJGRALp3IVzwq8NMDyx6ucRs= -github.com/ava-labs/libevm v1.13.15-0.20251113215524-d0d2e6e207df h1:kljCS+Ya/Ay0UaP/M4UVEJh/a+OOxU3EPfTjv6PV9LM= -github.com/ava-labs/libevm v1.13.15-0.20251113215524-d0d2e6e207df/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= +github.com/ava-labs/libevm v1.13.15-0.20251114170401-749b6cefda28 h1:7WTOLtDzlI+L1cU2PLc++xPxliDzfkJWkaxpXYmuaZM= +github.com/ava-labs/libevm v1.13.15-0.20251114170401-749b6cefda28/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= From ee4dcf3ef268b3521903e7b683ecbe10d39e480e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 14 Nov 2025 15:38:10 -0500 Subject: [PATCH 42/42] Include stacktrace in error log --- blocks/settlement.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/blocks/settlement.go b/blocks/settlement.go index 31fa363..5e70e34 100644 --- a/blocks/settlement.go +++ b/blocks/settlement.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "slices" + + "go.uber.org/zap" ) type ancestry struct { @@ -66,7 +68,10 @@ func (b *Block) LastSettled() *Block { if a := b.ancestry.Load(); a != nil { return a.lastSettled } - b.log.Error(getSettledOfSettledMsg) + b.log.Error( + getSettledOfSettledMsg, + zap.Stack("stacktrace"), + ) return nil }