mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
fix: remove ava-labs dependencies and integrate with luxfi packages
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
# https://golangci-lint.run/usage/configuration/
|
||||
version: "2"
|
||||
run:
|
||||
# If set we pass it to "go list -mod={option}". From "go help modules":
|
||||
# If invoked with -mod=readonly, the go command is disallowed from the implicit
|
||||
# automatic updating of go.mod described above. Instead, it fails when any changes
|
||||
# to go.mod are needed. This setting is most useful to check that go.mod does
|
||||
# not need updates, such as in a continuous integration and testing system.
|
||||
# If invoked with -mod=vendor, the go command assumes that the vendor
|
||||
# directory holds the correct copies of dependencies and ignores
|
||||
# the dependency descriptions in go.mod.
|
||||
#
|
||||
# Allowed values: readonly|vendor|mod
|
||||
# By default, it isn't set.
|
||||
modules-download-mode: readonly
|
||||
|
||||
# Include non-test files tagged as test-only.
|
||||
# Context: https://github.com/ava-labs/avalanchego/pull/3173
|
||||
build-tags:
|
||||
- test
|
||||
|
||||
issues:
|
||||
# Make issues output unique by line.
|
||||
uniq-by-line: false
|
||||
|
||||
# Maximum issues count per one linter.
|
||||
max-issues-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text.
|
||||
max-same-issues: 0
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofmt
|
||||
- gofumpt
|
||||
settings:
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- blank
|
||||
- prefix(github.com/ava-labs/subnet-evm)
|
||||
- alias
|
||||
- dot
|
||||
custom-order: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
|
||||
# Avalanche linters should be added incrementally
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- copyloopvar
|
||||
# - depguard
|
||||
# - errcheck
|
||||
- errorlint
|
||||
# - forbidigo
|
||||
- goconst
|
||||
# - gocritic
|
||||
- goprintffuncname
|
||||
# - gosec
|
||||
- govet
|
||||
- importas
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nakedret
|
||||
# - nilerr
|
||||
- noctx
|
||||
- nolintlint
|
||||
- perfsprint
|
||||
# - prealloc
|
||||
# - predeclared
|
||||
- revive
|
||||
- spancheck
|
||||
# - staticcheck
|
||||
- tagalign
|
||||
# - testifylint
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- usestdlibvars
|
||||
- whitespace
|
||||
settings:
|
||||
depguard:
|
||||
rules:
|
||||
packages:
|
||||
deny:
|
||||
- pkg: container/list
|
||||
desc: github.com/ava-labs/avalanchego/utils/linked should be used instead.
|
||||
- pkg: github.com/golang/mock/gomock
|
||||
desc: go.uber.org/mock/gomock should be used instead.
|
||||
- pkg: github.com/stretchr/testify/assert
|
||||
desc: github.com/stretchr/testify/require should be used instead.
|
||||
- pkg: io/ioutil
|
||||
desc: io/ioutil is deprecated. Use package io or os instead.
|
||||
errorlint:
|
||||
# Check for plain type assertions and type switches.
|
||||
asserts: false
|
||||
# Check for plain error comparisons.
|
||||
comparison: false
|
||||
forbidigo:
|
||||
# Forbid the following identifiers (list of regexp).
|
||||
forbid:
|
||||
- pattern: require\.Error$(# ErrorIs should be used instead)?
|
||||
- pattern: require\.ErrorContains$(# ErrorIs should be used instead)?
|
||||
- pattern: require\.EqualValues$(# Equal should be used instead)?
|
||||
- pattern: require\.NotEqualValues$(# NotEqual should be used instead)?
|
||||
- pattern: ^(t|b|tb|f)\.(Fatal|Fatalf|Error|Errorf)$(# the require library should be used instead)?
|
||||
- pattern: ^sort\.(Slice|Strings)$(# the slices package should be used instead)?
|
||||
# Exclude godoc examples from forbidigo checks.
|
||||
exclude-godoc-examples: false
|
||||
gosec:
|
||||
excludes:
|
||||
- G107 # Url provided to HTTP request as taint input https://securego.io/docs/rules/g107
|
||||
- G115 # TODO(marun) Enable this ruleset in a follow-up PR
|
||||
importas:
|
||||
# Do not allow unaliased imports of aliased packages.
|
||||
no-unaliased: false
|
||||
# Do not allow non-required aliases.
|
||||
no-extra-aliases: false
|
||||
# List of aliases
|
||||
alias:
|
||||
- pkg: github.com/ava-labs/avalanchego/utils/math
|
||||
alias: safemath
|
||||
- pkg: github.com/ava-labs/avalanchego/utils/json
|
||||
alias: avajson
|
||||
revive:
|
||||
rules:
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bool-literal-in-expr
|
||||
- name: bool-literal-in-expr
|
||||
disabled: false
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#early-return
|
||||
- name: early-return
|
||||
disabled: false
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
|
||||
- name: empty-lines
|
||||
disabled: false
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#redundant-import-alias
|
||||
- name: redundant-import-alias
|
||||
disabled: false
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-format
|
||||
- name: string-format
|
||||
disabled: false
|
||||
arguments:
|
||||
- - b.Logf[0]
|
||||
- /.*%.*/
|
||||
- no format directive, use b.Log instead
|
||||
- - fmt.Errorf[0]
|
||||
- /.*%.*/
|
||||
- no format directive, use errors.New instead
|
||||
- - fmt.Fprintf[1]
|
||||
- /.*%.*/
|
||||
- no format directive, use fmt.Fprint instead
|
||||
- - fmt.Printf[0]
|
||||
- /.*%.*/
|
||||
- no format directive, use fmt.Print instead
|
||||
- - fmt.Sprintf[0]
|
||||
- /.*%.*/
|
||||
- no format directive, use fmt.Sprint instead
|
||||
- - log.Fatalf[0]
|
||||
- /.*%.*/
|
||||
- no format directive, use log.Fatal instead
|
||||
- - log.Printf[0]
|
||||
- /.*%.*/
|
||||
- no format directive, use log.Print instead
|
||||
- - t.Logf[0]
|
||||
- /.*%.*/
|
||||
- no format directive, use t.Log instead
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag
|
||||
- name: struct-tag
|
||||
disabled: false
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-naming
|
||||
- name: unexported-naming
|
||||
disabled: false
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error
|
||||
- name: unhandled-error
|
||||
# prefer the errcheck linter since it can be disabled directly with nolint directive
|
||||
# but revive's disable directive (e.g. //revive:disable:unhandled-error) is not
|
||||
# supported when run under golangci_lint
|
||||
disabled: true
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter
|
||||
- name: unused-parameter
|
||||
disabled: false
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
|
||||
- name: unused-receiver
|
||||
disabled: false
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#useless-break
|
||||
- name: useless-break
|
||||
disabled: false
|
||||
spancheck:
|
||||
# https://github.com/jjti/go-spancheck#checks
|
||||
checks:
|
||||
- end
|
||||
staticcheck:
|
||||
# https://staticcheck.io/docs/options#checks
|
||||
checks:
|
||||
- all
|
||||
- -SA6002A # Storing non-pointer values in sync.Pool allocates memory
|
||||
- -SA1019 # Using a deprecated function, variable, constant or field
|
||||
- -QF1008 # Unnecessary embedded expressions
|
||||
tagalign:
|
||||
align: true
|
||||
sort: true
|
||||
order:
|
||||
- serialize
|
||||
strict: true
|
||||
testifylint:
|
||||
# Enable all checkers (https://github.com/Antonboom/testifylint#checkers).
|
||||
# Default: false
|
||||
enable-all: true
|
||||
# Disable checkers by name
|
||||
# (in addition to default
|
||||
# suite-thelper
|
||||
# ).
|
||||
disable:
|
||||
- go-require
|
||||
- float-compare
|
||||
unused:
|
||||
# Mark all struct fields that have been written to as used.
|
||||
# Default: true
|
||||
field-writes-are-uses: false
|
||||
# Treat IncDec statement (e.g. `i++` or `i--`) as both read and write operation instead of just write.
|
||||
# Default: false
|
||||
post-statements-are-reads: true
|
||||
# Mark all local variables as used.
|
||||
# default: true
|
||||
local-variables-are-used: false
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Publish Antithesis Images
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
REGISTRY: us-central1-docker.pkg.dev
|
||||
REPOSITORY: molten-verve-216720/avalanche-repository
|
||||
|
||||
jobs:
|
||||
antithesis:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to GAR
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: _json_key
|
||||
password: ${{ secrets.ANTITHESIS_GAR_JSON_KEY }}
|
||||
|
||||
- name: Build and publish images
|
||||
run: ./scripts/run_task.sh build-antithesis-images
|
||||
env:
|
||||
IMAGE_PREFIX: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}
|
||||
IMAGE_TAG: latest
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Trigger Antithesis
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# When adjusting the schedule and duration of execution, make sure
|
||||
# to check the schedule of antithesis jobs scheduled by other
|
||||
# repos (avalanchego and hypersdk). Ideally the schedules for
|
||||
# different test setups should not overlap to avoid unintended
|
||||
# resource contention.
|
||||
- cron: '0 14 * * *' # Every day at 2PM UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
duration:
|
||||
description: 'The duration (in hours) to run the test for'
|
||||
default: '0.5'
|
||||
required: true
|
||||
type: string
|
||||
recipients:
|
||||
description: 'Comma-seperated email addresses to send the test report to'
|
||||
required: true
|
||||
type: string
|
||||
image_tag:
|
||||
description: 'The image tag to target'
|
||||
default: latest
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
antithesis:
|
||||
name: Run Antithesis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: antithesishq/antithesis-trigger-action@6c0a27302c0a3cd97d87d40bd6704e673abed4bb #v0.9
|
||||
with:
|
||||
notebook_name: avalanche
|
||||
tenant: avalanche
|
||||
username: ${{ secrets.ANTITHESIS_USERNAME }}
|
||||
password: ${{ secrets.ANTITHESIS_PASSWORD }}
|
||||
github_token: ${{ secrets.ANTITHESIS_GH_PAT }}
|
||||
config_image: antithesis-subnet-evm-config:${{ github.event.inputs.image_tag || 'latest' }}
|
||||
images: antithesis-subnet-evm-workload:${{ github.event.inputs.image_tag || 'latest' }};antithesis-subnet-evm-node:${{ github.event.inputs.image_tag || 'latest' }}
|
||||
email_recipients: ${{ github.event.inputs.recipients || secrets.ANTITHESIS_RECIPIENTS }}
|
||||
# Duration is in hours
|
||||
additional_parameters: |-
|
||||
custom.duration=${{ github.event.inputs.duration || '7.5' }}
|
||||
custom.workload=subnet-evm
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package extstate
|
||||
|
||||
import (
|
||||
"github.com/ava-labs/libevm/core/state"
|
||||
"github.com/ava-labs/libevm/ethdb"
|
||||
"github.com/ava-labs/libevm/triedb"
|
||||
|
||||
"github.com/ava-labs/subnet-evm/triedb/firewood"
|
||||
)
|
||||
|
||||
func NewDatabaseWithConfig(db ethdb.Database, config *triedb.Config) state.Database {
|
||||
coredb := state.NewDatabaseWithConfig(db, config)
|
||||
return wrapIfFirewood(coredb)
|
||||
}
|
||||
|
||||
func NewDatabaseWithNodeDB(db ethdb.Database, triedb *triedb.Database) state.Database {
|
||||
coredb := state.NewDatabaseWithNodeDB(db, triedb)
|
||||
return wrapIfFirewood(coredb)
|
||||
}
|
||||
|
||||
func wrapIfFirewood(db state.Database) state.Database {
|
||||
fw, ok := db.TrieDB().Backend().(*firewood.Database)
|
||||
if !ok {
|
||||
return db
|
||||
}
|
||||
return &firewoodAccessorDb{
|
||||
Database: db,
|
||||
fw: fw,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package extstate
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/ava-labs/libevm/common"
|
||||
"github.com/ava-labs/libevm/core/rawdb"
|
||||
"github.com/ava-labs/libevm/core/state"
|
||||
"github.com/ava-labs/libevm/core/types"
|
||||
"github.com/ava-labs/libevm/crypto"
|
||||
"github.com/ava-labs/libevm/libevm/stateconf"
|
||||
"github.com/ava-labs/libevm/trie/trienode"
|
||||
"github.com/ava-labs/libevm/triedb"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ava-labs/subnet-evm/triedb/firewood"
|
||||
"github.com/ava-labs/subnet-evm/triedb/hashdb"
|
||||
)
|
||||
|
||||
const (
|
||||
commit byte = iota
|
||||
createAccount
|
||||
updateAccount
|
||||
deleteAccount
|
||||
addStorage
|
||||
updateStorage
|
||||
deleteStorage
|
||||
maxStep
|
||||
)
|
||||
|
||||
var stepMap = map[byte]string{
|
||||
commit: "commit",
|
||||
createAccount: "createAccount",
|
||||
updateAccount: "updateAccount",
|
||||
deleteAccount: "deleteAccount",
|
||||
addStorage: "addStorage",
|
||||
updateStorage: "updateStorage",
|
||||
deleteStorage: "deleteStorage",
|
||||
}
|
||||
|
||||
type fuzzState struct {
|
||||
require *require.Assertions
|
||||
|
||||
// current state
|
||||
currentAddrs []common.Address
|
||||
currentStorageInputIndices map[common.Address]uint64
|
||||
inputCounter uint64
|
||||
blockNumber uint64
|
||||
|
||||
// pending changes to be committed
|
||||
merkleTries []*merkleTrie
|
||||
}
|
||||
type merkleTrie struct {
|
||||
name string
|
||||
ethDatabase state.Database
|
||||
accountTrie state.Trie
|
||||
openStorageTries map[common.Address]state.Trie
|
||||
lastRoot common.Hash
|
||||
}
|
||||
|
||||
func newFuzzState(t *testing.T) *fuzzState {
|
||||
r := require.New(t)
|
||||
|
||||
hashState := NewDatabaseWithConfig(
|
||||
rawdb.NewMemoryDatabase(),
|
||||
&triedb.Config{
|
||||
DBOverride: hashdb.Defaults.BackendConstructor,
|
||||
})
|
||||
ethRoot := types.EmptyRootHash
|
||||
hashTr, err := hashState.OpenTrie(ethRoot)
|
||||
r.NoError(err)
|
||||
t.Cleanup(func() {
|
||||
r.NoError(hashState.TrieDB().Close())
|
||||
})
|
||||
|
||||
firewoodMemdb := rawdb.NewMemoryDatabase()
|
||||
fwCfg := firewood.Defaults
|
||||
fwCfg.FilePath = filepath.Join(t.TempDir(), "firewood") // Use a temporary directory for the Firewood
|
||||
firewoodState := NewDatabaseWithConfig(
|
||||
firewoodMemdb,
|
||||
&triedb.Config{
|
||||
DBOverride: fwCfg.BackendConstructor,
|
||||
},
|
||||
)
|
||||
fwTr, err := firewoodState.OpenTrie(ethRoot)
|
||||
r.NoError(err)
|
||||
t.Cleanup(func() {
|
||||
r.NoError(firewoodState.TrieDB().Close())
|
||||
})
|
||||
|
||||
return &fuzzState{
|
||||
merkleTries: []*merkleTrie{
|
||||
{
|
||||
name: "hash",
|
||||
ethDatabase: hashState,
|
||||
accountTrie: hashTr,
|
||||
openStorageTries: make(map[common.Address]state.Trie),
|
||||
lastRoot: ethRoot,
|
||||
},
|
||||
{
|
||||
name: "firewood",
|
||||
ethDatabase: firewoodState,
|
||||
accountTrie: fwTr,
|
||||
openStorageTries: make(map[common.Address]state.Trie),
|
||||
lastRoot: ethRoot,
|
||||
},
|
||||
},
|
||||
currentStorageInputIndices: make(map[common.Address]uint64),
|
||||
require: r,
|
||||
}
|
||||
}
|
||||
|
||||
// commit writes the pending changes to both tries and clears the pending changes
|
||||
func (fs *fuzzState) commit() {
|
||||
for _, tr := range fs.merkleTries {
|
||||
mergedNodeSet := trienode.NewMergedNodeSet()
|
||||
for addr, str := range tr.openStorageTries {
|
||||
accountStateRoot, set, err := str.Commit(false)
|
||||
fs.require.NoError(err, "failed to commit storage trie for account %s in %s", addr.Hex(), tr.name)
|
||||
// A no-op change returns a nil set, which will cause merge to panic.
|
||||
if set != nil {
|
||||
fs.require.NoError(mergedNodeSet.Merge(set), "failed to merge storage trie nodeset for account %s in %s", addr.Hex(), tr.name)
|
||||
}
|
||||
|
||||
acc, err := tr.accountTrie.GetAccount(addr)
|
||||
fs.require.NoError(err, "failed to get account %s in %s", addr.Hex(), tr.name)
|
||||
// If the account was deleted, we can skip updating the account's
|
||||
// state root.
|
||||
fs.require.NotNil(acc, "account %s is nil in %s", addr.Hex(), tr.name)
|
||||
|
||||
acc.Root = accountStateRoot
|
||||
fs.require.NoError(tr.accountTrie.UpdateAccount(addr, acc), "failed to update account %s in %s", addr.Hex(), tr.name)
|
||||
}
|
||||
|
||||
updatedRoot, set, err := tr.accountTrie.Commit(true)
|
||||
fs.require.NoError(err, "failed to commit account trie in %s", tr.name)
|
||||
|
||||
// A no-op change returns a nil set, which will cause merge to panic.
|
||||
if set != nil {
|
||||
fs.require.NoError(mergedNodeSet.Merge(set), "failed to merge account trie nodeset in %s", tr.name)
|
||||
}
|
||||
|
||||
// HashDB/PathDB only allows updating the triedb if there have been changes.
|
||||
if _, ok := tr.ethDatabase.TrieDB().Backend().(*firewood.Database); ok {
|
||||
triedbopt := stateconf.WithTrieDBUpdatePayload(common.Hash{byte(int64(fs.blockNumber - 1))}, common.Hash{byte(int64(fs.blockNumber))})
|
||||
fs.require.NoError(tr.ethDatabase.TrieDB().Update(updatedRoot, tr.lastRoot, fs.blockNumber, mergedNodeSet, nil, triedbopt), "failed to update triedb in %s", tr.name)
|
||||
tr.lastRoot = updatedRoot
|
||||
} else if updatedRoot != tr.lastRoot {
|
||||
fs.require.NoError(tr.ethDatabase.TrieDB().Update(updatedRoot, tr.lastRoot, fs.blockNumber, mergedNodeSet, nil), "failed to update triedb in %s", tr.name)
|
||||
tr.lastRoot = updatedRoot
|
||||
}
|
||||
tr.openStorageTries = make(map[common.Address]state.Trie)
|
||||
fs.require.NoError(tr.ethDatabase.TrieDB().Commit(updatedRoot, true),
|
||||
"failed to commit %s: expected hashdb root %s", tr.name, fs.merkleTries[0].lastRoot.Hex())
|
||||
tr.accountTrie, err = tr.ethDatabase.OpenTrie(tr.lastRoot)
|
||||
fs.require.NoError(err, "failed to reopen account trie for %s", tr.name)
|
||||
}
|
||||
fs.blockNumber++
|
||||
|
||||
// After computing the new root for each trie, we can confirm that the hashing matches
|
||||
expectedRoot := fs.merkleTries[0].lastRoot
|
||||
for i, tr := range fs.merkleTries[1:] {
|
||||
fs.require.Equalf(expectedRoot, tr.lastRoot,
|
||||
"root mismatch for %s: expected %x, got %x (trie index %d)",
|
||||
tr.name, expectedRoot.Hex(), tr.lastRoot.Hex(), i,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// createAccount generates a new, unique account and adds it to both tries and the tracked
|
||||
// current state.
|
||||
func (fs *fuzzState) createAccount() {
|
||||
fs.inputCounter++
|
||||
addr := common.BytesToAddress(crypto.Keccak256Hash(binary.BigEndian.AppendUint64(nil, fs.inputCounter)).Bytes())
|
||||
acc := &types.StateAccount{
|
||||
Nonce: 1,
|
||||
Balance: uint256.NewInt(100),
|
||||
Root: types.EmptyRootHash,
|
||||
CodeHash: types.EmptyCodeHash[:],
|
||||
}
|
||||
fs.currentAddrs = append(fs.currentAddrs, addr)
|
||||
|
||||
for _, tr := range fs.merkleTries {
|
||||
fs.require.NoError(tr.accountTrie.UpdateAccount(addr, acc), "failed to create account %s in %s", addr.Hex(), tr.name)
|
||||
}
|
||||
}
|
||||
|
||||
// selectAccount returns a random account and account hash for the provided index
|
||||
// assumes: addrIndex < len(tr.currentAddrs)
|
||||
func (fs *fuzzState) selectAccount(addrIndex int) common.Address {
|
||||
return fs.currentAddrs[addrIndex]
|
||||
}
|
||||
|
||||
// updateAccount selects a random account, increments its nonce, and adds the update
|
||||
// to the pending changes for both tries.
|
||||
func (fs *fuzzState) updateAccount(addrIndex int) {
|
||||
addr := fs.selectAccount(addrIndex)
|
||||
|
||||
for _, tr := range fs.merkleTries {
|
||||
acc, err := tr.accountTrie.GetAccount(addr)
|
||||
fs.require.NoError(err, "failed to get account %s for update in %s", addr.Hex(), tr.name)
|
||||
fs.require.NotNil(acc, "account %s is nil for update in %s", addr.Hex(), tr.name)
|
||||
acc.Nonce++
|
||||
acc.CodeHash = crypto.Keccak256Hash(acc.CodeHash[:]).Bytes()
|
||||
acc.Balance.Add(acc.Balance, uint256.NewInt(3))
|
||||
fs.require.NoError(tr.accountTrie.UpdateAccount(addr, acc), "failed to update account %s in %s", addr.Hex(), tr.name)
|
||||
}
|
||||
}
|
||||
|
||||
// deleteAccount selects a random account and deletes it from both tries and the tracked
|
||||
// current state.
|
||||
func (fs *fuzzState) deleteAccount(accountIndex int) {
|
||||
deleteAddr := fs.selectAccount(accountIndex)
|
||||
fs.currentAddrs = slices.DeleteFunc(fs.currentAddrs, func(addr common.Address) bool {
|
||||
return deleteAddr == addr
|
||||
})
|
||||
for _, tr := range fs.merkleTries {
|
||||
fs.require.NoError(tr.accountTrie.DeleteAccount(deleteAddr), "failed to delete account %s in %s", deleteAddr.Hex(), tr.name)
|
||||
delete(tr.openStorageTries, deleteAddr) // remove any open storage trie for the deleted account
|
||||
}
|
||||
}
|
||||
|
||||
// openStorageTrie opens the storage trie for the provided account address.
|
||||
// Uses an already opened trie, if there's a pending update to the ethereum nested
|
||||
// storage trie.
|
||||
//
|
||||
// must maintain a map of currently open storage tries, so we can defer committing them
|
||||
// until commit as opposed to after each storage update.
|
||||
// This mimics the actual handling of state commitments in the EVM where storage tries are all committed immediately
|
||||
// before updating the account trie along with the updated storage trie roots:
|
||||
// https://github.com/ava-labs/libevm/blob/0bfe4a0380c86d7c9bf19fe84368b9695fcb96c7/core/state/statedb.go#L1155
|
||||
//
|
||||
// If we attempt to commit the storage tries after each operation, then attempting to re-open the storage trie
|
||||
// with an updated storage trie root from ethDatabase will fail since the storage trie root will not have been
|
||||
// persisted yet - leading to a missing trie node error.
|
||||
func (fs *fuzzState) openStorageTrie(addr common.Address, tr *merkleTrie) state.Trie {
|
||||
storageTrie, ok := tr.openStorageTries[addr]
|
||||
if ok {
|
||||
return storageTrie
|
||||
}
|
||||
|
||||
acc, err := tr.accountTrie.GetAccount(addr)
|
||||
fs.require.NoError(err, "failed to get account %s for storage trie in %s", addr.Hex(), tr.name)
|
||||
fs.require.NotNil(acc, "account %s not found in %s", addr.Hex(), tr.name)
|
||||
storageTrie, err = tr.ethDatabase.OpenStorageTrie(tr.lastRoot, addr, acc.Root, tr.accountTrie)
|
||||
fs.require.NoError(err, "failed to open storage trie for %s in %s", addr.Hex(), tr.name)
|
||||
tr.openStorageTries[addr] = storageTrie
|
||||
return storageTrie
|
||||
}
|
||||
|
||||
// addStorage selects an account and adds a new storage key-value pair to the account.
|
||||
func (fs *fuzzState) addStorage(accountIndex int) {
|
||||
addr := fs.selectAccount(accountIndex)
|
||||
// Increment storageInputIndices for the account and take the next input to generate
|
||||
// a new storage key-value pair for the account.
|
||||
fs.currentStorageInputIndices[addr]++
|
||||
storageIndex := fs.currentStorageInputIndices[addr]
|
||||
key := crypto.Keccak256Hash(binary.BigEndian.AppendUint64(nil, storageIndex))
|
||||
keyHash := crypto.Keccak256Hash(key[:])
|
||||
val := crypto.Keccak256Hash(keyHash[:])
|
||||
|
||||
for _, tr := range fs.merkleTries {
|
||||
str := fs.openStorageTrie(addr, tr)
|
||||
fs.require.NoError(str.UpdateStorage(addr, key[:], val[:]), "failed to add storage for account %s in %s", addr.Hex(), tr.name)
|
||||
}
|
||||
|
||||
fs.currentStorageInputIndices[addr]++
|
||||
}
|
||||
|
||||
// updateStorage selects an account and updates an existing storage key-value pair
|
||||
// note: this may "update" a key-value pair that doesn't exist if it was previously deleted.
|
||||
func (fs *fuzzState) updateStorage(accountIndex int, storageIndexInput uint64) {
|
||||
addr := fs.selectAccount(accountIndex)
|
||||
storageIndex := fs.currentStorageInputIndices[addr]
|
||||
storageIndex %= storageIndexInput
|
||||
|
||||
storageKey := crypto.Keccak256Hash(binary.BigEndian.AppendUint64(nil, storageIndex))
|
||||
storageKeyHash := crypto.Keccak256Hash(storageKey[:])
|
||||
fs.inputCounter++
|
||||
updatedValInput := binary.BigEndian.AppendUint64(storageKeyHash[:], fs.inputCounter)
|
||||
updatedVal := crypto.Keccak256Hash(updatedValInput[:])
|
||||
|
||||
for _, tr := range fs.merkleTries {
|
||||
str := fs.openStorageTrie(addr, tr)
|
||||
fs.require.NoError(str.UpdateStorage(addr, storageKey[:], updatedVal[:]), "failed to update storage for account %s in %s", addr.Hex(), tr.name)
|
||||
}
|
||||
}
|
||||
|
||||
// deleteStorage selects an account and deletes an existing storage key-value pair
|
||||
// note: this may "delete" a key-value pair that doesn't exist if it was previously deleted.
|
||||
func (fs *fuzzState) deleteStorage(accountIndex int, storageIndexInput uint64) {
|
||||
addr := fs.selectAccount(accountIndex)
|
||||
storageIndex := fs.currentStorageInputIndices[addr]
|
||||
storageIndex %= storageIndexInput
|
||||
storageKey := crypto.Keccak256Hash(binary.BigEndian.AppendUint64(nil, storageIndex))
|
||||
|
||||
for _, tr := range fs.merkleTries {
|
||||
str := fs.openStorageTrie(addr, tr)
|
||||
fs.require.NoError(str.DeleteStorage(addr, storageKey[:]), "failed to delete storage for account %s in %s", addr.Hex(), tr.name)
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzTree(f *testing.F) {
|
||||
for randSeed := range int64(1000) {
|
||||
rand := rand.New(rand.NewSource(randSeed))
|
||||
steps := make([]byte, 32)
|
||||
_, err := rand.Read(steps)
|
||||
if err != nil {
|
||||
f.Fatal(err)
|
||||
}
|
||||
f.Add(randSeed, steps)
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, randSeed int64, byteSteps []byte) {
|
||||
fuzzState := newFuzzState(t)
|
||||
rand := rand.New(rand.NewSource(randSeed))
|
||||
|
||||
for range 10 {
|
||||
fuzzState.createAccount()
|
||||
}
|
||||
fuzzState.commit()
|
||||
|
||||
const maxSteps = 1000
|
||||
if len(byteSteps) > maxSteps {
|
||||
byteSteps = byteSteps[:maxSteps]
|
||||
}
|
||||
|
||||
for _, step := range byteSteps {
|
||||
step = step % maxStep
|
||||
t.Log(stepMap[step])
|
||||
switch step {
|
||||
case commit:
|
||||
fuzzState.commit()
|
||||
case createAccount:
|
||||
fuzzState.createAccount()
|
||||
case updateAccount:
|
||||
if len(fuzzState.currentAddrs) > 0 {
|
||||
fuzzState.updateAccount(rand.Intn(len(fuzzState.currentAddrs)))
|
||||
}
|
||||
case deleteAccount:
|
||||
if len(fuzzState.currentAddrs) > 0 {
|
||||
fuzzState.deleteAccount(rand.Intn(len(fuzzState.currentAddrs)))
|
||||
}
|
||||
case addStorage:
|
||||
if len(fuzzState.currentAddrs) > 0 {
|
||||
fuzzState.addStorage(rand.Intn(len(fuzzState.currentAddrs)))
|
||||
}
|
||||
case updateStorage:
|
||||
if len(fuzzState.currentAddrs) > 0 {
|
||||
fuzzState.updateStorage(rand.Intn(len(fuzzState.currentAddrs)), rand.Uint64())
|
||||
}
|
||||
case deleteStorage:
|
||||
if len(fuzzState.currentAddrs) > 0 {
|
||||
fuzzState.deleteStorage(rand.Intn(len(fuzzState.currentAddrs)), rand.Uint64())
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unknown step: %d", step)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package extstate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ava-labs/libevm/common"
|
||||
"github.com/ava-labs/libevm/core/state"
|
||||
|
||||
"github.com/ava-labs/subnet-evm/triedb/firewood"
|
||||
)
|
||||
|
||||
var (
|
||||
_ state.Database = (*firewoodAccessorDb)(nil)
|
||||
_ state.Trie = (*firewood.AccountTrie)(nil)
|
||||
_ state.Trie = (*firewood.StorageTrie)(nil)
|
||||
)
|
||||
|
||||
type firewoodAccessorDb struct {
|
||||
state.Database
|
||||
fw *firewood.Database
|
||||
}
|
||||
|
||||
// OpenTrie opens the main account trie.
|
||||
func (db *firewoodAccessorDb) OpenTrie(root common.Hash) (state.Trie, error) {
|
||||
return firewood.NewAccountTrie(root, db.fw)
|
||||
}
|
||||
|
||||
// OpenStorageTrie opens a wrapped version of the account trie.
|
||||
func (*firewoodAccessorDb) OpenStorageTrie(_ common.Hash, _ common.Address, accountRoot common.Hash, self state.Trie) (state.Trie, error) {
|
||||
accountTrie, ok := self.(*firewood.AccountTrie)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Invalid account trie type: %T", self)
|
||||
}
|
||||
return firewood.NewStorageTrie(accountTrie, accountRoot)
|
||||
}
|
||||
|
||||
// CopyTrie returns a deep copy of the given trie.
|
||||
// It can be altered by the caller.
|
||||
func (*firewoodAccessorDb) CopyTrie(t state.Trie) state.Trie {
|
||||
switch t := t.(type) {
|
||||
case *firewood.AccountTrie:
|
||||
return t.Copy()
|
||||
case *firewood.StorageTrie:
|
||||
return nil // The storage trie just wraps the account trie, so we must re-open it separately.
|
||||
default:
|
||||
panic(fmt.Errorf("unknown trie type %T", t))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package extstate
|
||||
|
||||
import (
|
||||
"github.com/luxfi/geth/core/state"
|
||||
|
||||
"github.com/luxfi/evm/utils"
|
||||
)
|
||||
|
||||
type workerPool struct {
|
||||
*utils.BoundedWorkers
|
||||
}
|
||||
|
||||
func (wp *workerPool) Done() {
|
||||
// Done is guaranteed to only be called after all work is already complete,
|
||||
// so we call Wait for goroutines to finish before returning.
|
||||
wp.BoundedWorkers.Wait()
|
||||
}
|
||||
|
||||
func WithConcurrentWorkers(prefetchers int) state.PrefetcherOption {
|
||||
pool := &workerPool{
|
||||
BoundedWorkers: utils.NewBoundedWorkers(prefetchers),
|
||||
}
|
||||
return state.WithWorkerPools(func() state.WorkerPool { return pool })
|
||||
}
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
//
|
||||
// This file is a derived work, based on the go-ethereum library whose original
|
||||
// notices appear below.
|
||||
//
|
||||
// It is distributed under a license compatible with the licensing terms of the
|
||||
// original code from which it is derived.
|
||||
//
|
||||
// Much love to the original authors for their work.
|
||||
// **********
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
const (
|
||||
timeFormat = "2006-01-02T15:04:05-0700"
|
||||
floatFormat = 'f'
|
||||
termMsgJust = 40
|
||||
termCtxMaxPadding = 40
|
||||
)
|
||||
|
||||
// 40 spaces
|
||||
var spaces = []byte(" ")
|
||||
|
||||
// TerminalStringer is an analogous interface to the stdlib stringer, allowing
|
||||
// own types to have custom shortened serialization formats when printed to the
|
||||
// screen.
|
||||
type TerminalStringer interface {
|
||||
TerminalString() string
|
||||
}
|
||||
|
||||
func (h *TerminalHandler) format(buf []byte, r slog.Record, usecolor bool) []byte {
|
||||
msg := escapeMessage(r.Message)
|
||||
var color = ""
|
||||
if usecolor {
|
||||
switch r.Level {
|
||||
case LevelCrit:
|
||||
color = "\x1b[35m"
|
||||
case slog.LevelError:
|
||||
color = "\x1b[31m"
|
||||
case slog.LevelWarn:
|
||||
color = "\x1b[33m"
|
||||
case slog.LevelInfo:
|
||||
color = "\x1b[32m"
|
||||
case slog.LevelDebug:
|
||||
color = "\x1b[36m"
|
||||
case LevelTrace:
|
||||
color = "\x1b[34m"
|
||||
}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, 0, 30+termMsgJust)
|
||||
}
|
||||
b := bytes.NewBuffer(buf)
|
||||
|
||||
// Note the timestamp is moved before the log level compared to upstream
|
||||
b.WriteString("[")
|
||||
writeTimeTermFormat(b, r.Time)
|
||||
b.WriteString("] ")
|
||||
|
||||
if color != "" { // Start color
|
||||
b.WriteString(color)
|
||||
b.WriteString(LevelAlignedString(r.Level))
|
||||
b.WriteString("\x1b[0m")
|
||||
} else {
|
||||
b.WriteString(LevelAlignedString(r.Level))
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
|
||||
// Prefix is added compared to upstream
|
||||
if h.Prefix != nil {
|
||||
b.WriteString(h.Prefix(r))
|
||||
}
|
||||
|
||||
b.WriteString(msg)
|
||||
|
||||
// try to justify the log output for short messages
|
||||
//length := utf8.RuneCountInString(msg)
|
||||
length := len(msg)
|
||||
if (r.NumAttrs()+len(h.attrs)) > 0 && length < termMsgJust {
|
||||
b.Write(spaces[:termMsgJust-length])
|
||||
}
|
||||
// print the attributes
|
||||
h.formatAttributes(b, r, color)
|
||||
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func (h *TerminalHandler) formatAttributes(buf *bytes.Buffer, r slog.Record, color string) {
|
||||
// tmp is a temporary buffer we use, until bytes.Buffer.AvailableBuffer() (1.21)
|
||||
// can be used.
|
||||
var tmp = make([]byte, 40)
|
||||
writeAttr := func(attr slog.Attr, _, last bool) {
|
||||
buf.WriteByte(' ')
|
||||
|
||||
if color != "" {
|
||||
buf.WriteString(color)
|
||||
//buf.Write(appendEscapeString(buf.AvailableBuffer(), attr.Key))
|
||||
buf.Write(appendEscapeString(tmp[:0], attr.Key))
|
||||
buf.WriteString("\x1b[0m=")
|
||||
} else {
|
||||
//buf.Write(appendEscapeString(buf.AvailableBuffer(), attr.Key))
|
||||
buf.Write(appendEscapeString(tmp[:0], attr.Key))
|
||||
buf.WriteByte('=')
|
||||
}
|
||||
//val := FormatSlogValue(attr.Value, true, buf.AvailableBuffer())
|
||||
val := FormatSlogValue(attr.Value, tmp[:0])
|
||||
|
||||
padding := h.fieldPadding[attr.Key]
|
||||
|
||||
length := utf8.RuneCount(val)
|
||||
if padding < length && length <= termCtxMaxPadding {
|
||||
padding = length
|
||||
h.fieldPadding[attr.Key] = padding
|
||||
}
|
||||
buf.Write(val)
|
||||
if !last && padding > length {
|
||||
buf.Write(spaces[:padding-length])
|
||||
}
|
||||
}
|
||||
var n = 0
|
||||
var nAttrs = len(h.attrs) + r.NumAttrs()
|
||||
for _, attr := range h.attrs {
|
||||
writeAttr(attr, n == 0, n == nAttrs-1)
|
||||
n++
|
||||
}
|
||||
r.Attrs(func(attr slog.Attr) bool {
|
||||
writeAttr(attr, n == 0, n == nAttrs-1)
|
||||
n++
|
||||
return true
|
||||
})
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
// FormatSlogValue formats a slog.Value for serialization to terminal.
|
||||
func FormatSlogValue(v slog.Value, tmp []byte) (result []byte) {
|
||||
var value any
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() {
|
||||
result = []byte("<nil>")
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
switch v.Kind() {
|
||||
case slog.KindString:
|
||||
return appendEscapeString(tmp, v.String())
|
||||
case slog.KindInt64: // All int-types (int8, int16 etc) wind up here
|
||||
return appendInt64(tmp, v.Int64())
|
||||
case slog.KindUint64: // All uint-types (uint8, uint16 etc) wind up here
|
||||
return appendUint64(tmp, v.Uint64(), false)
|
||||
case slog.KindFloat64:
|
||||
return strconv.AppendFloat(tmp, v.Float64(), floatFormat, 3, 64)
|
||||
case slog.KindBool:
|
||||
return strconv.AppendBool(tmp, v.Bool())
|
||||
case slog.KindDuration:
|
||||
value = v.Duration()
|
||||
case slog.KindTime:
|
||||
// Performance optimization: No need for escaping since the provided
|
||||
// timeFormat doesn't have any escape characters, and escaping is
|
||||
// expensive.
|
||||
return v.Time().AppendFormat(tmp, timeFormat)
|
||||
default:
|
||||
value = v.Any()
|
||||
}
|
||||
if value == nil {
|
||||
return []byte("<nil>")
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case *big.Int: // Need to be before fmt.Stringer-clause
|
||||
return appendBigInt(tmp, v)
|
||||
case *uint256.Int: // Need to be before fmt.Stringer-clause
|
||||
return appendU256(tmp, v)
|
||||
case error:
|
||||
return appendEscapeString(tmp, v.Error())
|
||||
case TerminalStringer:
|
||||
return appendEscapeString(tmp, v.TerminalString())
|
||||
case fmt.Stringer:
|
||||
return appendEscapeString(tmp, v.String())
|
||||
}
|
||||
|
||||
// We can use the 'tmp' as a scratch-buffer, to first format the
|
||||
// value, and in a second step do escaping.
|
||||
internal := fmt.Appendf(tmp, "%+v", value)
|
||||
return appendEscapeString(tmp, string(internal))
|
||||
}
|
||||
|
||||
// appendInt64 formats n with thousand separators and writes into buffer dst.
|
||||
func appendInt64(dst []byte, n int64) []byte {
|
||||
if n < 0 {
|
||||
return appendUint64(dst, uint64(-n), true)
|
||||
}
|
||||
return appendUint64(dst, uint64(n), false)
|
||||
}
|
||||
|
||||
// appendUint64 formats n with thousand separators and writes into buffer dst.
|
||||
func appendUint64(dst []byte, n uint64, neg bool) []byte {
|
||||
// Small numbers are fine as is
|
||||
if n < 100000 {
|
||||
if neg {
|
||||
return strconv.AppendInt(dst, -int64(n), 10)
|
||||
} else {
|
||||
return strconv.AppendInt(dst, int64(n), 10)
|
||||
}
|
||||
}
|
||||
// Large numbers should be split
|
||||
const maxLength = 26
|
||||
|
||||
var (
|
||||
out = make([]byte, maxLength)
|
||||
i = maxLength - 1
|
||||
comma = 0
|
||||
)
|
||||
for ; n > 0; i-- {
|
||||
if comma == 3 {
|
||||
comma = 0
|
||||
out[i] = ','
|
||||
} else {
|
||||
comma++
|
||||
out[i] = '0' + byte(n%10)
|
||||
n /= 10
|
||||
}
|
||||
}
|
||||
if neg {
|
||||
out[i] = '-'
|
||||
i--
|
||||
}
|
||||
return append(dst, out[i+1:]...)
|
||||
}
|
||||
|
||||
// FormatLogfmtUint64 formats n with thousand separators.
|
||||
func FormatLogfmtUint64(n uint64) string {
|
||||
return string(appendUint64(nil, n, false))
|
||||
}
|
||||
|
||||
// appendBigInt formats n with thousand separators and writes to dst.
|
||||
func appendBigInt(dst []byte, n *big.Int) []byte {
|
||||
if n.IsUint64() {
|
||||
return appendUint64(dst, n.Uint64(), false)
|
||||
}
|
||||
if n.IsInt64() {
|
||||
return appendInt64(dst, n.Int64())
|
||||
}
|
||||
|
||||
var (
|
||||
text = n.String()
|
||||
buf = make([]byte, len(text)+len(text)/3)
|
||||
comma = 0
|
||||
i = len(buf) - 1
|
||||
)
|
||||
for j := len(text) - 1; j >= 0; j, i = j-1, i-1 {
|
||||
c := text[j]
|
||||
|
||||
switch {
|
||||
case c == '-':
|
||||
buf[i] = c
|
||||
case comma == 3:
|
||||
buf[i] = ','
|
||||
i--
|
||||
comma = 0
|
||||
fallthrough
|
||||
default:
|
||||
buf[i] = c
|
||||
comma++
|
||||
}
|
||||
}
|
||||
return append(dst, buf[i+1:]...)
|
||||
}
|
||||
|
||||
// appendU256 formats n with thousand separators.
|
||||
func appendU256(dst []byte, n *uint256.Int) []byte {
|
||||
if n.IsUint64() {
|
||||
return appendUint64(dst, n.Uint64(), false)
|
||||
}
|
||||
res := []byte(n.PrettyDec(','))
|
||||
return append(dst, res...)
|
||||
}
|
||||
|
||||
// appendEscapeString writes the string s to the given writer, with
|
||||
// escaping/quoting if needed.
|
||||
func appendEscapeString(dst []byte, s string) []byte {
|
||||
needsQuoting := false
|
||||
needsEscaping := false
|
||||
for _, r := range s {
|
||||
// If it contains spaces or equal-sign, we need to quote it.
|
||||
if r == ' ' || r == '=' {
|
||||
needsQuoting = true
|
||||
continue
|
||||
}
|
||||
// We need to escape it, if it contains
|
||||
// - character " (0x22) and lower (except space)
|
||||
// - characters above ~ (0x7E), plus equal-sign
|
||||
if r <= '"' || r > '~' {
|
||||
needsEscaping = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if needsEscaping {
|
||||
return strconv.AppendQuote(dst, s)
|
||||
}
|
||||
// No escaping needed, but we might have to place within quote-marks, in case
|
||||
// it contained a space
|
||||
if needsQuoting {
|
||||
dst = append(dst, '"')
|
||||
dst = append(dst, []byte(s)...)
|
||||
return append(dst, '"')
|
||||
}
|
||||
return append(dst, []byte(s)...)
|
||||
}
|
||||
|
||||
// escapeMessage checks if the provided string needs escaping/quoting, similarly
|
||||
// to escapeString. The difference is that this method is more lenient: it allows
|
||||
// for spaces and linebreaks to occur without needing quoting.
|
||||
func escapeMessage(s string) string {
|
||||
needsQuoting := false
|
||||
for _, r := range s {
|
||||
// Allow CR/LF/TAB. This is to make multi-line messages work.
|
||||
if r == '\r' || r == '\n' || r == '\t' {
|
||||
continue
|
||||
}
|
||||
// We quote everything below <space> (0x20) and above~ (0x7E),
|
||||
// plus equal-sign
|
||||
if r < ' ' || r > '~' || r == '=' {
|
||||
needsQuoting = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !needsQuoting {
|
||||
return s
|
||||
}
|
||||
return strconv.Quote(s)
|
||||
}
|
||||
|
||||
// writeTimeTermFormat writes on the format "01-02|15:04:05.000"
|
||||
func writeTimeTermFormat(buf *bytes.Buffer, t time.Time) {
|
||||
_, month, day := t.Date()
|
||||
writePosIntWidth(buf, int(month), 2)
|
||||
buf.WriteByte('-')
|
||||
writePosIntWidth(buf, day, 2)
|
||||
buf.WriteByte('|')
|
||||
hour, min, sec := t.Clock()
|
||||
writePosIntWidth(buf, hour, 2)
|
||||
buf.WriteByte(':')
|
||||
writePosIntWidth(buf, min, 2)
|
||||
buf.WriteByte(':')
|
||||
writePosIntWidth(buf, sec, 2)
|
||||
ns := t.Nanosecond()
|
||||
buf.WriteByte('.')
|
||||
writePosIntWidth(buf, ns/1e6, 3)
|
||||
}
|
||||
|
||||
// writePosIntWidth writes non-negative integer i to the buffer, padded on the left
|
||||
// by zeroes to the given width. Use a width of 0 to omit padding.
|
||||
// Adapted from golang.org/x/exp/slog/internal/buffer/buffer.go
|
||||
func writePosIntWidth(b *bytes.Buffer, i, width int) {
|
||||
// Cheap integer to fixed-width decimal ASCII.
|
||||
// Copied from log/log.go.
|
||||
if i < 0 {
|
||||
panic("negative int")
|
||||
}
|
||||
// Assemble decimal in reverse order.
|
||||
var bb [20]byte
|
||||
bp := len(bb) - 1
|
||||
for i >= 10 || width > 1 {
|
||||
width--
|
||||
q := i / 10
|
||||
bb[bp] = byte('0' + i - q*10)
|
||||
bp--
|
||||
i = q
|
||||
}
|
||||
// i < 10
|
||||
bb[bp] = byte('0' + i)
|
||||
b.Write(bb[bp:])
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
//
|
||||
// This file is a derived work, based on the go-ethereum library whose original
|
||||
// notices appear below.
|
||||
//
|
||||
// It is distributed under a license compatible with the licensing terms of the
|
||||
// original code from which it is derived.
|
||||
//
|
||||
// Much love to the original authors for their work.
|
||||
// **********
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
type discardHandler struct{}
|
||||
|
||||
// DiscardHandler returns a no-op handler
|
||||
func DiscardHandler() slog.Handler {
|
||||
return &discardHandler{}
|
||||
}
|
||||
|
||||
func (h *discardHandler) Handle(_ context.Context, r slog.Record) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *discardHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *discardHandler) WithGroup(name string) slog.Handler {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (h *discardHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &discardHandler{}
|
||||
}
|
||||
|
||||
type TerminalHandler struct {
|
||||
mu sync.Mutex
|
||||
wr io.Writer
|
||||
lvl slog.Leveler
|
||||
useColor bool
|
||||
attrs []slog.Attr
|
||||
// fieldPadding is a map with maximum field value lengths seen until now
|
||||
// to allow padding log contexts in a bit smarter way.
|
||||
fieldPadding map[string]int
|
||||
|
||||
buf []byte
|
||||
|
||||
// Prefix returns a string that is output before each log message.
|
||||
Prefix func(r slog.Record) string
|
||||
}
|
||||
|
||||
// NewTerminalHandler returns a handler which formats log records at all levels optimized for human readability on
|
||||
// a terminal with color-coded level output and terser human friendly timestamp.
|
||||
// This format should only be used for interactive programs or while developing.
|
||||
//
|
||||
// [LEVEL] [TIME] MESSAGE key=value key=value ...
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// [DBUG] [May 16 20:58:45] remove route ns=haproxy addr=127.0.0.1:50002
|
||||
func NewTerminalHandler(wr io.Writer, useColor bool) *TerminalHandler {
|
||||
return NewTerminalHandlerWithLevel(wr, levelMaxVerbosity, useColor)
|
||||
}
|
||||
|
||||
// NewTerminalHandlerWithLevel returns the same handler as NewTerminalHandler but only outputs
|
||||
// records which are less than or equal to the specified verbosity level.
|
||||
func NewTerminalHandlerWithLevel(wr io.Writer, lvl slog.Leveler, useColor bool) *TerminalHandler {
|
||||
return &TerminalHandler{
|
||||
wr: wr,
|
||||
lvl: lvl,
|
||||
useColor: useColor,
|
||||
fieldPadding: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TerminalHandler) Handle(_ context.Context, r slog.Record) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
buf := h.format(h.buf, r, h.useColor)
|
||||
h.wr.Write(buf)
|
||||
h.buf = buf[:0]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TerminalHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return level >= h.lvl.Level()
|
||||
}
|
||||
|
||||
func (h *TerminalHandler) WithGroup(name string) slog.Handler {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (h *TerminalHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &TerminalHandler{
|
||||
wr: h.wr,
|
||||
lvl: h.lvl,
|
||||
useColor: h.useColor,
|
||||
attrs: append(h.attrs, attrs...),
|
||||
fieldPadding: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
// ResetFieldPadding zeroes the field-padding for all attribute pairs.
|
||||
func (t *TerminalHandler) ResetFieldPadding() {
|
||||
t.mu.Lock()
|
||||
t.fieldPadding = make(map[string]int)
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
// JSONHandler returns a handler which prints records in JSON format.
|
||||
func JSONHandler(wr io.Writer) slog.Handler {
|
||||
return slog.NewJSONHandler(wr, &slog.HandlerOptions{
|
||||
ReplaceAttr: builtinReplaceJSON,
|
||||
})
|
||||
}
|
||||
|
||||
func JSONHandlerWithLevel(wr io.Writer, level slog.Leveler) slog.Handler {
|
||||
return slog.NewJSONHandler(wr, &slog.HandlerOptions{
|
||||
ReplaceAttr: builtinReplaceJSON,
|
||||
Level: level,
|
||||
})
|
||||
}
|
||||
|
||||
// LogfmtHandler returns a handler which prints records in logfmt format, an easy machine-parseable but human-readable
|
||||
// format for key/value pairs.
|
||||
//
|
||||
// For more details see: http://godoc.org/github.com/kr/logfmt
|
||||
func LogfmtHandler(wr io.Writer) slog.Handler {
|
||||
return slog.NewTextHandler(wr, &slog.HandlerOptions{
|
||||
ReplaceAttr: builtinReplaceLogfmt,
|
||||
})
|
||||
}
|
||||
|
||||
// LogfmtHandlerWithLevel returns the same handler as LogfmtHandler but it only outputs
|
||||
// records which are less than or equal to the specified verbosity level.
|
||||
func LogfmtHandlerWithLevel(wr io.Writer, level slog.Leveler) slog.Handler {
|
||||
return slog.NewTextHandler(wr, &slog.HandlerOptions{
|
||||
ReplaceAttr: builtinReplaceLogfmt,
|
||||
Level: level,
|
||||
})
|
||||
}
|
||||
|
||||
func builtinReplaceLogfmt(_ []string, attr slog.Attr) slog.Attr {
|
||||
return builtinReplace(nil, attr, true)
|
||||
}
|
||||
|
||||
func builtinReplaceJSON(_ []string, attr slog.Attr) slog.Attr {
|
||||
return builtinReplace(nil, attr, false)
|
||||
}
|
||||
|
||||
func builtinReplace(_ []string, attr slog.Attr, logfmt bool) slog.Attr {
|
||||
switch attr.Key {
|
||||
case slog.TimeKey:
|
||||
if attr.Value.Kind() == slog.KindTime {
|
||||
if logfmt {
|
||||
return slog.String("timestamp", attr.Value.Time().Format(timeFormat))
|
||||
} else {
|
||||
return slog.Attr{Key: "timestamp", Value: attr.Value}
|
||||
}
|
||||
}
|
||||
case slog.LevelKey:
|
||||
if l, ok := attr.Value.Any().(slog.Level); ok {
|
||||
attr = slog.Any("level", LevelString(l))
|
||||
return attr
|
||||
}
|
||||
}
|
||||
|
||||
switch v := attr.Value.Any().(type) {
|
||||
case time.Time:
|
||||
if logfmt {
|
||||
attr = slog.String(attr.Key, v.Format(timeFormat))
|
||||
}
|
||||
case *big.Int:
|
||||
if v == nil {
|
||||
attr.Value = slog.StringValue("<nil>")
|
||||
} else {
|
||||
attr.Value = slog.StringValue(v.String())
|
||||
}
|
||||
case *uint256.Int:
|
||||
if v == nil {
|
||||
attr.Value = slog.StringValue("<nil>")
|
||||
} else {
|
||||
attr.Value = slog.StringValue(v.Dec())
|
||||
}
|
||||
case fmt.Stringer:
|
||||
if v == nil || (reflect.ValueOf(v).Kind() == reflect.Pointer && reflect.ValueOf(v).IsNil()) {
|
||||
attr.Value = slog.StringValue("<nil>")
|
||||
} else {
|
||||
attr.Value = slog.StringValue(v.String())
|
||||
}
|
||||
}
|
||||
return attr
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
//
|
||||
// This file is a derived work, based on the go-ethereum library whose original
|
||||
// notices appear below.
|
||||
//
|
||||
// It is distributed under a license compatible with the licensing terms of the
|
||||
// original code from which it is derived.
|
||||
//
|
||||
// Much love to the original authors for their work.
|
||||
// **********
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
const errorKey = "LOG_ERROR"
|
||||
|
||||
const (
|
||||
legacyLevelCrit = iota
|
||||
legacyLevelError
|
||||
legacyLevelWarn
|
||||
legacyLevelInfo
|
||||
legacyLevelDebug
|
||||
legacyLevelTrace
|
||||
)
|
||||
|
||||
const (
|
||||
levelMaxVerbosity slog.Level = math.MinInt
|
||||
LevelTrace slog.Level = -8
|
||||
LevelDebug = slog.LevelDebug
|
||||
LevelInfo = slog.LevelInfo
|
||||
LevelWarn = slog.LevelWarn
|
||||
LevelError = slog.LevelError
|
||||
LevelCrit slog.Level = 12
|
||||
|
||||
// for backward-compatibility
|
||||
LvlTrace = LevelTrace
|
||||
LvlInfo = LevelInfo
|
||||
LvlDebug = LevelDebug
|
||||
)
|
||||
|
||||
// LvlFromString returns the appropriate Lvl from a string name.
|
||||
// Useful for parsing command line args and configuration files.
|
||||
func LvlFromString(lvlString string) (slog.Level, error) {
|
||||
switch strings.ToLower(lvlString) {
|
||||
case "trace", "trce":
|
||||
return LevelTrace, nil
|
||||
case "debug", "dbug":
|
||||
return LevelDebug, nil
|
||||
case "info":
|
||||
return LevelInfo, nil
|
||||
case "warn":
|
||||
return LevelWarn, nil
|
||||
case "error", "eror":
|
||||
return LevelError, nil
|
||||
case "crit":
|
||||
return LevelCrit, nil
|
||||
default:
|
||||
return LvlDebug, fmt.Errorf("unknown level: %v", lvlString)
|
||||
}
|
||||
}
|
||||
|
||||
// convert from old Geth verbosity level constants
|
||||
// to levels defined by slog
|
||||
func FromLegacyLevel(lvl int) slog.Level {
|
||||
switch lvl {
|
||||
case legacyLevelCrit:
|
||||
return LevelCrit
|
||||
case legacyLevelError:
|
||||
return slog.LevelError
|
||||
case legacyLevelWarn:
|
||||
return slog.LevelWarn
|
||||
case legacyLevelInfo:
|
||||
return slog.LevelInfo
|
||||
case legacyLevelDebug:
|
||||
return slog.LevelDebug
|
||||
case legacyLevelTrace:
|
||||
return LevelTrace
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
// TODO: should we allow use of custom levels or force them to match existing max/min if they fall outside the range as I am doing here?
|
||||
if lvl > legacyLevelTrace {
|
||||
return LevelTrace
|
||||
}
|
||||
return LevelCrit
|
||||
}
|
||||
|
||||
// LevelAlignedString returns a 5-character string containing the name of a Lvl.
|
||||
func LevelAlignedString(l slog.Level) string {
|
||||
switch l {
|
||||
case LevelTrace:
|
||||
return "TRACE"
|
||||
case slog.LevelDebug:
|
||||
return "DEBUG"
|
||||
case slog.LevelInfo:
|
||||
return "INFO"
|
||||
case slog.LevelWarn:
|
||||
return "WARN"
|
||||
case slog.LevelError:
|
||||
return "ERROR"
|
||||
case LevelCrit:
|
||||
return "CRIT"
|
||||
default:
|
||||
return "unknown level"
|
||||
}
|
||||
}
|
||||
|
||||
// LevelString returns a string containing the name of a Lvl.
|
||||
func LevelString(l slog.Level) string {
|
||||
switch l {
|
||||
case LevelTrace:
|
||||
return "trace"
|
||||
case slog.LevelDebug:
|
||||
return "debug"
|
||||
case slog.LevelInfo:
|
||||
return "info"
|
||||
case slog.LevelWarn:
|
||||
return "warn"
|
||||
case slog.LevelError:
|
||||
return "error"
|
||||
case LevelCrit:
|
||||
return "crit"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// A Logger writes key/value pairs to a Handler
|
||||
type Logger interface {
|
||||
// With returns a new Logger that has this logger's attributes plus the given attributes
|
||||
With(ctx ...interface{}) Logger
|
||||
|
||||
// With returns a new Logger that has this logger's attributes plus the given attributes. Identical to 'With'.
|
||||
New(ctx ...interface{}) Logger
|
||||
|
||||
// Log logs a message at the specified level with context key/value pairs
|
||||
Log(level slog.Level, msg string, ctx ...interface{})
|
||||
|
||||
// Trace log a message at the trace level with context key/value pairs
|
||||
Trace(msg string, ctx ...interface{})
|
||||
|
||||
// Debug logs a message at the debug level with context key/value pairs
|
||||
Debug(msg string, ctx ...interface{})
|
||||
|
||||
// Info logs a message at the info level with context key/value pairs
|
||||
Info(msg string, ctx ...interface{})
|
||||
|
||||
// Warn logs a message at the warn level with context key/value pairs
|
||||
Warn(msg string, ctx ...interface{})
|
||||
|
||||
// Error logs a message at the error level with context key/value pairs
|
||||
Error(msg string, ctx ...interface{})
|
||||
|
||||
// Crit logs a message at the crit level with context key/value pairs, and exits
|
||||
Crit(msg string, ctx ...interface{})
|
||||
|
||||
// Write logs a message at the specified level
|
||||
Write(level slog.Level, msg string, attrs ...any)
|
||||
|
||||
// Enabled reports whether l emits log records at the given context and level.
|
||||
Enabled(ctx context.Context, level slog.Level) bool
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
inner *slog.Logger
|
||||
}
|
||||
|
||||
// NewLogger returns a logger with the specified handler set
|
||||
func NewLogger(h slog.Handler) Logger {
|
||||
return &logger{
|
||||
slog.New(h),
|
||||
}
|
||||
}
|
||||
|
||||
// write logs a message at the specified level:
|
||||
func (l *logger) Write(level slog.Level, msg string, attrs ...any) {
|
||||
if !l.inner.Enabled(context.Background(), level) {
|
||||
return
|
||||
}
|
||||
|
||||
var pcs [1]uintptr
|
||||
runtime.Callers(3, pcs[:])
|
||||
|
||||
if len(attrs)%2 != 0 {
|
||||
attrs = append(attrs, nil, errorKey, "Normalized odd number of arguments by adding nil")
|
||||
}
|
||||
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
|
||||
r.Add(attrs...)
|
||||
l.inner.Handler().Handle(context.Background(), r)
|
||||
}
|
||||
|
||||
func (l *logger) Log(level slog.Level, msg string, attrs ...any) {
|
||||
l.Write(level, msg, attrs...)
|
||||
}
|
||||
|
||||
func (l *logger) With(ctx ...interface{}) Logger {
|
||||
return &logger{l.inner.With(ctx...)}
|
||||
}
|
||||
|
||||
func (l *logger) New(ctx ...interface{}) Logger {
|
||||
return l.With(ctx...)
|
||||
}
|
||||
|
||||
// Enabled reports whether l emits log records at the given context and level.
|
||||
func (l *logger) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
return l.inner.Enabled(ctx, level)
|
||||
}
|
||||
|
||||
func (l *logger) Trace(msg string, ctx ...interface{}) {
|
||||
l.Write(LevelTrace, msg, ctx...)
|
||||
}
|
||||
|
||||
func (l *logger) Debug(msg string, ctx ...interface{}) {
|
||||
l.Write(slog.LevelDebug, msg, ctx...)
|
||||
}
|
||||
|
||||
func (l *logger) Info(msg string, ctx ...interface{}) {
|
||||
l.Write(slog.LevelInfo, msg, ctx...)
|
||||
}
|
||||
|
||||
func (l *logger) Warn(msg string, ctx ...any) {
|
||||
l.Write(slog.LevelWarn, msg, ctx...)
|
||||
}
|
||||
|
||||
func (l *logger) Error(msg string, ctx ...interface{}) {
|
||||
l.Write(slog.LevelError, msg, ctx...)
|
||||
}
|
||||
|
||||
func (l *logger) Crit(msg string, ctx ...interface{}) {
|
||||
l.Write(LevelCrit, msg, ctx...)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package paramstest
|
||||
|
||||
import (
|
||||
"github.com/ava-labs/avalanchego/upgrade/upgradetest"
|
||||
|
||||
"github.com/ava-labs/subnet-evm/params"
|
||||
)
|
||||
|
||||
// ForkToChainConfig maps a fork to a chain config
|
||||
var ForkToChainConfig = map[upgradetest.Fork]*params.ChainConfig{
|
||||
upgradetest.ApricotPhase5: params.TestPreSubnetEVMChainConfig,
|
||||
upgradetest.ApricotPhase6: params.TestSubnetEVMChainConfig,
|
||||
upgradetest.Durango: params.TestDurangoChainConfig,
|
||||
upgradetest.Etna: params.TestEtnaChainConfig,
|
||||
upgradetest.Fortuna: params.TestFortunaChainConfig,
|
||||
upgradetest.Granite: params.TestGraniteChainConfig,
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/node/database/pebbledb"
|
||||
"github.com/luxfi/geth/common"
|
||||
)
|
||||
|
||||
const defaultCommitInterval = 4096
|
||||
|
||||
func NewDefaultConfig() Config {
|
||||
return Config{
|
||||
AllowUnprotectedTxHashes: []common.Hash{
|
||||
common.HexToHash("0xfefb2da535e927b85fe68eb81cb2e4a5827c905f78381a01ef2322aa9b0aee8e"), // EIP-1820: https://eips.ethereum.org/EIPS/eip-1820
|
||||
},
|
||||
EnabledEthAPIs: []string{
|
||||
"eth",
|
||||
"eth-filter",
|
||||
"net",
|
||||
"web3",
|
||||
"internal-eth",
|
||||
"internal-blockchain",
|
||||
"internal-transaction",
|
||||
},
|
||||
// Provides 2 minutes of buffer (2s block target) for a commit delay
|
||||
AcceptorQueueLimit: 64,
|
||||
Pruning: true,
|
||||
CommitInterval: defaultCommitInterval,
|
||||
TrieCleanCache: 512,
|
||||
TrieDirtyCache: 512,
|
||||
TrieDirtyCommitTarget: 20,
|
||||
TriePrefetcherParallelism: 16,
|
||||
SnapshotCache: 256,
|
||||
StateSyncCommitInterval: defaultCommitInterval * 4,
|
||||
SnapshotWait: false,
|
||||
RPCGasCap: 50_000_000, // 50M Gas Limit
|
||||
RPCTxFeeCap: 100, // 100 AVAX
|
||||
MetricsExpensiveEnabled: true,
|
||||
// Default to no maximum API call duration
|
||||
APIMaxDuration: timeToDuration(0),
|
||||
// Default to no maximum WS CPU usage
|
||||
WSCPURefillRate: timeToDuration(0),
|
||||
// Default to no maximum WS CPU usage
|
||||
WSCPUMaxStored: timeToDuration(0),
|
||||
// Default to no maximum on the number of blocks per getLogs request
|
||||
MaxBlocksPerRequest: 0,
|
||||
ContinuousProfilerFrequency: timeToDuration(15 * time.Minute),
|
||||
ContinuousProfilerMaxFiles: 5,
|
||||
PushGossipPercentStake: .9,
|
||||
PushGossipNumValidators: 100,
|
||||
PushGossipNumPeers: 0,
|
||||
PushRegossipNumValidators: 10,
|
||||
PushRegossipNumPeers: 0,
|
||||
PushGossipFrequency: timeToDuration(100 * time.Millisecond),
|
||||
PullGossipFrequency: timeToDuration(1 * time.Second),
|
||||
RegossipFrequency: timeToDuration(30 * time.Second),
|
||||
// Default size (MB) for the offline pruner to use
|
||||
OfflinePruningBloomFilterSize: uint64(512),
|
||||
LogLevel: "info",
|
||||
LogJSONFormat: false,
|
||||
MaxOutboundActiveRequests: 16,
|
||||
PopulateMissingTriesParallelism: 1024,
|
||||
StateSyncServerTrieCache: 64, // MB
|
||||
AcceptedCacheSize: 32, // blocks
|
||||
// StateSyncMinBlocks is the minimum number of blocks the blockchain
|
||||
// should be ahead of local last accepted to perform state sync.
|
||||
// This constant is chosen so normal bootstrapping is preferred when it would
|
||||
// be faster than state sync.
|
||||
// time assumptions:
|
||||
// - normal bootstrap processing time: ~14 blocks / second
|
||||
// - state sync time: ~6 hrs.
|
||||
StateSyncMinBlocks: 300_000,
|
||||
// the number of key/values to ask peers for per request
|
||||
StateSyncRequestSize: 1024,
|
||||
StateHistory: uint64(32),
|
||||
// Estimated block count in 24 hours with 2s block accept period
|
||||
HistoricalProofQueryWindow: uint64(24 * time.Hour / (2 * time.Second)),
|
||||
// Mempool settings
|
||||
TxPoolPriceLimit: 1,
|
||||
TxPoolPriceBump: 10,
|
||||
TxPoolAccountSlots: 16,
|
||||
TxPoolGlobalSlots: 4096 + 1024, // urgent + floating queue capacity with 4:1 ratio,
|
||||
TxPoolAccountQueue: 64,
|
||||
TxPoolGlobalQueue: 1024,
|
||||
TxPoolLifetime: timeToDuration(10 * time.Minute),
|
||||
// RPC settings
|
||||
BatchRequestLimit: 1000,
|
||||
BatchResponseMaxSize: 25 * 1000 * 1000, // 25MB
|
||||
// Subnet EVM API settings
|
||||
ValidatorsAPIEnabled: true,
|
||||
// Database settings
|
||||
DatabaseType: pebbledb.Name,
|
||||
// Additional settings with sensible defaults
|
||||
AllowUnprotectedTxs: false,
|
||||
}
|
||||
}
|
||||
|
||||
func timeToDuration(t time.Duration) Duration {
|
||||
return Duration{t}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// TODO: move to network
|
||||
|
||||
package evm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
consensusCore "github.com/luxfi/node/consensus/engine/core"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/network/p2p"
|
||||
"github.com/luxfi/node/network/p2p/gossip"
|
||||
|
||||
"github.com/luxfi/evm/core"
|
||||
"github.com/luxfi/evm/core/txpool"
|
||||
"github.com/luxfi/evm/eth"
|
||||
"github.com/luxfi/evm/plugin/evm/config"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/geth/core/types"
|
||||
)
|
||||
|
||||
const pendingTxsBuffer = 10
|
||||
|
||||
var (
|
||||
_ gossip.Gossipable = (*GossipEthTx)(nil)
|
||||
_ gossip.Marshaller[*GossipEthTx] = (*GossipEthTxMarshaller)(nil)
|
||||
_ gossip.Set[*GossipEthTx] = (*GossipEthTxPool)(nil)
|
||||
|
||||
_ eth.PushGossiper = (*EthPushGossiper)(nil)
|
||||
)
|
||||
|
||||
<<<<<<< HEAD:plugin/evm/gossip.go
|
||||
func newTxGossipHandler[T gossip.Gossipable](
|
||||
log log.Logger,
|
||||
marshaller gossip.Marshaller[T],
|
||||
mempool gossip.Set[T],
|
||||
metrics gossip.Metrics,
|
||||
maxMessageSize int,
|
||||
throttlingPeriod time.Duration,
|
||||
throttlingLimit int,
|
||||
validators p2p.ValidatorSet,
|
||||
) txGossipHandler {
|
||||
// push gossip messages can be handled from any peer
|
||||
handler := gossip.NewHandler(
|
||||
log,
|
||||
marshaller,
|
||||
mempool,
|
||||
metrics,
|
||||
maxMessageSize,
|
||||
)
|
||||
|
||||
// pull gossip requests are filtered by validators and are throttled
|
||||
// to prevent spamming
|
||||
validatorHandler := p2p.NewValidatorHandler(
|
||||
p2p.NewThrottlerHandler(
|
||||
handler,
|
||||
p2p.NewSlidingWindowThrottler(throttlingPeriod, throttlingLimit),
|
||||
log,
|
||||
),
|
||||
validators,
|
||||
log,
|
||||
)
|
||||
|
||||
return txGossipHandler{
|
||||
appGossipHandler: handler,
|
||||
appRequestHandler: validatorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
type txGossipHandler struct {
|
||||
appGossipHandler p2p.Handler
|
||||
appRequestHandler p2p.Handler
|
||||
}
|
||||
|
||||
func (t txGossipHandler) AppGossip(ctx context.Context, nodeID ids.NodeID, gossipBytes []byte) {
|
||||
t.appGossipHandler.AppGossip(ctx, nodeID, gossipBytes)
|
||||
}
|
||||
|
||||
func (t txGossipHandler) AppRequest(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *consensusCore.AppError) {
|
||||
return t.appRequestHandler.AppRequest(ctx, nodeID, deadline, requestBytes)
|
||||
}
|
||||
|
||||
func (t txGossipHandler) CrossChainAppRequest(ctx context.Context, chainID ids.ID, deadline time.Time, requestBytes []byte) ([]byte, error) {
|
||||
// Cross-chain requests not supported for tx gossip
|
||||
return nil, fmt.Errorf("cross-chain requests not supported")
|
||||
}
|
||||
|
||||
func NewGossipEthTxPool(mempool *txpool.TxPool, registerer prometheus.Registerer) (*GossipEthTxPool, error) {
|
||||
bloom, err := gossip.NewBloomFilter(registerer, "eth_tx_bloom_filter", config.TxGossipBloomMinTargetElements, config.TxGossipBloomTargetFalsePositiveRate, config.TxGossipBloomResetFalsePositiveRate)
|
||||
=======
|
||||
func NewGossipEthTxPool(mempool *txpool.TxPool, registerer prometheus.Registerer) (*GossipEthTxPool, error) {
|
||||
bloom, err := gossip.NewBloomFilter(
|
||||
registerer,
|
||||
"eth_tx_bloom_filter",
|
||||
config.TxGossipBloomMinTargetElements,
|
||||
config.TxGossipBloomTargetFalsePositiveRate,
|
||||
config.TxGossipBloomResetFalsePositiveRate,
|
||||
)
|
||||
>>>>>>> upstream/master:plugin/evm/eth_gossiper.go
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize bloom filter: %w", err)
|
||||
}
|
||||
|
||||
return &GossipEthTxPool{
|
||||
mempool: mempool,
|
||||
pendingTxs: make(chan core.NewTxsEvent, pendingTxsBuffer),
|
||||
bloom: bloom,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type GossipEthTxPool struct {
|
||||
mempool *txpool.TxPool
|
||||
pendingTxs chan core.NewTxsEvent
|
||||
|
||||
bloom *gossip.BloomFilter
|
||||
lock sync.RWMutex
|
||||
|
||||
// subscribed is set to true when the gossip subscription is active
|
||||
// mostly used for testing
|
||||
subscribed atomic.Bool
|
||||
}
|
||||
|
||||
// IsSubscribed returns whether or not the gossip subscription is active.
|
||||
func (g *GossipEthTxPool) IsSubscribed() bool {
|
||||
return g.subscribed.Load()
|
||||
}
|
||||
|
||||
func (g *GossipEthTxPool) Subscribe(ctx context.Context) {
|
||||
sub := g.mempool.SubscribeTransactions(g.pendingTxs, false)
|
||||
if sub == nil {
|
||||
log.Warn("failed to subscribe to new txs event")
|
||||
return
|
||||
}
|
||||
g.subscribed.CompareAndSwap(false, true)
|
||||
defer func() {
|
||||
sub.Unsubscribe()
|
||||
g.subscribed.CompareAndSwap(true, false)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debug("shutting down subscription")
|
||||
return
|
||||
case pendingTxs := <-g.pendingTxs:
|
||||
g.lock.Lock()
|
||||
optimalElements := (g.mempool.PendingSize(txpool.PendingFilter{}) + len(pendingTxs.Txs)) * config.TxGossipBloomChurnMultiplier
|
||||
for _, pendingTx := range pendingTxs.Txs {
|
||||
tx := &GossipEthTx{Tx: pendingTx}
|
||||
g.bloom.Add(tx)
|
||||
reset, err := gossip.ResetBloomFilterIfNeeded(g.bloom, optimalElements)
|
||||
if err != nil {
|
||||
log.Error("failed to reset bloom filter", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if reset {
|
||||
log.Debug("resetting bloom filter", "reason", "reached max filled ratio")
|
||||
|
||||
g.mempool.IteratePending(func(tx *types.Transaction) bool {
|
||||
g.bloom.Add(&GossipEthTx{Tx: tx})
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
g.lock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add enqueues the transaction to the mempool. Subscribe should be called
|
||||
// to receive an event if tx is actually added to the mempool or not.
|
||||
func (g *GossipEthTxPool) Add(tx *GossipEthTx) error {
|
||||
return g.mempool.Add([]*types.Transaction{tx.Tx}, false, false)[0]
|
||||
}
|
||||
|
||||
// Has should just return whether or not the [txID] is still in the mempool,
|
||||
// not whether it is in the mempool AND pending.
|
||||
func (g *GossipEthTxPool) Has(txID ids.ID) bool {
|
||||
return g.mempool.Has(common.Hash(txID))
|
||||
}
|
||||
|
||||
func (g *GossipEthTxPool) Iterate(f func(tx *GossipEthTx) bool) {
|
||||
g.mempool.IteratePending(func(tx *types.Transaction) bool {
|
||||
return f(&GossipEthTx{Tx: tx})
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GossipEthTxPool) GetFilter() ([]byte, []byte) {
|
||||
g.lock.RLock()
|
||||
defer g.lock.RUnlock()
|
||||
|
||||
return g.bloom.Marshal()
|
||||
}
|
||||
|
||||
type GossipEthTxMarshaller struct{}
|
||||
|
||||
func (GossipEthTxMarshaller) MarshalGossip(tx *GossipEthTx) ([]byte, error) {
|
||||
return tx.Tx.MarshalBinary()
|
||||
}
|
||||
|
||||
func (GossipEthTxMarshaller) UnmarshalGossip(bytes []byte) (*GossipEthTx, error) {
|
||||
tx := &GossipEthTx{
|
||||
Tx: &types.Transaction{},
|
||||
}
|
||||
|
||||
return tx, tx.Tx.UnmarshalBinary(bytes)
|
||||
}
|
||||
|
||||
type GossipEthTx struct {
|
||||
Tx *types.Transaction
|
||||
}
|
||||
|
||||
func (tx *GossipEthTx) GossipID() ids.ID {
|
||||
return ids.ID(tx.Tx.Hash())
|
||||
}
|
||||
|
||||
// EthPushGossiper is used by the ETH backend to push transactions issued over
|
||||
// the RPC and added to the mempool to peers.
|
||||
type EthPushGossiper struct {
|
||||
vm *VM
|
||||
}
|
||||
|
||||
func (e *EthPushGossiper) Add(tx *types.Transaction) {
|
||||
// eth.Backend is initialized before the [ethTxPushGossiper] is created, so
|
||||
// we just ignore any gossip requests until it is set.
|
||||
ethTxPushGossiper := e.vm.ethTxPushGossiper.Get()
|
||||
if ethTxPushGossiper == nil {
|
||||
return
|
||||
}
|
||||
ethTxPushGossiper.Add(&GossipEthTx{tx})
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package gossip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ava-labs/avalanchego/ids"
|
||||
"github.com/ava-labs/avalanchego/network/p2p"
|
||||
"github.com/ava-labs/avalanchego/network/p2p/gossip"
|
||||
"github.com/ava-labs/avalanchego/snow/engine/common"
|
||||
"github.com/ava-labs/avalanchego/utils/logging"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var _ p2p.Handler = (*txGossipHandler)(nil)
|
||||
|
||||
func NewTxGossipHandler[T gossip.Gossipable](
|
||||
log logging.Logger,
|
||||
marshaller gossip.Marshaller[T],
|
||||
mempool gossip.Set[T],
|
||||
metrics gossip.Metrics,
|
||||
maxMessageSize int,
|
||||
throttlingPeriod time.Duration,
|
||||
requestsPerPeer float64,
|
||||
validators p2p.ValidatorSet,
|
||||
registerer prometheus.Registerer,
|
||||
namespace string,
|
||||
) (*txGossipHandler, error) {
|
||||
// push gossip messages can be handled from any peer
|
||||
handler := gossip.NewHandler(
|
||||
log,
|
||||
marshaller,
|
||||
mempool,
|
||||
metrics,
|
||||
maxMessageSize,
|
||||
)
|
||||
|
||||
throttledHandler, err := p2p.NewDynamicThrottlerHandler(
|
||||
log,
|
||||
handler,
|
||||
validators,
|
||||
throttlingPeriod,
|
||||
requestsPerPeer,
|
||||
registerer,
|
||||
namespace,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize throttler handler: %w", err)
|
||||
}
|
||||
|
||||
// pull gossip requests are filtered by validators and are throttled
|
||||
// to prevent spamming
|
||||
validatorHandler := p2p.NewValidatorHandler(
|
||||
throttledHandler,
|
||||
validators,
|
||||
log,
|
||||
)
|
||||
|
||||
return &txGossipHandler{
|
||||
appGossipHandler: handler,
|
||||
appRequestHandler: validatorHandler,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type txGossipHandler struct {
|
||||
appGossipHandler p2p.Handler
|
||||
appRequestHandler p2p.Handler
|
||||
}
|
||||
|
||||
func (t *txGossipHandler) AppGossip(ctx context.Context, nodeID ids.NodeID, gossipBytes []byte) {
|
||||
t.appGossipHandler.AppGossip(ctx, nodeID, gossipBytes)
|
||||
}
|
||||
|
||||
func (t *txGossipHandler) AppRequest(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *common.AppError) {
|
||||
return t.appRequestHandler.AppRequest(ctx, nodeID, deadline, requestBytes)
|
||||
}
|
||||
@@ -0,0 +1,690 @@
|
||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package warp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/ava-labs/avalanchego/ids"
|
||||
"github.com/ava-labs/avalanchego/snow/validators"
|
||||
"github.com/ava-labs/avalanchego/snow/validators/validatorsmock"
|
||||
"github.com/ava-labs/avalanchego/utils/constants"
|
||||
"github.com/ava-labs/avalanchego/utils/crypto/bls"
|
||||
"github.com/ava-labs/avalanchego/utils/set"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp"
|
||||
)
|
||||
|
||||
type signatureTest struct {
|
||||
name string
|
||||
stateF func(*gomock.Controller) validators.State
|
||||
quorumNum uint64
|
||||
quorumDen uint64
|
||||
msgF func(*require.Assertions) *avalancheWarp.Message
|
||||
verifyErr error
|
||||
canonicalErr error
|
||||
}
|
||||
|
||||
// This test copies the test coverage from https://github.com/ava-labs/avalanchego/blob/0117ab96/vms/platformvm/warp/signature_test.go#L137.
|
||||
// These tests are only expected to fail if there is a breaking change in AvalancheGo that unexpectedly changes behavior.
|
||||
func TestSignatureVerification(t *testing.T) {
|
||||
tests := []signatureTest{
|
||||
{
|
||||
name: "can't get subnetID",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, errTest)
|
||||
return state
|
||||
},
|
||||
quorumNum: 1,
|
||||
quorumDen: 2,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
canonicalErr: errTest,
|
||||
},
|
||||
{
|
||||
name: "can't get validator set",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(nil, errTest)
|
||||
return state
|
||||
},
|
||||
quorumNum: 1,
|
||||
quorumDen: 2,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
canonicalErr: errTest,
|
||||
},
|
||||
{
|
||||
name: "weight overflow",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(map[ids.NodeID]*validators.GetValidatorOutput{
|
||||
testVdrs[0].nodeID: {
|
||||
NodeID: testVdrs[0].nodeID,
|
||||
PublicKey: testVdrs[0].vdr.PublicKey,
|
||||
Weight: math.MaxUint64,
|
||||
},
|
||||
testVdrs[1].nodeID: {
|
||||
NodeID: testVdrs[1].nodeID,
|
||||
PublicKey: testVdrs[1].vdr.PublicKey,
|
||||
Weight: math.MaxUint64,
|
||||
},
|
||||
}, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 1,
|
||||
quorumDen: 2,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: make([]byte, 8),
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
canonicalErr: avalancheWarp.ErrWeightOverflow,
|
||||
},
|
||||
{
|
||||
name: "invalid bit set index",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 1,
|
||||
quorumDen: 2,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: make([]byte, 1),
|
||||
Signature: [bls.SignatureLen]byte{},
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
verifyErr: avalancheWarp.ErrInvalidBitSet,
|
||||
},
|
||||
{
|
||||
name: "unknown index",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 1,
|
||||
quorumDen: 2,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
signers := set.NewBits()
|
||||
signers.Add(3) // vdr oob
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: signers.Bytes(),
|
||||
Signature: [bls.SignatureLen]byte{},
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
verifyErr: avalancheWarp.ErrUnknownValidator,
|
||||
},
|
||||
{
|
||||
name: "insufficient weight",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 1,
|
||||
quorumDen: 1,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
// [signers] has weight from [vdr[0], vdr[1]],
|
||||
// which is 6, which is less than 9
|
||||
signers := set.NewBits()
|
||||
signers.Add(0)
|
||||
signers.Add(1)
|
||||
|
||||
unsignedBytes := unsignedMsg.Bytes()
|
||||
vdr0Sig, err := testVdrs[0].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
vdr1Sig, err := testVdrs[1].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
aggSig, err := bls.AggregateSignatures([]*bls.Signature{vdr0Sig, vdr1Sig})
|
||||
require.NoError(err)
|
||||
aggSigBytes := [bls.SignatureLen]byte{}
|
||||
copy(aggSigBytes[:], bls.SignatureToBytes(aggSig))
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: signers.Bytes(),
|
||||
Signature: aggSigBytes,
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
verifyErr: avalancheWarp.ErrInsufficientWeight,
|
||||
},
|
||||
{
|
||||
name: "can't parse sig",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 1,
|
||||
quorumDen: 2,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
signers := set.NewBits()
|
||||
signers.Add(0)
|
||||
signers.Add(1)
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: signers.Bytes(),
|
||||
Signature: [bls.SignatureLen]byte{},
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
verifyErr: avalancheWarp.ErrParseSignature,
|
||||
},
|
||||
{
|
||||
name: "no validators",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(nil, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 1,
|
||||
quorumDen: 2,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
unsignedBytes := unsignedMsg.Bytes()
|
||||
vdr0Sig, err := testVdrs[0].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
aggSigBytes := [bls.SignatureLen]byte{}
|
||||
copy(aggSigBytes[:], bls.SignatureToBytes(vdr0Sig))
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: nil,
|
||||
Signature: aggSigBytes,
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
verifyErr: bls.ErrNoPublicKeys,
|
||||
},
|
||||
{
|
||||
name: "invalid signature (substitute)",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 3,
|
||||
quorumDen: 5,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
signers := set.NewBits()
|
||||
signers.Add(0)
|
||||
signers.Add(1)
|
||||
|
||||
unsignedBytes := unsignedMsg.Bytes()
|
||||
vdr0Sig, err := testVdrs[0].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
// Give sig from vdr[2] even though the bit vector says it
|
||||
// should be from vdr[1]
|
||||
vdr2Sig, err := testVdrs[2].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
aggSig, err := bls.AggregateSignatures([]*bls.Signature{vdr0Sig, vdr2Sig})
|
||||
require.NoError(err)
|
||||
aggSigBytes := [bls.SignatureLen]byte{}
|
||||
copy(aggSigBytes[:], bls.SignatureToBytes(aggSig))
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: signers.Bytes(),
|
||||
Signature: aggSigBytes,
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
verifyErr: avalancheWarp.ErrInvalidSignature,
|
||||
},
|
||||
{
|
||||
name: "invalid signature (missing one)",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 3,
|
||||
quorumDen: 5,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
signers := set.NewBits()
|
||||
signers.Add(0)
|
||||
signers.Add(1)
|
||||
|
||||
unsignedBytes := unsignedMsg.Bytes()
|
||||
vdr0Sig, err := testVdrs[0].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
// Don't give the sig from vdr[1]
|
||||
aggSigBytes := [bls.SignatureLen]byte{}
|
||||
copy(aggSigBytes[:], bls.SignatureToBytes(vdr0Sig))
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: signers.Bytes(),
|
||||
Signature: aggSigBytes,
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
verifyErr: avalancheWarp.ErrInvalidSignature,
|
||||
},
|
||||
{
|
||||
name: "invalid signature (extra one)",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 3,
|
||||
quorumDen: 5,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
signers := set.NewBits()
|
||||
signers.Add(0)
|
||||
signers.Add(1)
|
||||
|
||||
unsignedBytes := unsignedMsg.Bytes()
|
||||
vdr0Sig, err := testVdrs[0].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
vdr1Sig, err := testVdrs[1].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
// Give sig from vdr[2] even though the bit vector doesn't have
|
||||
// it
|
||||
vdr2Sig, err := testVdrs[2].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
aggSig, err := bls.AggregateSignatures([]*bls.Signature{vdr0Sig, vdr1Sig, vdr2Sig})
|
||||
require.NoError(err)
|
||||
aggSigBytes := [bls.SignatureLen]byte{}
|
||||
copy(aggSigBytes[:], bls.SignatureToBytes(aggSig))
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: signers.Bytes(),
|
||||
Signature: aggSigBytes,
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
verifyErr: avalancheWarp.ErrInvalidSignature,
|
||||
},
|
||||
{
|
||||
name: "valid signature",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 1,
|
||||
quorumDen: 2,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
// [signers] has weight from [vdr[1], vdr[2]],
|
||||
// which is 6, which is greater than 4.5
|
||||
signers := set.NewBits()
|
||||
signers.Add(1)
|
||||
signers.Add(2)
|
||||
|
||||
unsignedBytes := unsignedMsg.Bytes()
|
||||
vdr1Sig, err := testVdrs[1].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
vdr2Sig, err := testVdrs[2].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
aggSig, err := bls.AggregateSignatures([]*bls.Signature{vdr1Sig, vdr2Sig})
|
||||
require.NoError(err)
|
||||
aggSigBytes := [bls.SignatureLen]byte{}
|
||||
copy(aggSigBytes[:], bls.SignatureToBytes(aggSig))
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: signers.Bytes(),
|
||||
Signature: aggSigBytes,
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
verifyErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid signature (boundary)",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(vdrs, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 2,
|
||||
quorumDen: 3,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
// [signers] has weight from [vdr[1], vdr[2]],
|
||||
// which is 6, which meets the minimum 6
|
||||
signers := set.NewBits()
|
||||
signers.Add(1)
|
||||
signers.Add(2)
|
||||
|
||||
unsignedBytes := unsignedMsg.Bytes()
|
||||
vdr1Sig, err := testVdrs[1].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
vdr2Sig, err := testVdrs[2].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
aggSig, err := bls.AggregateSignatures([]*bls.Signature{vdr1Sig, vdr2Sig})
|
||||
require.NoError(err)
|
||||
aggSigBytes := [bls.SignatureLen]byte{}
|
||||
copy(aggSigBytes[:], bls.SignatureToBytes(aggSig))
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: signers.Bytes(),
|
||||
Signature: aggSigBytes,
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
verifyErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid signature (missing key)",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(map[ids.NodeID]*validators.GetValidatorOutput{
|
||||
testVdrs[0].nodeID: {
|
||||
NodeID: testVdrs[0].nodeID,
|
||||
PublicKey: nil,
|
||||
Weight: testVdrs[0].vdr.Weight,
|
||||
},
|
||||
testVdrs[1].nodeID: {
|
||||
NodeID: testVdrs[1].nodeID,
|
||||
PublicKey: testVdrs[1].vdr.PublicKey,
|
||||
Weight: testVdrs[1].vdr.Weight,
|
||||
},
|
||||
testVdrs[2].nodeID: {
|
||||
NodeID: testVdrs[2].nodeID,
|
||||
PublicKey: testVdrs[2].vdr.PublicKey,
|
||||
Weight: testVdrs[2].vdr.Weight,
|
||||
},
|
||||
}, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 1,
|
||||
quorumDen: 3,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
// [signers] has weight from [vdr2, vdr3],
|
||||
// which is 6, which is greater than 3
|
||||
signers := set.NewBits()
|
||||
// Note: the bits are shifted because vdr[0]'s key was zeroed
|
||||
signers.Add(0) // vdr[1]
|
||||
signers.Add(1) // vdr[2]
|
||||
|
||||
unsignedBytes := unsignedMsg.Bytes()
|
||||
vdr1Sig, err := testVdrs[1].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
vdr2Sig, err := testVdrs[2].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
aggSig, err := bls.AggregateSignatures([]*bls.Signature{vdr1Sig, vdr2Sig})
|
||||
require.NoError(err)
|
||||
aggSigBytes := [bls.SignatureLen]byte{}
|
||||
copy(aggSigBytes[:], bls.SignatureToBytes(aggSig))
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: signers.Bytes(),
|
||||
Signature: aggSigBytes,
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
verifyErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid signature (duplicate key)",
|
||||
stateF: func(ctrl *gomock.Controller) validators.State {
|
||||
state := validatorsmock.NewState(ctrl)
|
||||
state.EXPECT().GetSubnetID(gomock.Any(), sourceChainID).Return(sourceSubnetID, nil)
|
||||
state.EXPECT().GetValidatorSet(gomock.Any(), pChainHeight, sourceSubnetID).Return(map[ids.NodeID]*validators.GetValidatorOutput{
|
||||
testVdrs[0].nodeID: {
|
||||
NodeID: testVdrs[0].nodeID,
|
||||
PublicKey: nil,
|
||||
Weight: testVdrs[0].vdr.Weight,
|
||||
},
|
||||
testVdrs[1].nodeID: {
|
||||
NodeID: testVdrs[1].nodeID,
|
||||
PublicKey: testVdrs[2].vdr.PublicKey,
|
||||
Weight: testVdrs[1].vdr.Weight,
|
||||
},
|
||||
testVdrs[2].nodeID: {
|
||||
NodeID: testVdrs[2].nodeID,
|
||||
PublicKey: testVdrs[2].vdr.PublicKey,
|
||||
Weight: testVdrs[2].vdr.Weight,
|
||||
},
|
||||
}, nil)
|
||||
return state
|
||||
},
|
||||
quorumNum: 2,
|
||||
quorumDen: 3,
|
||||
msgF: func(require *require.Assertions) *avalancheWarp.Message {
|
||||
unsignedMsg, err := avalancheWarp.NewUnsignedMessage(
|
||||
constants.UnitTestID,
|
||||
sourceChainID,
|
||||
addressedPayloadBytes,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
// [signers] has weight from [vdr2, vdr3],
|
||||
// which is 6, which meets the minimum 6
|
||||
signers := set.NewBits()
|
||||
// Note: the bits are shifted because vdr[0]'s key was zeroed
|
||||
// Note: vdr[1] and vdr[2] were combined because of a shared pk
|
||||
signers.Add(0) // vdr[1] + vdr[2]
|
||||
|
||||
unsignedBytes := unsignedMsg.Bytes()
|
||||
// Because vdr[1] and vdr[2] share a key, only one of them sign.
|
||||
vdr2Sig, err := testVdrs[2].sk.Sign(unsignedBytes)
|
||||
require.NoError(err)
|
||||
aggSigBytes := [bls.SignatureLen]byte{}
|
||||
copy(aggSigBytes[:], bls.SignatureToBytes(vdr2Sig))
|
||||
|
||||
msg, err := avalancheWarp.NewMessage(
|
||||
unsignedMsg,
|
||||
&avalancheWarp.BitSetSignature{
|
||||
Signers: signers.Bytes(),
|
||||
Signature: aggSigBytes,
|
||||
},
|
||||
)
|
||||
require.NoError(err)
|
||||
return msg
|
||||
},
|
||||
verifyErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
msg := tt.msgF(require)
|
||||
pChainState := tt.stateF(ctrl)
|
||||
|
||||
validatorSet, err := avalancheWarp.GetCanonicalValidatorSetFromChainID(
|
||||
context.Background(),
|
||||
pChainState,
|
||||
pChainHeight,
|
||||
msg.UnsignedMessage.SourceChainID,
|
||||
)
|
||||
require.ErrorIs(err, tt.canonicalErr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = msg.Signature.Verify(
|
||||
&msg.UnsignedMessage,
|
||||
constants.UnitTestID,
|
||||
validatorSet,
|
||||
tt.quorumNum,
|
||||
tt.quorumDen,
|
||||
)
|
||||
require.ErrorIs(err, tt.verifyErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/geth/core/tracing"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/luxfi/evm/core/extstate"
|
||||
"github.com/luxfi/evm/precompile/contract"
|
||||
)
|
||||
|
||||
// StateDBAdapter wraps *extstate.StateDB to implement contract.StateDB
|
||||
type StateDBAdapter struct {
|
||||
*extstate.StateDB
|
||||
}
|
||||
|
||||
// AddBalance adapts the AddBalance method to match the contract.StateDB interface
|
||||
func (s *StateDBAdapter) AddBalance(address common.Address, amount *uint256.Int) {
|
||||
// Call the underlying StateDB.AddBalance with a default BalanceChangeReason
|
||||
s.StateDB.AddBalance(address, amount, tracing.BalanceChangeUnspecified)
|
||||
}
|
||||
|
||||
// SetNonce implements the contract.StateDB interface
|
||||
func (s *StateDBAdapter) SetNonce(address common.Address, nonce uint64) {
|
||||
s.StateDB.SetNonce(address, nonce, tracing.NonceChangeUnspecified)
|
||||
}
|
||||
|
||||
// SetState implements the contract.StateDB interface
|
||||
func (s *StateDBAdapter) SetState(address common.Address, key, value common.Hash) {
|
||||
// The underlying SetState returns the old value, but contract.StateDB expects no return
|
||||
_ = s.StateDB.SetState(address, key, value)
|
||||
}
|
||||
|
||||
// GetPredicateStorageSlots implements the contract.StateDB interface
|
||||
func (s *StateDBAdapter) GetPredicateStorageSlots(address common.Address, index int) ([]byte, bool) {
|
||||
return s.StateDB.GetPredicateStorageSlots(address, index)
|
||||
}
|
||||
|
||||
// WrapStateDB wraps an *extstate.StateDB to implement contract.StateDB
|
||||
func WrapStateDB(state *extstate.StateDB) contract.StateDB {
|
||||
return &StateDBAdapter{StateDB: state}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package firewood
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ava-labs/libevm/common"
|
||||
"github.com/ava-labs/libevm/core/types"
|
||||
"github.com/ava-labs/libevm/crypto"
|
||||
"github.com/ava-labs/libevm/ethdb"
|
||||
"github.com/ava-labs/libevm/log"
|
||||
"github.com/ava-labs/libevm/rlp"
|
||||
"github.com/ava-labs/libevm/trie"
|
||||
"github.com/ava-labs/libevm/trie/trienode"
|
||||
"github.com/ava-labs/libevm/triedb/database"
|
||||
)
|
||||
|
||||
// AccountTrie implements state.Trie for managing account states.
|
||||
// There are a couple caveats to the current implementation:
|
||||
// 1. `Commit` is not used as expected in the state package. The `StorageTrie` doesn't return
|
||||
// values, and we thus rely on the `AccountTrie`.
|
||||
// 2. The `Hash` method actually creates the proposal, since Firewood cannot calculate
|
||||
// the hash of the trie without committing it. It is immediately dropped, and this
|
||||
// can likely be optimized.
|
||||
type AccountTrie struct {
|
||||
fw *Database
|
||||
parentRoot common.Hash
|
||||
root common.Hash
|
||||
reader database.Reader
|
||||
dirtyKeys map[string][]byte // Store dirty changes
|
||||
updateKeys [][]byte
|
||||
updateValues [][]byte
|
||||
hasChanges bool
|
||||
}
|
||||
|
||||
func NewAccountTrie(root common.Hash, db *Database) (*AccountTrie, error) {
|
||||
reader, err := db.Reader(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &AccountTrie{
|
||||
fw: db,
|
||||
parentRoot: root,
|
||||
reader: reader,
|
||||
dirtyKeys: make(map[string][]byte),
|
||||
hasChanges: true, // Start with hasChanges true to allow computing the proposal hash
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAccount implements state.Trie.
|
||||
func (a *AccountTrie) GetAccount(addr common.Address) (*types.StateAccount, error) {
|
||||
key := crypto.Keccak256Hash(addr.Bytes()).Bytes()
|
||||
|
||||
// First check if there's a pending update for this account
|
||||
keyStr := string(key)
|
||||
if updateValue, exists := a.dirtyKeys[keyStr]; exists {
|
||||
// If the value is empty, it indicates deletion
|
||||
// Invariant: All encoded values have length > 0
|
||||
if len(updateValue) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// Decode and return the updated account
|
||||
account := new(types.StateAccount)
|
||||
err := rlp.DecodeBytes(updateValue, account)
|
||||
return account, err
|
||||
}
|
||||
|
||||
// No pending update found, read from the underlying reader
|
||||
accountBytes, err := a.reader.Node(common.Hash{}, key, common.Hash{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if accountBytes == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Decode the account node
|
||||
account := new(types.StateAccount)
|
||||
err = rlp.DecodeBytes(accountBytes, account)
|
||||
return account, err
|
||||
}
|
||||
|
||||
// GetStorage implements state.Trie.
|
||||
func (a *AccountTrie) GetStorage(addr common.Address, key []byte) ([]byte, error) {
|
||||
// If the account has been deleted, we should return nil
|
||||
accountKey := crypto.Keccak256Hash(addr.Bytes()).Bytes()
|
||||
if val, exists := a.dirtyKeys[string(accountKey)]; exists && len(val) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var combinedKey [2 * common.HashLength]byte
|
||||
storageKey := crypto.Keccak256Hash(key).Bytes()
|
||||
copy(combinedKey[:common.HashLength], accountKey)
|
||||
copy(combinedKey[common.HashLength:], storageKey)
|
||||
|
||||
// Check if there's a pending update for this storage slot
|
||||
keyStr := string(combinedKey[:])
|
||||
if updateValue, exists := a.dirtyKeys[keyStr]; exists {
|
||||
// If the value is empty, it indicates deletion
|
||||
if len(updateValue) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// Decode and return the updated storage value
|
||||
_, decoded, _, err := rlp.Split(updateValue)
|
||||
return decoded, err
|
||||
}
|
||||
|
||||
// No pending update found, read from the underlying reader
|
||||
storageBytes, err := a.reader.Node(common.Hash{}, combinedKey[:], common.Hash{})
|
||||
if err != nil || storageBytes == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode the storage value
|
||||
_, decoded, _, err := rlp.Split(storageBytes)
|
||||
return decoded, err
|
||||
}
|
||||
|
||||
// UpdateAccount implements state.Trie.
|
||||
func (a *AccountTrie) UpdateAccount(addr common.Address, account *types.StateAccount) error {
|
||||
// Queue the keys and values for later commit
|
||||
key := crypto.Keccak256Hash(addr.Bytes()).Bytes()
|
||||
data, err := rlp.EncodeToBytes(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.dirtyKeys[string(key)] = data
|
||||
a.updateKeys = append(a.updateKeys, key)
|
||||
a.updateValues = append(a.updateValues, data)
|
||||
a.hasChanges = true // Mark that there are changes to commit
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStorage implements state.Trie.
|
||||
func (a *AccountTrie) UpdateStorage(addr common.Address, key []byte, value []byte) error {
|
||||
var combinedKey [2 * common.HashLength]byte
|
||||
accountKey := crypto.Keccak256Hash(addr.Bytes()).Bytes()
|
||||
storageKey := crypto.Keccak256Hash(key).Bytes()
|
||||
copy(combinedKey[:common.HashLength], accountKey)
|
||||
copy(combinedKey[common.HashLength:], storageKey)
|
||||
|
||||
data, err := rlp.EncodeToBytes(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Queue the keys and values for later commit
|
||||
a.dirtyKeys[string(combinedKey[:])] = data
|
||||
a.updateKeys = append(a.updateKeys, combinedKey[:])
|
||||
a.updateValues = append(a.updateValues, data)
|
||||
a.hasChanges = true // Mark that there are changes to commit
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAccount implements state.Trie.
|
||||
func (a *AccountTrie) DeleteAccount(addr common.Address) error {
|
||||
key := crypto.Keccak256Hash(addr.Bytes()).Bytes()
|
||||
// Queue the key for deletion
|
||||
a.dirtyKeys[string(key)] = []byte{}
|
||||
a.updateKeys = append(a.updateKeys, key)
|
||||
a.updateValues = append(a.updateValues, []byte{}) // Empty value indicates deletion
|
||||
a.hasChanges = true // Mark that there are changes to commit
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteStorage implements state.Trie.
|
||||
func (a *AccountTrie) DeleteStorage(addr common.Address, key []byte) error {
|
||||
var combinedKey [2 * common.HashLength]byte
|
||||
accountKey := crypto.Keccak256Hash(addr.Bytes()).Bytes()
|
||||
storageKey := crypto.Keccak256Hash(key).Bytes()
|
||||
copy(combinedKey[:common.HashLength], accountKey)
|
||||
copy(combinedKey[common.HashLength:], storageKey)
|
||||
|
||||
// Queue the key for deletion
|
||||
a.dirtyKeys[string(combinedKey[:])] = []byte{}
|
||||
a.updateKeys = append(a.updateKeys, combinedKey[:])
|
||||
a.updateValues = append(a.updateValues, []byte{}) // Empty value indicates deletion
|
||||
a.hasChanges = true // Mark that there are changes to commit
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hash implements state.Trie.
|
||||
func (a *AccountTrie) Hash() common.Hash {
|
||||
hash, err := a.hash()
|
||||
if err != nil {
|
||||
log.Error("Failed to hash account trie", "error", err)
|
||||
return common.Hash{}
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
func (a *AccountTrie) hash() (common.Hash, error) {
|
||||
// If we haven't already hashed, we need to do so.
|
||||
if a.hasChanges {
|
||||
root, err := a.fw.getProposalHash(a.parentRoot, a.updateKeys, a.updateValues)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
a.root = root
|
||||
a.hasChanges = false // Avoid re-hashing until next update
|
||||
}
|
||||
return a.root, nil
|
||||
}
|
||||
|
||||
// Commit implements state.Trie.
|
||||
func (a *AccountTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet, error) {
|
||||
// Get the hash of the trie.
|
||||
hash, err := a.hash()
|
||||
if err != nil {
|
||||
return common.Hash{}, nil, err
|
||||
}
|
||||
|
||||
// Create the NodeSet. This will be sent to `triedb.Update` later.
|
||||
nodeset := trienode.NewNodeSet(a.parentRoot)
|
||||
for i, key := range a.updateKeys {
|
||||
nodeset.AddNode(key, &trienode.Node{
|
||||
Blob: a.updateValues[i],
|
||||
})
|
||||
}
|
||||
|
||||
return hash, nodeset, nil
|
||||
}
|
||||
|
||||
// UpdateContractCode implements state.Trie.
|
||||
// Contract code is controlled by rawdb, so we don't need to do anything here.
|
||||
func (*AccountTrie) UpdateContractCode(_ common.Address, _ common.Hash, _ []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetKey implements state.Trie.
|
||||
func (*AccountTrie) GetKey(_ []byte) []byte {
|
||||
return nil // Not implemented, as this is only used in APIs
|
||||
}
|
||||
|
||||
// NodeIterator implements state.Trie.
|
||||
func (*AccountTrie) NodeIterator(_ []byte) (trie.NodeIterator, error) {
|
||||
return nil, errors.New("NodeIterator not implemented for Firewood")
|
||||
}
|
||||
|
||||
// Prove implements state.Trie.
|
||||
func (*AccountTrie) Prove(_ []byte, _ ethdb.KeyValueWriter) error {
|
||||
return errors.New("Prove not implemented for Firewood")
|
||||
}
|
||||
|
||||
func (a *AccountTrie) Copy() *AccountTrie {
|
||||
// Create a new AccountTrie with the same root and reader
|
||||
newTrie := &AccountTrie{
|
||||
fw: a.fw,
|
||||
parentRoot: a.parentRoot,
|
||||
root: a.root,
|
||||
reader: a.reader, // Share the same reader
|
||||
hasChanges: a.hasChanges,
|
||||
dirtyKeys: make(map[string][]byte, len(a.dirtyKeys)),
|
||||
updateKeys: make([][]byte, len(a.updateKeys)),
|
||||
updateValues: make([][]byte, len(a.updateValues)),
|
||||
}
|
||||
|
||||
// Deep copy dirtyKeys map
|
||||
for k, v := range a.dirtyKeys {
|
||||
newTrie.dirtyKeys[k] = append([]byte{}, v...)
|
||||
}
|
||||
|
||||
// Deep copy updateKeys and updateValues slices
|
||||
for i := range a.updateKeys {
|
||||
newTrie.updateKeys[i] = append([]byte{}, a.updateKeys[i]...)
|
||||
newTrie.updateValues[i] = append([]byte{}, a.updateValues[i]...)
|
||||
}
|
||||
|
||||
return newTrie
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package firewood
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ava-labs/firewood-go-ethhash/ffi"
|
||||
"github.com/ava-labs/libevm/common"
|
||||
"github.com/ava-labs/libevm/core/rawdb"
|
||||
"github.com/ava-labs/libevm/core/types"
|
||||
"github.com/ava-labs/libevm/ethdb"
|
||||
"github.com/ava-labs/libevm/libevm/stateconf"
|
||||
"github.com/ava-labs/libevm/log"
|
||||
"github.com/ava-labs/libevm/metrics"
|
||||
"github.com/ava-labs/libevm/trie/trienode"
|
||||
"github.com/ava-labs/libevm/trie/triestate"
|
||||
"github.com/ava-labs/libevm/triedb"
|
||||
"github.com/ava-labs/libevm/triedb/database"
|
||||
)
|
||||
|
||||
var (
|
||||
_ proposable = (*ffi.Database)(nil)
|
||||
_ proposable = (*ffi.Proposal)(nil)
|
||||
|
||||
// FFI triedb operation metrics
|
||||
ffiProposeCount = metrics.GetOrRegisterCounter("firewood/triedb/propose/count", nil)
|
||||
ffiProposeTimer = metrics.GetOrRegisterCounter("firewood/triedb/propose/time", nil)
|
||||
ffiCommitCount = metrics.GetOrRegisterCounter("firewood/triedb/commit/count", nil)
|
||||
ffiCommitTimer = metrics.GetOrRegisterCounter("firewood/triedb/commit/time", nil)
|
||||
ffiCleanupTimer = metrics.GetOrRegisterCounter("firewood/triedb/cleanup/time", nil)
|
||||
ffiOutstandingProposals = metrics.GetOrRegisterGauge("firewood/triedb/propose/outstanding", nil)
|
||||
|
||||
// FFI Trie operation metrics
|
||||
ffiHashCount = metrics.GetOrRegisterCounter("firewood/triedb/hash/count", nil)
|
||||
ffiHashTimer = metrics.GetOrRegisterCounter("firewood/triedb/hash/time", nil)
|
||||
ffiReadCount = metrics.GetOrRegisterCounter("firewood/triedb/read/count", nil)
|
||||
ffiReadTimer = metrics.GetOrRegisterCounter("firewood/triedb/read/time", nil)
|
||||
)
|
||||
|
||||
type proposable interface {
|
||||
// Propose creates a new proposal from the current state with the given keys and values.
|
||||
Propose(keys, values [][]byte) (*ffi.Proposal, error)
|
||||
}
|
||||
|
||||
// ProposalContext represents a proposal in the Firewood database.
|
||||
// This tracks all outstanding proposals to allow dereferencing upon commit.
|
||||
type ProposalContext struct {
|
||||
Proposal *ffi.Proposal
|
||||
Hashes map[common.Hash]struct{} // All corresponding block hashes
|
||||
Root common.Hash
|
||||
Block uint64
|
||||
Parent *ProposalContext
|
||||
Children []*ProposalContext
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
FilePath string
|
||||
CleanCacheSize int // Size of the clean cache in bytes
|
||||
FreeListCacheEntries uint // Number of free list entries to cache
|
||||
Revisions uint
|
||||
ReadCacheStrategy ffi.CacheStrategy
|
||||
}
|
||||
|
||||
// Note that `FilePath` is not specificied, and must always be set by the user.
|
||||
var Defaults = &Config{
|
||||
CleanCacheSize: 1024 * 1024, // 1MB
|
||||
FreeListCacheEntries: 40_000,
|
||||
Revisions: 100,
|
||||
ReadCacheStrategy: ffi.CacheAllReads,
|
||||
}
|
||||
|
||||
func (c Config) BackendConstructor(_ ethdb.Database) triedb.DBOverride {
|
||||
return New(&c)
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
fwDisk *ffi.Database // The underlying Firewood database, used for storing proposals and revisions.
|
||||
proposalLock sync.RWMutex
|
||||
// proposalMap provides O(1) access by state root to all proposals stored in the proposalTree
|
||||
proposalMap map[common.Hash][]*ProposalContext
|
||||
// The proposal tree tracks the structure of the current proposals, and which proposals are children of which.
|
||||
// This is used to ensure that we can dereference proposals correctly and commit the correct ones
|
||||
// in the case of duplicate state roots.
|
||||
// The root of the tree is stored here, and represents the top-most layer on disk.
|
||||
proposalTree *ProposalContext
|
||||
}
|
||||
|
||||
// New creates a new Firewood database with the given disk database and configuration.
|
||||
// Any error during creation will cause the program to exit.
|
||||
func New(config *Config) *Database {
|
||||
if config == nil {
|
||||
config = Defaults
|
||||
}
|
||||
|
||||
fwConfig, err := validatePath(config)
|
||||
if err != nil {
|
||||
log.Crit("firewood: error validating config", "error", err)
|
||||
}
|
||||
|
||||
fw, err := ffi.New(config.FilePath, fwConfig)
|
||||
if err != nil {
|
||||
log.Crit("firewood: error creating firewood database", "error", err)
|
||||
}
|
||||
|
||||
currentRoot, err := fw.Root()
|
||||
if err != nil {
|
||||
log.Crit("firewood: error getting current root", "error", err)
|
||||
}
|
||||
|
||||
return &Database{
|
||||
fwDisk: fw,
|
||||
proposalMap: make(map[common.Hash][]*ProposalContext),
|
||||
proposalTree: &ProposalContext{
|
||||
Root: common.Hash(currentRoot),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func validatePath(trieConfig *Config) (*ffi.Config, error) {
|
||||
if trieConfig.FilePath == "" {
|
||||
return nil, errors.New("firewood database file path must be set")
|
||||
}
|
||||
|
||||
// Check that the directory exists
|
||||
dir := filepath.Dir(trieConfig.FilePath)
|
||||
_, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("error checking database directory: %w", err)
|
||||
}
|
||||
log.Info("Database directory not found, creating", "path", dir)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("error creating database directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the Firewood config from the provided config.
|
||||
config := &ffi.Config{
|
||||
NodeCacheEntries: uint(trieConfig.CleanCacheSize) / 256, // TODO: estimate 256 bytes per node
|
||||
FreeListCacheEntries: trieConfig.FreeListCacheEntries,
|
||||
Revisions: trieConfig.Revisions,
|
||||
ReadCacheStrategy: trieConfig.ReadCacheStrategy,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Scheme returns the scheme of the database.
|
||||
// This is only used in some API calls
|
||||
// and in StateDB to avoid iterating through deleted storage tries.
|
||||
// WARNING: If cherry-picking anything from upstream that uses this,
|
||||
// it must be overwritten to use something like:
|
||||
// `_, ok := db.(*Database); if !ok { return "" }`
|
||||
// to recognize the Firewood database.
|
||||
func (*Database) Scheme() string {
|
||||
return rawdb.HashScheme
|
||||
}
|
||||
|
||||
// Initialized checks whether a non-empty genesis block has been written.
|
||||
func (db *Database) Initialized(_ common.Hash) bool {
|
||||
rootBytes, err := db.fwDisk.Root()
|
||||
if err != nil {
|
||||
log.Error("firewood: error getting current root", "error", err)
|
||||
return false
|
||||
}
|
||||
root := common.BytesToHash(rootBytes)
|
||||
// If the current root isn't empty, then unless the database is empty, we have a genesis block recorded.
|
||||
return root != types.EmptyRootHash
|
||||
}
|
||||
|
||||
// Update takes a root and a set of keys-values and creates a new proposal.
|
||||
// It will not be committed until the Commit method is called.
|
||||
// This function should be called even if there are no changes to the state to ensure proper tracking of block hashes.
|
||||
func (db *Database) Update(root common.Hash, parentRoot common.Hash, block uint64, nodes *trienode.MergedNodeSet, _ *triestate.Set, opts ...stateconf.TrieDBUpdateOption) error {
|
||||
// We require block hashes to be provided for all blocks in production.
|
||||
// However, many tests cannot reasonably provide a block hash for genesis, so we allow it to be omitted.
|
||||
parentHash, hash, ok := stateconf.ExtractTrieDBUpdatePayload(opts...)
|
||||
if !ok {
|
||||
log.Error("firewood: no block hash provided for block %d", block)
|
||||
}
|
||||
|
||||
// The rest of the operations except key-value arranging must occur with a lock
|
||||
db.proposalLock.Lock()
|
||||
defer db.proposalLock.Unlock()
|
||||
|
||||
// Check if this proposal already exists.
|
||||
// During reorgs, we may have already created this proposal.
|
||||
// Additionally, we may have already created this proposal with a different block hash.
|
||||
if existingProposals, ok := db.proposalMap[root]; ok {
|
||||
for _, existing := range existingProposals {
|
||||
// If the block hash is already tracked, we can skip proposing this again.
|
||||
if _, exists := existing.Hashes[hash]; exists {
|
||||
log.Debug("firewood: proposal already exists", "root", root.Hex(), "parent", parentRoot.Hex(), "block", block, "hash", hash.Hex())
|
||||
return nil
|
||||
}
|
||||
// We already have this proposal, but should create a new context with the correct hash.
|
||||
// This solves the case of a unique block hash, but the same underlying proposal.
|
||||
if _, exists := existing.Parent.Hashes[parentHash]; exists {
|
||||
log.Debug("firewood: proposal already exists, updating hash", "root", root.Hex(), "parent", parentRoot.Hex(), "block", block, "hash", hash.Hex())
|
||||
existing.Hashes[hash] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keys, values := arrangeKeyValuePairs(nodes) // may return nil, nil if no changes
|
||||
return db.propose(root, parentRoot, hash, parentHash, block, keys, values)
|
||||
}
|
||||
|
||||
// propose creates a new proposal for every possible parent with the given keys and values.
|
||||
// If the parent cannot be found, an error will be returned.
|
||||
//
|
||||
// To avoid having to create a new proposal for each valid state root, the block hashes are
|
||||
// provided to ensure uniqueness. When this method is called, we can guarantee that the proposalContext
|
||||
// must be created and tracked.
|
||||
//
|
||||
// Should only be accessed with the proposal lock held.
|
||||
func (db *Database) propose(root common.Hash, parentRoot common.Hash, hash common.Hash, parentHash common.Hash, block uint64, keys [][]byte, values [][]byte) error {
|
||||
// Find the parent proposal with the correct hash.
|
||||
// We assume the number of proposals at a given root is small, so we can iterate through them.
|
||||
for _, parentProposal := range db.proposalMap[parentRoot] {
|
||||
// If we know this proposal cannot be the parent, we can skip it.
|
||||
// Since the only possible block that won't have a parent hash is block 1,
|
||||
// and that will always be proposed from the database root,
|
||||
// we can guarantee that the parent hash will be present in one of the proposals.
|
||||
if _, exists := parentProposal.Hashes[parentHash]; !exists {
|
||||
continue
|
||||
}
|
||||
log.Debug("firewood: proposing from parent proposal", "parent", parentProposal.Root.Hex(), "root", root.Hex(), "height", block)
|
||||
p, err := createProposal(parentProposal.Proposal, root, keys, values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pCtx := &ProposalContext{
|
||||
Proposal: p,
|
||||
Hashes: map[common.Hash]struct{}{hash: {}},
|
||||
Root: root,
|
||||
Block: block,
|
||||
Parent: parentProposal,
|
||||
}
|
||||
|
||||
db.proposalMap[root] = append(db.proposalMap[root], pCtx)
|
||||
parentProposal.Children = append(parentProposal.Children, pCtx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Since we were unable to find a parent proposal with the given parent hash,
|
||||
// we must create a new proposal from the database root.
|
||||
// We must avoid the case in which we are reexecuting blocks upon startup, and haven't yet stored the parent block.
|
||||
if _, exists := db.proposalTree.Hashes[parentHash]; db.proposalTree.Block != 0 && !exists {
|
||||
return fmt.Errorf("firewood: parent hash %s not found for block %s at height %d", parentHash.Hex(), hash.Hex(), block)
|
||||
} else if db.proposalTree.Root != parentRoot {
|
||||
return fmt.Errorf("firewood: parent root %s does not match proposal tree root %s for root %s at height %d", parentRoot.Hex(), db.proposalTree.Root.Hex(), root.Hex(), block)
|
||||
}
|
||||
|
||||
log.Debug("firewood: proposing from database root", "root", root.Hex(), "height", block)
|
||||
p, err := createProposal(db.fwDisk, root, keys, values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pCtx := &ProposalContext{
|
||||
Proposal: p,
|
||||
Hashes: map[common.Hash]struct{}{hash: {}}, // This may be common.Hash{} for genesis blocks.
|
||||
Root: root,
|
||||
Block: block,
|
||||
Parent: db.proposalTree,
|
||||
}
|
||||
db.proposalMap[root] = append(db.proposalMap[root], pCtx)
|
||||
db.proposalTree.Children = append(db.proposalTree.Children, pCtx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Commit persists a proposal as a revision to the database.
|
||||
//
|
||||
// Any time this is called, we expect either:
|
||||
// 1. The root is the same as the current root of the database (empty block during bootstrapping)
|
||||
// 2. We have created a valid propsal with that root, and it is of height +1 above the proposal tree root.
|
||||
// Additionally, this should be unique.
|
||||
//
|
||||
// Afterward, we know that no other proposal at this height can be committed, so we can dereference all
|
||||
// children in the the other branches of the proposal tree.
|
||||
func (db *Database) Commit(root common.Hash, report bool) (err error) {
|
||||
// We need to lock the proposal tree to prevent concurrent writes.
|
||||
var pCtx *ProposalContext
|
||||
db.proposalLock.Lock()
|
||||
defer db.proposalLock.Unlock()
|
||||
|
||||
// On success, we should persist the genesis root as necessary, and dereference all children
|
||||
// of the committed proposal.
|
||||
defer func() {
|
||||
// If we attempted to commit a proposal, but it failed, we must dereference its children.
|
||||
if pCtx != nil {
|
||||
db.cleanupCommittedProposal(pCtx)
|
||||
}
|
||||
}()
|
||||
|
||||
// Find the proposal with the given root.
|
||||
for _, possible := range db.proposalMap[root] {
|
||||
if possible.Parent.Root == db.proposalTree.Root && possible.Parent.Block == db.proposalTree.Block {
|
||||
// We found the proposal with the correct parent.
|
||||
if pCtx != nil {
|
||||
// This should never happen, as we ensure that we don't create duplicate proposals in `propose`.
|
||||
return fmt.Errorf("firewood: multiple proposals found for %s", root.Hex())
|
||||
}
|
||||
pCtx = possible
|
||||
}
|
||||
}
|
||||
if pCtx == nil {
|
||||
return fmt.Errorf("firewood: committable proposal not found for %s", root.Hex())
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
// Commit the proposal to the database.
|
||||
if commitErr := pCtx.Proposal.Commit(); commitErr != nil {
|
||||
return fmt.Errorf("firewood: error committing proposal %s: %w", root.Hex(), commitErr)
|
||||
}
|
||||
ffiCommitCount.Inc(1)
|
||||
ffiCommitTimer.Inc(time.Since(start).Milliseconds())
|
||||
ffiOutstandingProposals.Dec(1)
|
||||
|
||||
// Assert that the root of the database matches the committed proposal root.
|
||||
currentRootBytes, err := db.fwDisk.Root()
|
||||
if err != nil {
|
||||
return fmt.Errorf("firewood: error getting current root after commit: %w", err)
|
||||
}
|
||||
currentRoot := common.BytesToHash(currentRootBytes)
|
||||
if currentRoot != root {
|
||||
return fmt.Errorf("firewood: current root %s does not match expected root %s", currentRoot.Hex(), root.Hex())
|
||||
}
|
||||
|
||||
if report {
|
||||
log.Info("Persisted proposal to firewood database", "root", root)
|
||||
} else {
|
||||
log.Debug("Persisted proposal to firewood database", "root", root)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Size returns the storage size of diff layer nodes above the persistent disk
|
||||
// layer and the dirty nodes buffered within the disk layer
|
||||
// Only used for metrics and Commit intervals in APIs.
|
||||
// This will be implemented in the firewood database eventually.
|
||||
// Currently, Firewood stores all revisions in disk and proposals in memory.
|
||||
func (*Database) Size() (common.StorageSize, common.StorageSize) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// This isn't called anywhere in subnet-evm
|
||||
func (*Database) Reference(common.Hash, common.Hash) {
|
||||
log.Error("firewood: Reference not implemented")
|
||||
}
|
||||
|
||||
// Dereference drops a proposal from the database.
|
||||
// This function is no-op because unused proposals are dereferenced when no longer valid.
|
||||
// We cannot dereference at this call. Consider the following case:
|
||||
// Chain 1 has root A and root C
|
||||
// Chain 2 has root B and root C
|
||||
// We commit root A, and immediately dereference root B and its child.
|
||||
// Root C is Rejected, (which is intended to be 2C) but there's now only one record of root C in the proposal map.
|
||||
// Thus, we recognize the single root C as the only proposal, and dereference it.
|
||||
func (*Database) Dereference(common.Hash) {
|
||||
}
|
||||
|
||||
// Firewood does not support this.
|
||||
func (*Database) Cap(common.StorageSize) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) Close() error {
|
||||
db.proposalLock.Lock()
|
||||
defer db.proposalLock.Unlock()
|
||||
|
||||
// We don't need to explicitly dereference the proposals, since they will be cleaned up
|
||||
// within the firewood close method.
|
||||
db.proposalMap = nil
|
||||
db.proposalTree.Children = nil
|
||||
// Close the database
|
||||
return db.fwDisk.Close()
|
||||
}
|
||||
|
||||
// createProposal creates a new proposal from the given layer
|
||||
// If there are no changes, it will return nil.
|
||||
func createProposal(layer proposable, root common.Hash, keys, values [][]byte) (p *ffi.Proposal, err error) {
|
||||
// If there's an error after creating the proposal, we must drop it.
|
||||
defer func() {
|
||||
if err != nil && p != nil {
|
||||
if dropErr := p.Drop(); dropErr != nil {
|
||||
// We should still return the original error.
|
||||
log.Error("firewood: error dropping proposal after error", "root", root.Hex(), "error", dropErr)
|
||||
}
|
||||
p = nil
|
||||
}
|
||||
}()
|
||||
|
||||
if len(keys) != len(values) {
|
||||
return nil, fmt.Errorf("firewood: keys and values must have the same length, got %d keys and %d values", len(keys), len(values))
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
p, err = layer.Propose(keys, values)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("firewood: unable to create proposal for root %s: %w", root.Hex(), err)
|
||||
}
|
||||
ffiProposeCount.Inc(1)
|
||||
ffiProposeTimer.Inc(time.Since(start).Milliseconds())
|
||||
ffiOutstandingProposals.Inc(1)
|
||||
|
||||
currentRootBytes, err := p.Root()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("firewood: error getting root of proposal %s: %w", root, err)
|
||||
}
|
||||
currentRoot := common.BytesToHash(currentRootBytes)
|
||||
if root != currentRoot {
|
||||
return nil, fmt.Errorf("firewood: proposed root %s does not match expected root %s", currentRoot.Hex(), root.Hex())
|
||||
}
|
||||
|
||||
// Store the proposal context.
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// cleanupCommittedProposal dereferences the proposal and removes it from the proposal map.
|
||||
// It also recursively dereferences all children of the proposal.
|
||||
func (db *Database) cleanupCommittedProposal(pCtx *ProposalContext) {
|
||||
start := time.Now()
|
||||
oldChildren := db.proposalTree.Children
|
||||
db.proposalTree = pCtx
|
||||
db.proposalTree.Parent = nil
|
||||
|
||||
db.removeProposalFromMap(pCtx)
|
||||
|
||||
for _, childCtx := range oldChildren {
|
||||
// Don't dereference the recently commit proposal.
|
||||
if childCtx != pCtx {
|
||||
db.dereference(childCtx)
|
||||
}
|
||||
}
|
||||
ffiCleanupTimer.Inc(time.Since(start).Milliseconds())
|
||||
}
|
||||
|
||||
// Internally removes all references of the proposal from the database.
|
||||
// Should only be accessed with the proposal lock held.
|
||||
// Consumer must not be iterating the proposal map at this root.
|
||||
func (db *Database) dereference(pCtx *ProposalContext) {
|
||||
// Base case: if there are children, we need to dereference them as well.
|
||||
for _, child := range pCtx.Children {
|
||||
db.dereference(child)
|
||||
}
|
||||
pCtx.Children = nil
|
||||
|
||||
// Remove the proposal from the map.
|
||||
db.removeProposalFromMap(pCtx)
|
||||
|
||||
// Drop the proposal in the backend.
|
||||
if err := pCtx.Proposal.Drop(); err != nil {
|
||||
log.Error("firewood: error dropping proposal", "root", pCtx.Root.Hex(), "error", err)
|
||||
}
|
||||
ffiOutstandingProposals.Dec(1)
|
||||
}
|
||||
|
||||
// removeProposalFromMap removes the proposal from the proposal map.
|
||||
// The proposal lock must be held when calling this function.
|
||||
func (db *Database) removeProposalFromMap(pCtx *ProposalContext) {
|
||||
rootList := db.proposalMap[pCtx.Root]
|
||||
for i, p := range rootList {
|
||||
if p == pCtx { // pointer comparison - guaranteed to be unique
|
||||
rootList[i] = rootList[len(rootList)-1]
|
||||
rootList[len(rootList)-1] = nil
|
||||
rootList = rootList[:len(rootList)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(rootList) == 0 {
|
||||
delete(db.proposalMap, pCtx.Root)
|
||||
} else {
|
||||
db.proposalMap[pCtx.Root] = rootList
|
||||
}
|
||||
}
|
||||
|
||||
// Reader retrieves a node reader belonging to the given state root.
|
||||
// An error will be returned if the requested state is not available.
|
||||
func (db *Database) Reader(root common.Hash) (database.Reader, error) {
|
||||
if _, err := db.fwDisk.GetFromRoot(root.Bytes(), []byte{}); err != nil {
|
||||
return nil, fmt.Errorf("firewood: unable to retrieve from root %s: %w", root.Hex(), err)
|
||||
}
|
||||
return &reader{db: db, root: root}, nil
|
||||
}
|
||||
|
||||
// reader is a state reader of Database which implements the Reader interface.
|
||||
type reader struct {
|
||||
db *Database
|
||||
root common.Hash // The root of the state this reader is reading.
|
||||
}
|
||||
|
||||
// Node retrieves the trie node with the given node hash. No error will be
|
||||
// returned if the node is not found.
|
||||
func (reader *reader) Node(_ common.Hash, path []byte, _ common.Hash) ([]byte, error) {
|
||||
// This function relies on Firewood's internal locking to ensure concurrent reads are safe.
|
||||
// This is safe even if a proposal is being committed concurrently.
|
||||
start := time.Now()
|
||||
result, err := reader.db.fwDisk.GetFromRoot(reader.root.Bytes(), path)
|
||||
if metrics.EnabledExpensive {
|
||||
ffiReadCount.Inc(1)
|
||||
ffiReadTimer.Inc(time.Since(start).Milliseconds())
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// getProposalHash calculates the hash if the set of keys and values are
|
||||
// proposed from the given parent root.
|
||||
func (db *Database) getProposalHash(parentRoot common.Hash, keys, values [][]byte) (common.Hash, error) {
|
||||
// This function only reads from existing tracked proposals, so we can use a read lock.
|
||||
db.proposalLock.RLock()
|
||||
defer db.proposalLock.RUnlock()
|
||||
|
||||
var (
|
||||
p *ffi.Proposal
|
||||
err error
|
||||
)
|
||||
start := time.Now()
|
||||
if db.proposalTree.Root == parentRoot {
|
||||
// Propose from the database root.
|
||||
p, err = db.fwDisk.Propose(keys, values)
|
||||
if err != nil {
|
||||
return common.Hash{}, fmt.Errorf("firewood: error proposing from root %s: %w", parentRoot.Hex(), err)
|
||||
}
|
||||
} else {
|
||||
// Find any proposal with the given parent root.
|
||||
// Since we are only using the proposal to find the root hash,
|
||||
// we can use the first proposal found.
|
||||
proposals, ok := db.proposalMap[parentRoot]
|
||||
if !ok || len(proposals) == 0 {
|
||||
return common.Hash{}, fmt.Errorf("firewood: no proposal found for parent root %s", parentRoot.Hex())
|
||||
}
|
||||
rootProposal := proposals[0].Proposal
|
||||
|
||||
p, err = rootProposal.Propose(keys, values)
|
||||
if err != nil {
|
||||
return common.Hash{}, fmt.Errorf("firewood: error proposing from parent proposal %s: %w", parentRoot.Hex(), err)
|
||||
}
|
||||
}
|
||||
ffiHashCount.Inc(1)
|
||||
ffiHashTimer.Inc(time.Since(start).Milliseconds())
|
||||
|
||||
// We succesffuly created a proposal, so we must drop it after use.
|
||||
defer p.Drop()
|
||||
|
||||
rootBytes, err := p.Root()
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
return common.BytesToHash(rootBytes), nil
|
||||
}
|
||||
|
||||
func arrangeKeyValuePairs(nodes *trienode.MergedNodeSet) ([][]byte, [][]byte) {
|
||||
if nodes == nil {
|
||||
return nil, nil // No changes to propose
|
||||
}
|
||||
// Create key-value pairs for the nodes in bytes.
|
||||
var (
|
||||
acctKeys [][]byte
|
||||
acctValues [][]byte
|
||||
storageKeys [][]byte
|
||||
storageValues [][]byte
|
||||
)
|
||||
|
||||
flattenedNodes := nodes.Flatten()
|
||||
|
||||
for _, nodeset := range flattenedNodes {
|
||||
for str, node := range nodeset {
|
||||
if len(str) == common.HashLength {
|
||||
// This is an account node.
|
||||
acctKeys = append(acctKeys, []byte(str))
|
||||
acctValues = append(acctValues, node.Blob)
|
||||
} else {
|
||||
storageKeys = append(storageKeys, []byte(str))
|
||||
storageValues = append(storageValues, node.Blob)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to do all storage operations first, so prefix-deletion works for accounts.
|
||||
keys := append(storageKeys, acctKeys...)
|
||||
values := append(storageValues, acctValues...)
|
||||
return keys, values
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package firewood
|
||||
|
||||
import (
|
||||
"github.com/ava-labs/libevm/common"
|
||||
"github.com/ava-labs/libevm/trie/trienode"
|
||||
)
|
||||
|
||||
type StorageTrie struct {
|
||||
*AccountTrie
|
||||
storageRoot common.Hash
|
||||
}
|
||||
|
||||
// `NewStorageTrie` returns a wrapper around an `AccountTrie` since Firewood
|
||||
// does not require a separate storage trie. All changes are managed by the account trie.
|
||||
func NewStorageTrie(accountTrie *AccountTrie, storageRoot common.Hash) (*StorageTrie, error) {
|
||||
return &StorageTrie{
|
||||
AccountTrie: accountTrie,
|
||||
storageRoot: storageRoot,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Actual commit is handled by the account trie.
|
||||
// Return the old storage root as if there was no change - we don't want to use the
|
||||
// actual account trie hash and nodeset here.
|
||||
func (s *StorageTrie) Commit(bool) (common.Hash, *trienode.NodeSet, error) {
|
||||
return s.storageRoot, nil, nil
|
||||
}
|
||||
|
||||
// Firewood doesn't require tracking storage roots inside of an account.
|
||||
func (s *StorageTrie) Hash() common.Hash {
|
||||
return s.storageRoot // only used in statedb to populate a `StateAccount`
|
||||
}
|
||||
|
||||
// Copy should never be called on a storage trie, as it is just a wrapper around the account trie.
|
||||
// Each storage trie should be re-opened with the account trie separately.
|
||||
func (*StorageTrie) Copy() *StorageTrie {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user