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..473c709 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,52 @@ +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 + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.63.3 + + 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..8694165 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,101 @@ +# 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 + - 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/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 + - name: exported + severity: error + disabled: false + exclude: [""] + arguments: + - "sayRepetitiveInsteadOfStutters" + - name: package-comments + severity: warning + disabled: false + +issues: + 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/adaptor/adaptor.go b/adaptor/adaptor.go index b808016..ee1114c 100644 --- a/adaptor/adaptor.go +++ b/adaptor/adaptor.go @@ -1,3 +1,6 @@ +// 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. @@ -13,6 +16,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,9 +30,12 @@ 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]. + // 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 @@ -46,7 +58,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} } @@ -79,10 +91,20 @@ 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)) } +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 703506b..4039498 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" @@ -117,7 +118,20 @@ func (vm *VM) RejectBlock(ctx context.Context, b *blocks.Block) error { return vm.removeBlocksFromMemory(ctx, b) } +func (vm *VM) ShouldVerifyBlockWithContext(ctx context.Context, b *blocks.Block) (bool, error) { + return true, nil +} + +func (vm *VM) VerifyBlockWithContext(ctx context.Context, blockContext *block.Context, b *blocks.Block) error { + // 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) @@ -144,7 +158,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 } diff --git a/blocks/cmpopt.go b/blocks/cmpopt.go index c349fce..da888fd 100644 --- a/blocks/cmpopt.go +++ b/blocks/cmpopt.go @@ -52,7 +52,7 @@ func (b *Block) equalForTests(c *Block) bool { func (e *executionResults) equalForTests(f *executionResults) bool { fn := saetest.ComparerWithNilCheck(func(e, f *executionResults) bool { - return e.byGas.Cmp(f.byGas.Time) == 0 && + return e.byGas.Compare(f.byGas.Time) == 0 && e.gasUsed == f.gasUsed && e.receiptRoot == f.receiptRoot && e.stateRootPost == f.stateRootPost diff --git a/blocks/execution.go b/blocks/execution.go index 409839e..bd46c19 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" "github.com/ava-labs/strevm/proxytime" ) @@ -55,7 +56,14 @@ type executionResults struct { // // 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) @@ -81,6 +89,10 @@ func (b *Block) MarkExecuted(db ethdb.Database, byGas *gastime.Time, byWall time return err } + if err := hooks.BlockExecuted(context.TODO(), b.Block, receipts); err != nil { + return err + } + return b.markExecuted(e) } diff --git a/blocks/settlement.go b/blocks/settlement.go index a382977..31fa363 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 } @@ -137,7 +137,7 @@ func LastToSettleAt(settleAt uint64, parent *Block) (*Block, bool) { continue } if e := block.execution.Load(); e != nil { - if e.byGas.CmpUnix(settleAt) > 0 { + if e.byGas.CompareUnix(settleAt) > 0 { // Although this check is redundant because of the similar one // just above, it's fast so there's no harm in double-checking. continue diff --git a/blocks/settlement_test.go b/blocks/settlement_test.go index ef93447..e3ada4b 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/builder.go b/builder.go index 20a3eff..97fcdb7 100644 --- a/builder.go +++ b/builder.go @@ -4,26 +4,33 @@ import ( "context" "errors" "fmt" + "iter" "math/big" "slices" "github.com/arr4n/sink" + "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/params" + "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) { - return vm.buildBlockWithCandidateTxs(timestamp, parent, pool) + block, err := vm.buildBlockWithCandidateTxs(timestamp, parent, pool, blockContext, vm.hooks.ConstructBlock) + if pool.Len() == 0 { + vm.mempoolHasTxs.Block() + } + return block, err }, ) if err != nil { @@ -44,7 +51,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()) } @@ -67,47 +80,28 @@ 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 { - return nil, errNoopBlock - } - - 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) @@ -116,7 +110,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( @@ -128,14 +122,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 } } } @@ -165,7 +180,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) @@ -180,7 +195,26 @@ func (vm *VM) buildBlockOnHistory(lastSettled, parent *blocks.Block, timestamp u "Unknown error from worst-case transaction checking", zap.Error(err), ) - return nil, 0, err + + // 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, 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 } } @@ -195,10 +229,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 { @@ -214,3 +264,29 @@ func (vm *VM) lastBlockToSettleAt(timestamp uint64, parent *blocks.Block) (*bloc settleAt := intmath.BoundedSubtract(timestamp, params.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/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/db.go b/db.go index efd5ce0..7f07038 100644 --- a/db.go +++ b/db.go @@ -56,7 +56,7 @@ func (vm *VM) upgradeLastSynchronousBlock(lastSync LastSynchronousBlock) error { clock := gastime.New(block.Time(), lastSync.Target, lastSync.ExcessAfter) receipts := rawdb.ReadRawReceipts(vm.db, lastSync.Hash, block.Height()) - if err := block.MarkExecuted(vm.db, clock, block.Timestamp(), receipts, block.Block.Root()); err != nil { + if err := block.MarkExecuted(vm.db, clock, block.Timestamp(), receipts, block.Block.Root(), vm.hooks); err != nil { return err } if err := block.WriteLastSettledNumber(vm.db); err != nil { diff --git a/execution_test.go b/execution_test.go index e74eed0..dd36925 100644 --- a/execution_test.go +++ b/execution_test.go @@ -14,6 +14,7 @@ import ( "github.com/ava-labs/libevm/libevm/hookstest" "github.com/ava-labs/libevm/params" "github.com/ava-labs/strevm/blocks" + "github.com/ava-labs/strevm/hook/hooktest" "github.com/ava-labs/strevm/proxytime" "github.com/holiman/uint256" "github.com/stretchr/testify/require" @@ -25,7 +26,7 @@ func TestExecutorClock(t *testing.T) { key := newTestPrivateKey(t, nil) eoa := crypto.PubkeyToAddress(key.PublicKey) - hooks := new(stubHooks) + hooks := new(hooktest.Simple) chainConfig := params.TestChainConfig const start = 42 @@ -46,7 +47,7 @@ func TestExecutorClock(t *testing.T) { got := exec.TimeNotThreadsafe() want := proxytime.New[gas.Gas](sec, 2*hooks.T) // R = 2T want.Tick(gasThisSec) - if got.Cmp(want) != 0 { + if got.Compare(want) != 0 { t.Errorf("%T.TimeNotThreadsafe() got %s; want %s", exec, got.String(), want.String()) } if got, want := got.Excess(), excess; got != want { @@ -154,7 +155,7 @@ func TestExecutorClock(t *testing.T) { requireGasTimeAndExcess(t, start+7, 0, 37_500+(900_000/2)-(25_000/2)) // Large transactions move the clock ahead of the block time. - halfSecondOfGas := hooks.fractionSecondsOfGas(t, 1, 2) + halfSecondOfGas := hooks.FractionSecondsOfGas(t, 1, 2) twoAndAHalfSeconds := 5 * halfSecondOfGas executeNextBlock(t, start+10, txs.next(uint64(twoAndAHalfSeconds))) requireFutureTime := func(t *testing.T) { diff --git a/gastime/cmpopt.go b/gastime/cmpopt.go index 3062039..0844bc3 100644 --- a/gastime/cmpopt.go +++ b/gastime/cmpopt.go @@ -1,10 +1,16 @@ +// 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 @@ -13,5 +19,6 @@ 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 index 083360c..8a2533a 100644 --- a/gastime/gastime.go +++ b/gastime/gastime.go @@ -1,11 +1,15 @@ +// 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" - "github.com/holiman/uint256" ) // Time represents an instant in time, its passage measured in [gas.Gas] @@ -77,9 +81,11 @@ func (tm *Time) BaseFee() *uint256.Int { // 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. -func (tm *Time) SetTarget(t gas.Gas) { - tm.SetRate(2 * t) // also updates target as it was passed to [proxytime.Time.SetRateInvariants] +// 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 @@ -88,7 +94,7 @@ func (tm *Time) Tick(g gas.Gas) { tm.Time.Tick(g) R, T := tm.Rate(), tm.Target() - quo, _, _ := intmath.MulDiv(g, R-T, R) //nolint:errcheck // R-T < R so the quotient is < g + quo, _, _ := intmath.MulDiv(g, R-T, R) // overflow is impossible as (R-T)/R < 1 tm.excess += quo } @@ -101,6 +107,6 @@ func (tm *Time) FastForwardTo(to uint64) { } R, T := tm.Rate(), tm.Target() - quo, _, _ := intmath.MulDiv(R*gas.Gas(sec)+frac.Numerator, T, R) //nolint:errcheck // T < R so the quotient is < LHS + 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 index b7a47b8..465a1dc 100644 --- a/gastime/gastime_test.go +++ b/gastime/gastime_test.go @@ -1,16 +1,43 @@ +// 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/ava-labs/strevm/intmath" - "github.com/ava-labs/strevm/proxytime" "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] @@ -36,16 +63,20 @@ func (tm *Time) requireState(tb testing.TB, desc string, want state, opts ...cmp } } -func (tm *Time) cloneViaCanoto(tb testing.TB) *Time { +func (tm *Time) mustSetRate(tb testing.TB, rate gas.Gas) { tb.Helper() - x := new(Time) - require.NoErrorf(tb, x.UnmarshalCanoto(tm.MarshalCanoto()), "%T.UnmarshalCanoto(%[1]T.MarshalCanoto())", tm) - return x + _, 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_600_000, initExcess) + 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. @@ -63,7 +94,7 @@ func TestScaling(t *testing.T) { Price: initPrice, }, ignore) - tm.SetTarget(3.2e6) + tm.mustSetTarget(t, 3.2e6) tm.requireState(t, "after SetTarget()", state{ Rate: 6.4e6, Target: 3.2e6, @@ -71,10 +102,10 @@ func TestScaling(t *testing.T) { Price: initPrice, // unchanged }, ignore) - // SetRate is equivalent to setting via the target, as long as the rate is + // 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.SetRate(4e6) + tm.mustSetRate(t, 4e6) want := state{ Rate: 4e6, Target: 2e6, @@ -89,23 +120,26 @@ func TestScaling(t *testing.T) { } tm.requireState(t, "after SetRate()", want, ignore) - testPostDuplicate := func(t *testing.T, tm *Time) { + testPostClone := func(t *testing.T, cloned *Time) { + t.Helper() want := want - tm.requireState(t, "unchanged immediately after", want, ignore) + 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) - tm.SetRate(tm.Rate() * 2) want.Rate *= 2 want.Target *= 2 want.Excess *= 2 - tm.requireState(t, "scaled after SetRate()", want, ignore) + cloned.requireState(t, "scaling after clone and then SetRate()", want, ignore) } t.Run("clone", func(t *testing.T) { - testPostDuplicate(t, tm.Clone()) + testPostClone(t, tm.Clone()) }) t.Run("canoto_roundtrip", func(t *testing.T) { - testPostDuplicate(t, tm.cloneViaCanoto(t)) + testPostClone(t, tm.cloneViaCanotoRoundTrip(t)) }) } @@ -115,7 +149,7 @@ func TestExcess(t *testing.T) { frac := func(num gas.Gas) (f proxytime.FractionalSecond[gas.Gas]) { f.Numerator = num - f.Denominator = 3.2e6 + f.Denominator = rate return f } @@ -204,13 +238,5 @@ func TestExcess(t *testing.T) { tm.Tick(tk) } tm.requireState(t, s.desc, s.want, ignore) - - t.Run("Clone()", func(t *testing.T) { - tm.Clone().requireState(t, s.desc, s.want, ignore) - }) - - t.Run("canoto_roundtrip", func(t *testing.T) { - tm.cloneViaCanoto(t).requireState(t, s.desc, s.want, ignore) - }) } } diff --git a/gastime/marshal.go b/gastime/marshal.go index 7b1abb9..59bbadd 100644 --- a/gastime/marshal.go +++ b/gastime/marshal.go @@ -1,3 +1,6 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package gastime import ( @@ -10,7 +13,7 @@ import ( // 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 { +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"` @@ -34,7 +37,7 @@ func (tm *Time) UnmarshalCanoto(bytes []byte) error { return tm.UnmarshalCanotoFrom(r) } -// UnmarshalCanoto populates the [TimeMarshaler] from the reader and then +// 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 { diff --git a/go.mod b/go.mod index adc55c6..694caa3 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,12 @@ 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/libevm v1.13.14-0.3.0.rc.1 + 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 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 @@ -38,11 +39,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 +54,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 +65,19 @@ 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_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,7 +110,7 @@ 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 diff --git a/go.sum b/go.sum index eec6f00..8a872c0 100644 --- a/go.sum +++ b/go.sum @@ -21,10 +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.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/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.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= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -75,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= @@ -97,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= @@ -154,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= @@ -238,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= @@ -272,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= @@ -292,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= @@ -322,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= @@ -487,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= @@ -601,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= 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/hook/hook.go b/hook/hook.go index ca0a0c0..9b7e584 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -5,17 +5,82 @@ package hook 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/common" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/params" "github.com/ava-labs/strevm/gastime" "github.com/ava-labs/strevm/intmath" saeparams "github.com/ava-labs/strevm/params" + "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 + + // Called during build + ConstructBlock( + ctx context.Context, + blockContext *block.Context, // May be nil + header *types.Header, + parent *types.Header, + ancestors iter.Seq[*types.Block], + state State, + txs []*types.Transaction, + receipts []*types.Receipt, + ) (*types.Block, error) + + // 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, + 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 new file mode 100644 index 0000000..e6ea6f6 --- /dev/null +++ b/hook/hooktest/hook.go @@ -0,0 +1,62 @@ +package hooktest + +import ( + "context" + "iter" + "testing" + + "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" + "github.com/ava-labs/strevm/intmath" + "github.com/stretchr/testify/require" +) + +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, receipts types.Receipts) error { + return nil +} + +func (s Simple) FractionSecondsOfGas(tb testing.TB, num, denom uint64) gas.Gas { + tb.Helper() + quo, rem, err := intmath.MulDiv(gas.Gas(num), 2*s.T, gas.Gas(denom)) + require.NoError(tb, err) + require.Zero(tb, rem, "remainder when calculating fractional seconds of gas") + return quo +} diff --git a/integration_test.go b/integration_test.go index 49101d7..d67420c 100644 --- a/integration_test.go +++ b/integration_test.go @@ -25,7 +25,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/intmath" + "github.com/ava-labs/strevm/hook/hooktest" "github.com/ava-labs/strevm/queue" "github.com/ava-labs/strevm/saetest" "github.com/ava-labs/strevm/saexec" @@ -48,22 +48,6 @@ func init() { flag.Var(&txsInIntegrationTest, "wrap_avax_tx_count", "Number of transactions to use in TestIntegrationWrapAVAX (comma-separated)") } -type stubHooks struct { - T gas.Gas -} - -func (h *stubHooks) GasTarget(parent *types.Block) gas.Gas { - return h.T -} - -func (h *stubHooks) fractionSecondsOfGas(tb testing.TB, num, denom uint64) gas.Gas { - tb.Helper() - quo, rem, err := intmath.MulDiv(gas.Gas(num), 2*h.T, gas.Gas(denom)) - require.NoErrorf(tb, err, "calculating fractional seconds of gas") - require.Zero(tb, rem, "remainder when calculating fractional seconds of gas") - return quo -} - func TestIntegrationWrapAVAX(t *testing.T) { for _, n := range txsInIntegrationTest { t.Run(fmt.Sprint(n), func(t *testing.T) { @@ -92,7 +76,7 @@ func testIntegrationWrapAVAX(t *testing.T, numTxsInTest uint64) { func() time.Time { return now }, - &stubHooks{ + hooktest.Simple{ T: 2e6, }, tbLogger{tb: t, level: logging.Debug + 1}, diff --git a/intmath/intmath.go b/intmath/intmath.go index 2b028d7..e83cf23 100644 --- a/intmath/intmath.go +++ b/intmath/intmath.go @@ -1,3 +1,6 @@ +// 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 @@ -37,6 +40,10 @@ func MulDiv[T ~uint64](a, b, den T) (quo, rem T, err error) { // 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 index 5c52ec9..c2b0508 100644 --- a/intmath/intmath_test.go +++ b/intmath/intmath_test.go @@ -1,9 +1,12 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package intmath import ( "errors" "math" - "math/rand" + "math/rand/v2" "testing" ) @@ -13,13 +16,13 @@ func TestBoundedSubtract(t *testing.T) { tests := []struct { a, b, floor, want uint64 }{ - {1, 2, 0, 0}, // a < b - {2, 1, 0, 1}, // not bounded - {2, 1, 1, 1}, // a - b == floor - {2, 2, 1, 1}, // bounded - {3, 1, 1, 2}, - {max, 10, max - 9, max - 9}, // `a` threshold (`max+1`) would overflow uint64 - {max, 10, max - 11, max - 10}, + {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 { @@ -34,16 +37,16 @@ func TestMulDiv(t *testing.T) { a, b, div, wantQuo, wantRem uint64 }{ { - 5, 2, 3, // 10/3 - 3, 1, + a: 5, b: 2, div: 3, // 10/3 + wantQuo: 3, wantRem: 1, }, { - 5, 3, 3, // 15/3 - 5, 0, + a: 5, b: 3, div: 3, // 15/3 + wantQuo: 5, wantRem: 0, }, { - max, 4, 8, // must avoid overflow - max / 2, 4, + a: max, b: 4, div: 8, // must avoid overflow + wantQuo: max / 2, wantRem: 4, }, } @@ -64,26 +67,26 @@ func TestCeilDiv(t *testing.T) { } tests := []test{ - {4, 2, 2}, - {4, 1, 4}, - {4, 3, 2}, - {10, 3, 4}, - {max, 2, 1 << 63}, // must not overflow + {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.NewSource(0)) + rng := rand.New(rand.NewPCG(0, 0)) //nolint:gosec // Reproducibility is valuable for tests for range 50 { - l := uint64(rng.Int63n(math.MaxUint32)) - r := uint64(rng.Int63n(math.MaxUint32)) + l := uint64(rng.Uint32()) + r := uint64(rng.Uint32()) tests = append(tests, []test{ - {l*r + 1, l, r + 1}, - {l*r + 0, l, r}, - {l*r - 1, l, r}, + {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 - {l*r + 1, r, l + 1}, - {l*r + 0, r, l}, - {l*r - 1, r, l}, + {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}, }...) } diff --git a/mempool.go b/mempool.go index 9da22f8..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 { @@ -50,7 +58,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,19 +72,13 @@ 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), zap.Uint64("nonce", tx.Nonce()), ) - - select { - case vm.toEngine <- snowcommon.PendingTxs: - default: - p := snowcommon.PendingTxs - vm.logger().Debug(fmt.Sprintf("%T(%s) dropped", p, p)) - } + vm.mempoolHasTxs.Open() } } } diff --git a/plugin/plugin.go b/plugin/plugin.go index 3c80128..7896eac 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -8,10 +8,17 @@ import ( "github.com/ava-labs/avalanchego/vms/rpcchainvm" sae "github.com/ava-labs/strevm" "github.com/ava-labs/strevm/adaptor" + "github.com/ava-labs/strevm/hook/hooktest" ) +const TargetGasPerSecond = 1_000_000 + func main() { - vm := adaptor.Convert(new(sae.SinceGenesis)) + vm := adaptor.Convert(&sae.SinceGenesis{ + Hooks: hooktest.Simple{ + T: TargetGasPerSecond, + }, + }) if err := rpcchainvm.Serve(context.Background(), vm); err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/proxytime/cmpopt.go b/proxytime/cmpopt.go index cd623eb..8bb7d24 100644 --- a/proxytime/cmpopt.go +++ b/proxytime/cmpopt.go @@ -1,18 +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]() cmp.Option { +func CmpOpt[D Duration](invariants CmpRateInvariantsBy) cmp.Option { return cmp.Options{ cmp.AllowUnexported(Time[D]{}), cmpopts.IgnoreTypes(canotoData_Time{}), - cmpopts.IgnoreFields(Time[D]{}, "rateInvariants"), + 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.go b/proxytime/proxytime.go index 95a98eb..5801651 100644 --- a/proxytime/proxytime.go +++ b/proxytime/proxytime.go @@ -1,9 +1,15 @@ -// Package proxytime measures time based on a proxy unit and associated unit -// rate. +// 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" @@ -53,7 +59,9 @@ func New[D Duration](unixSeconds uint64, hertz D) *Time[D] { } // Unix returns tm as a Unix timestamp. -func (tm *Time[D]) Unix() uint64 { return tm.seconds } +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 @@ -75,9 +83,10 @@ func (tm *Time[D]) Rate() D { // Tick advances the time by `d`. func (tm *Time[D]) Tick(d D) { - tm.fraction += d - tm.seconds += uint64(tm.fraction / tm.hertz) - tm.fraction %= tm.hertz + 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 @@ -104,9 +113,16 @@ func (tm *Time[D]) FastForwardTo(to uint64) (uint64, FractionalSecond[D]) { // 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) } @@ -133,10 +149,15 @@ func (tm *Time[D]) SetRate(hertz D) (truncated FractionalSecond[D], err error) { // 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 { @@ -145,40 +166,38 @@ func (tm *Time[D]) scale(val, newRate D) (scaled D, truncated FractionalSecond[D return scaled, FractionalSecond[D]{Numerator: trunc, Denominator: tm.hertz}, nil } -// Cmp returns +// 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]) Cmp(u *Time[D]) int { - if ts, us := tm.seconds, u.seconds; ts < us { - return -1 - } else if ts > us { - return 1 +func (tm *Time[D]) Compare(u *Time[D]) int { + if c := cmp.Compare(tm.seconds, u.seconds); c != 0 { + return c } - - if tf, uf := tm.fraction, u.fraction; tf < uf { - return -1 - } else if tf > uf { - return 1 - } - return 0 + return cmp.Compare(tm.fraction, u.fraction) } -// CmpUnix is equivalent to [Time.Cmp] 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 CmpUnix will -// return 1. -func (tm *Time[D]) CmpUnix(sec uint64) int { - return tm.Cmp(&Time[D]{seconds: sec}) +// 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. +// 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) } diff --git a/proxytime/proxytime_test.go b/proxytime/proxytime_test.go index 19d6b6c..25bd71a 100644 --- a/proxytime/proxytime_test.go +++ b/proxytime/proxytime_test.go @@ -1,32 +1,42 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + package proxytime import ( "cmp" "fmt" + "math" "testing" "time" - gocmp "github.com/google/go-cmp/cmp" "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]) { +func (tm *Time[D]) assertEq(tb testing.TB, desc string, seconds uint64, fraction FractionalSecond[D]) (equal bool) { tb.Helper() - if tm.Unix() != seconds || tm.Fraction() != fraction { - tb.Errorf("%s got (seconds, fraction) = (%d, %v); want (%d, %v)", desc, tm.Unix(), tm.Fraction(), seconds, fraction) + 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() - before := tb.Failed() - tm.assertEq(tb, desc, seconds, fraction) - if !before && tb.Failed() { + if !tm.assertEq(tb, desc, seconds, fraction) { tb.FailNow() } } @@ -37,20 +47,66 @@ func TestTickAndCmp(t *testing.T) { tm.assertEq(t, "New(0, ...)", 0, frac(0, rate)) steps := []struct { - tick uint64 - wantSeconds, wantFraction uint64 + tick uint64 + wantSec, wantFrac uint64 }{ - {100, 0, 100}, - {0, 0, 100}, - {399, 0, 499}, - {1, 1, 0}, - {500, 2, 0}, - {400, 2, 400}, - {200, 3, 100}, - {1600, 6, 200}, - {299, 6, 499}, - {2, 7, 1}, - {499, 8, 0}, + { + 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 @@ -59,12 +115,12 @@ func TestTickAndCmp(t *testing.T) { tm.Tick(s.tick) ticked += s.tick - tm.requireEq(t, fmt.Sprintf("%+d", ticked), s.wantSeconds, frac(s.wantFraction, rate)) + tm.requireEq(t, fmt.Sprintf("%+d", ticked), s.wantSec, frac(s.wantFrac, rate)) - if got, want := tm.Cmp(old), cmp.Compare(s.tick, 0); got != want { + 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.Cmp(tm), cmp.Compare(0, 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) } } @@ -87,26 +143,47 @@ func TestSetRate(t *testing.T) { tm.SetRateInvariants(&invariant) steps := []struct { - newRate, wantFraction uint64 - wantTruncated FractionalSecond[uint64] - wantInvariant uint64 + newRate, wantNumerator uint64 + wantTruncated FractionalSecond[uint64] + wantInvariant uint64 }{ - {initRate / divisor, tick / divisor, frac(0, 1), invariant / divisor}, // no rounding - {initRate * 5, tick * 5, frac(0, 1), invariant * 5}, // multiplication never has rounding - {15_000, 1_500, frac(0, 1), 3_000}, // same as above, but shows the numbers - {75, 7 /*7.5*/, frac(7_500, 15_000), 15}, // rounded down by 0.5, denominated in the old rate + { + 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) - tm.requireEq(t, fmt.Sprintf("rate changed from %d to %d", old, s.newRate), initSeconds, frac(s.wantFraction, 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, "%T.Denominator") + assert.NotZerof(t, gotTruncated.Denominator, "truncation %T.Denominator with 0 numerator", gotTruncated) } else { - assert.Equalf(t, s.wantTruncated, gotTruncated, "") + assert.Equal(t, s.wantTruncated, gotTruncated, "truncation") } assert.Equal(t, s.wantInvariant, invariant) } @@ -115,44 +192,86 @@ func TestSetRate(t *testing.T) { func TestAsTime(t *testing.T) { stdlib := time.Date(1986, time.October, 1, 0, 0, 0, 0, time.UTC) - const rate = 500 - tm := New[uint64](uint64(stdlib.Unix()), rate) - if diff := gocmp.Diff(stdlib, tm.AsTime()); diff != "" { - t.Fatalf("%T.AsTime() at construction (-want +got):\n%s", tm, diff) + 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 diff := gocmp.Diff(stdlib.Add(2*time.Millisecond), tm.AsTime()); diff != "" { - t.Fatalf("%T.AsTime() after ticking 1/%d (-want +got)\n%s", tm, rate, diff) + 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) { - const ( - seconds = 42 - rate = 10_000 - tick = 1_234 - ) - tm := New[uint64](seconds, rate) - tm.Tick(tick) + 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, + }, + } - 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), seconds, frac(tick, rate)) + 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, ffTo uint64 - wantSec uint64 - wantFrac FractionalSecond[uint64] + tickBefore uint64 + ffTo uint64 + wantSec uint64 + wantFrac FractionalSecond[uint64] }{ - {100, 42, 0, frac(0, 1000)}, - {0, 43, 0, frac(900, 1000)}, - {0, 44, 1, frac(0, 1000)}, - {200, 50, 5, frac(800, 1000)}, + { + 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 { @@ -162,16 +281,17 @@ func TestFastForward(t *testing.T) { assert.Equal(t, s.wantFrac, gotFrac) if t.Failed() { - break + t.FailNow() } } } func TestCmpUnix(t *testing.T) { tests := []struct { - tm *Time[uint64] - tick, cmpAgainst uint64 - want int + tm *Time[uint64] + tick uint64 + cmpAgainst uint64 + want int }{ { tm: New[uint64](42, 1e6), @@ -194,7 +314,7 @@ func TestCmpUnix(t *testing.T) { for _, tt := range tests { tt.tm.Tick(tt.tick) - if got := tt.tm.CmpUnix(tt.cmpAgainst); got != tt.want { + 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/rpc.go b/rpc.go index 62cdc7e..465ce8a 100644 --- a/rpc.go +++ b/rpc.go @@ -222,7 +222,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 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 570038c..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" @@ -17,12 +18,12 @@ import ( snowcommon "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/snowtest" "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/params" sae "github.com/ava-labs/strevm" + "github.com/ava-labs/strevm/hook/hooktest" "github.com/ava-labs/strevm/saedev/unsafedev" "go.uber.org/zap/zapcore" ) @@ -34,13 +35,11 @@ func main() { } } -type hooks struct{} - -func (hooks) GasTarget(*types.Block) gas.Gas { return 10e6 } - func run(ctx context.Context) error { vm := &sae.SinceGenesis{ - Hooks: hooks{}, + Hooks: hooktest.Simple{ + T: 10e6, + }, } // test test test test test test test test test test test junk @@ -74,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/saetest/cmpopts.go b/saetest/cmpopts.go index 3abd8e5..bbc495f 100644 --- a/saetest/cmpopts.go +++ b/saetest/cmpopts.go @@ -88,7 +88,7 @@ func CmpStateDBs() cmp.Option { func CmpTimes() cmp.Option { return cmp.Options{ gastime.CmpOpt(), - proxytime.CmpOpt[gas.Gas](), + proxytime.CmpOpt[gas.Gas](proxytime.IgnoreRateInvariants), } } diff --git a/saexec/execution.go b/saexec/execution.go index c6630a6..1b568bd 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -90,7 +90,7 @@ func (e *Executor) execute(ctx context.Context, b *blocks.Block) error { header := types.CopyHeader(b.Header()) header.BaseFee = e.gasClock.BaseFee().ToBig() - e.log.Debug( + e.log.Info( "Executing accepted block", zap.Uint64("height", b.Height()), zap.Uint64("timestamp", header.Time), @@ -138,9 +138,25 @@ 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) - if e.gasClock.Time.Cmp(perTxClock) != 0 { + if e.gasClock.Time.Compare(perTxClock) != 0 { return fmt.Errorf("broken invariant: block-resolution clock @ %s does not match tx-resolution clock @ %s", e.gasClock.String(), perTxClock.String()) } @@ -154,13 +170,13 @@ 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) e.sendPostExecutionEvents(b.Block, receipts) // (3) - e.log.Debug( + e.log.Info( "Block execution complete", zap.Uint64("height", b.Height()), zap.Time("gas_time", e.gasClock.AsTime()), diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..a2dc81c --- /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" "./plugin/"*.go +echo "Built SAE EVM at $BINARY_PATH" diff --git a/tools.go b/tools.go index 9906d24..dd6e0d7 100644 --- a/tools.go +++ b/tools.go @@ -1,6 +1,9 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + //go:build tools -package sae +package strevm // Protects indirect dependencies of tools from being pruned by `go mod tidy`. import ( diff --git a/vm.go b/vm.go index cc46ca8..36002be 100644 --- a/vm.go +++ b/vm.go @@ -11,8 +11,10 @@ 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" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/version" @@ -27,18 +29,22 @@ 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'} + // 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 // synchronous block, which MAY be a standard genesis block. type VM struct { + *p2p.Network + snowCtx *snow.Context - snowcommon.AppHandler - toEngine chan<- snowcommon.Message - hooks hook.Points - now func() time.Time + hooks hook.Points + now func() time.Time + metrics *prometheus.Registry consensusState utils.Atomic[snow.State] @@ -48,8 +54,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 @@ -66,7 +73,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 @@ -77,8 +94,8 @@ type Config struct { // event of a node restart. LastSynchronousBlock LastSynchronousBlock - ToEngine chan<- snowcommon.Message - SnowCtx *snow.Context + SnowCtx *snow.Context + AppSender snowcommon.AppSender // Now is optional, defaulting to [time.Now] if nil. Now func() time.Time @@ -90,25 +107,40 @@ 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, - toEngine: c.ToEngine, - 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])), - 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: make(chan struct{}), // 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 @@ -179,7 +211,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() @@ -206,8 +238,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) { @@ -252,7 +284,11 @@ func (vm *VM) ParseBlock(ctx context.Context, blockBytes []byte) (*blocks.Block, } func (vm *VM) BuildBlock(ctx context.Context) (*blocks.Block, error) { - return vm.buildBlock(ctx, uint64(vm.now().Unix()), vm.preference.Load()) + return vm.BuildBlockWithContext(ctx, nil) +} + +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 { 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 -}