Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d138092
feat: enforce blacklist in txpool validation
code0xff Nov 11, 2025
9f6d0ae
feat: enforce blacklist in EVM Call and Create
code0xff Nov 12, 2025
66def00
feat: enforce blacklist in opSelfdestruct and opSelfdestruct6780
code0xff Nov 12, 2025
20ee28a
feat: enforce blacklist checks for block signer
code0xff Nov 13, 2025
9f635c1
fix: fix minor typos
code0xff Nov 13, 2025
652d573
refactor: use %s format for hex addresses
code0xff Nov 13, 2025
3424c38
feat: add AccountManagerAddress to AccessList in StateDB Prepare
code0xff Nov 13, 2025
bdbb68a
chore: clean up duplicate copyright line
code0xff Nov 13, 2025
d323ca4
test: add transaction validation tests for blacklisted accounts
code0xff Nov 14, 2025
380fb62
test: add EVM validation tests for blacklisted accounts
code0xff Nov 14, 2025
d5a16ba
test: simplify account setup by applying blacklist after creation
code0xff Nov 14, 2025
1aa0a00
test: add selfdestruct validation tests for blacklisted accounts
code0xff Nov 14, 2025
8b128a0
feat: enforce blacklisted fee payer validation in StateTransition
code0xff Nov 14, 2025
f15c1f9
refactor: simplify StateAt error handling in signer verification
code0xff Nov 17, 2025
2a64056
test: add signer validation tests for blacklisted accounts
code0xff Nov 17, 2025
9ce0700
test: simplify blacklisted signer test
code0xff Nov 17, 2025
421dc60
refactor: use core ErrBlacklistedAccount in txpool
code0xff Nov 17, 2025
646031a
fix: handle missing parent state in verifySigner by skipping blacklis…
code0xff Nov 17, 2025
6edc12d
fix: add native managers to access list
eomti-wm Nov 18, 2025
3e08c26
refactor: switch to structured ErrBlacklistedAccount and update tests
code0xff Nov 18, 2025
7b3f795
test: assign initial balance to test accounts in EVM tests
code0xff Nov 18, 2025
81c7765
test: add block number to EVM BlockContext
code0xff Nov 18, 2025
dcef9de
test: check correct error message
eomti-wm Nov 18, 2025
a0b69c1
feat: enforce blacklist checks in DelegateCall, CallCode, and StaticCall
code0xff Nov 18, 2025
ab25bc5
test: add blacklist enforcement tests for DelegateCall, CallCode, and…
code0xff Nov 18, 2025
ea30e10
test: fix typos
code0xff Nov 18, 2025
e4e5c94
refactor: remove role field from ErrBlacklistedAccount
code0xff Nov 18, 2025
52458b7
refactor: provide parent header directly to verifySigner
code0xff Nov 19, 2025
2abcb5d
refactor: remove redundant parent nil check
code0xff Nov 19, 2025
8c01bf8
test: remove missing parent case from TestBlacklistedSigner
code0xff Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions consensus/wbft/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,8 @@ func (sb *Backend) CheckSignature(data []byte, address common.Address, sig []byt
return nil
}

// HasPropsal implements wbft.Backend.HashBlock
func (sb *Backend) HasPropsal(hash common.Hash, number *big.Int) bool {
// HasProposal implements wbft.Backend.HashBlock
func (sb *Backend) HasProposal(hash common.Hash, number *big.Int) bool {
return sb.chain.GetHeader(hash, number.Uint64()) != nil
}

Expand Down
5 changes: 5 additions & 0 deletions consensus/wbft/common/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,9 @@ var (
ErrIsNotWBFTBlock = errors.New("block is not a wbft block")

ErrEpochInfoIsNotNil = errors.New("epoch info should be nil for non-epoch block")

ErrStateUnavailable = errors.New("state unavailable for verification")

// ErrBlacklistedSigner is returned when a block is signed by a blacklisted account.
ErrBlacklistedSigner = errors.New("blacklisted signer")
)
4 changes: 2 additions & 2 deletions consensus/wbft/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ type Backend interface {
// LastProposal retrieves latest committed proposal and the address of proposer
LastProposal() (wbft.Proposal, common.Address)

// HasPropsal checks if the combination of the given hash and height matches any existing blocks
HasPropsal(hash common.Hash, number *big.Int) bool
// HasProposal checks if the combination of the given hash and height matches any existing blocks
HasProposal(hash common.Hash, number *big.Int) bool

// GetProposer returns the proposer of the given block height
GetProposer(number uint64) common.Address
Expand Down
28 changes: 24 additions & 4 deletions consensus/wbft/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,13 @@ func (e *Engine) verifyCascadingFields(chain consensus.ChainHeaderReader, header
}

// Verify signer
if err := e.verifySigner(chain, header, parents, validators); err != nil {
return err
if err := e.verifySigner(chain, header, parent, validators); err != nil {
if !errors.Is(err, wbftcommon.ErrStateUnavailable) {
return err
}
// Skip blacklisted signer verification when state is not yet available.
// For more details, refer to the comment in verifyGasTip.
log.Trace("WBFT: Skipping blacklisted signer verification due to unavailable state", "number", header.Number, "err", err)
}

// extract the extra data from the header
Expand Down Expand Up @@ -343,7 +348,7 @@ func (e *Engine) verifyCascadingFields(chain consensus.ChainHeaderReader, header
return nil
}

func (e *Engine) verifySigner(chain consensus.ChainHeaderReader, header *types.Header, parents []*types.Header, validators wbft.ValidatorSet) error {
func (e *Engine) verifySigner(chain consensus.ChainHeaderReader, header *types.Header, parent *types.Header, validators wbft.ValidatorSet) error {
// Verifying the genesis block is not supported
number := header.Number.Uint64()
if number == 0 {
Expand All @@ -361,6 +366,16 @@ func (e *Engine) verifySigner(chain consensus.ChainHeaderReader, header *types.H
return wbftcommon.ErrUnauthorized
}

// The caller ensures that parent is non-nil.
// It is already validated in the calling context.
state, err := chain.StateAt(parent.Root)
if err != nil {
return fmt.Errorf("%w: %v", wbftcommon.ErrStateUnavailable, err)
}
if state.IsBlacklisted(signer) {
return fmt.Errorf("%w: %s", wbftcommon.ErrBlacklistedSigner, signer.Hex())
}

return nil
}

Expand Down Expand Up @@ -455,7 +470,12 @@ func (e *Engine) VerifySeal(chain consensus.ChainHeaderReader, header *types.Hea
return wbftcommon.ErrInvalidDifficulty
}

return e.verifySigner(chain, header, nil, validators)
parent := chain.GetHeader(header.ParentHash, number-1)
if parent == nil {
return consensus.ErrUnknownAncestor
}

return e.verifySigner(chain, header, parent, validators)
}

func (e *Engine) PeriodToNextBlock(blockNumber *big.Int) uint64 {
Expand Down
77 changes: 76 additions & 1 deletion consensus/wbft/engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/wbft"
wbftcommon "github.com/ethereum/go-ethereum/consensus/wbft/common"
wbftcore "github.com/ethereum/go-ethereum/consensus/wbft/core"
"github.com/ethereum/go-ethereum/consensus/wbft/validator"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
Expand All @@ -45,6 +46,7 @@ import (
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/systemcontracts"
"github.com/stretchr/testify/require"
)

// RLP data can be generated by using TestGeneratingGenesisExtra in testutils package
Expand Down Expand Up @@ -442,6 +444,7 @@ func TestWriteRandao(t *testing.T) {
type fakeChain struct {
chainConfig *params.ChainConfig
headers []*types.Header
statedb *state.StateDB
}

var _ consensus.ChainHeaderReader = (*fakeChain)(nil)
Expand All @@ -456,7 +459,7 @@ func (f *fakeChain) GetHeader(hash common.Hash, number uint64) *types.Header {
func (f *fakeChain) GetHeaderByNumber(number uint64) *types.Header { return nil }
func (f *fakeChain) GetHeaderByHash(hash common.Hash) *types.Header { return nil }
func (f *fakeChain) GetTd(hash common.Hash, number uint64) *big.Int { return nil }
func (f *fakeChain) StateAt(root common.Hash) (*state.StateDB, error) { return nil, nil }
func (f *fakeChain) StateAt(root common.Hash) (*state.StateDB, error) { return f.statedb, nil }

func (f *fakeChain) insertHeader(h *types.Header) {
f.headers = append(f.headers, h)
Expand Down Expand Up @@ -882,3 +885,75 @@ func TestComputeShuffledIndex(t *testing.T) {
t.Errorf("expected results to be equal, but got different results: %v and %v", result1, result2)
}
}

func TestBlacklistedSigner(t *testing.T) {
signer := testAccount1

tests := []struct {
name string
blacklisted bool
expectErr error
errContainsPart string
}{
{
name: "not blacklisted signer",
blacklisted: false,
expectErr: nil,
},
{
name: "blacklisted signer",
blacklisted: true,
expectErr: wbftcommon.ErrBlacklistedSigner,
errContainsPart: signer.addr.Hex(),
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
parent := &types.Header{
Number: big.NewInt(1),
Root: common.Hash{},
}

mockState, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
mockState.CreateAccount(signer.addr)
if tc.blacklisted {
mockState.SetBlacklisted(signer.addr)
}

fc := &fakeChain{
chainConfig: params.TestWBFTChainConfig,
headers: []*types.Header{nil, parent},
statedb: mockState,
}

engine := NewEngine(nil, common.Address{}, nil, nil)

header := &types.Header{
Number: big.NewInt(2),
ParentHash: common.HexToHash("0x1"),
Coinbase: signer.addr,
}

addrs := []common.Address{
signer.addr,
}
blsPubKeys := [][]byte{
signer.blsKey.PublicKey().Marshal(),
}
validators := validator.NewSet(addrs, blsPubKeys, wbft.NewRoundRobinProposerPolicy())

err := engine.verifySigner(fc, header, parent, validators)

if tc.expectErr == nil {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, tc.expectErr)
if tc.errContainsPart != "" {
require.Contains(t, err.Error(), tc.errContainsPart)
}
}
})
}
}
13 changes: 10 additions & 3 deletions core/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package core

import (
"errors"
"fmt"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)

Expand Down Expand Up @@ -101,9 +103,6 @@ var (
// ErrSenderNoEOA is returned if the sender of a transaction is a contract.
ErrSenderNoEOA = errors.New("sender not an eoa")

// ErrBlacklistedAccount is returned if the sender or recipient is blacklisted.
ErrBlacklistedAccount = errors.New("account is blacklisted")

// ErrBlobFeeCapTooLow is returned if the transaction fee cap is less than the
// blob gas fee of the block.
ErrBlobFeeCapTooLow = errors.New("max fee per blob gas less than block blob gas fee")
Expand All @@ -114,3 +113,11 @@ var (
// ErrBlobTxCreate is returned if a blob transaction has no explicit to field.
ErrBlobTxCreate = errors.New("blob transaction of type create")
)

type ErrBlacklistedAccount struct {
Address common.Address
}

func (e *ErrBlacklistedAccount) Error() string {
return fmt.Sprintf("blacklisted account: %s", e.Address.Hex())
}
10 changes: 6 additions & 4 deletions core/state/statedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -1358,11 +1358,14 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er
// - Add precompiles to access list (2929)
// - Add the contents of the optional tx access list (2930)
//
// Anzeon fork:
// - Add native managers to access list
//
// Potential EIPs:
// - Reset access list (Berlin)
// - Add coinbase to access list (EIP-3651)
// - Reset transient storage (EIP-1153)
func (s *StateDB) Prepare(rules params.Rules, sender, coinbase common.Address, dst *common.Address, precompiles []common.Address, list types.AccessList) {
func (s *StateDB) Prepare(rules params.Rules, sender, coinbase common.Address, dst *common.Address, precompiles, nativeManagers []common.Address, list types.AccessList) {
if rules.IsBerlin {
// Clear out any leftover from previous executions
al := newAccessList()
Expand All @@ -1385,9 +1388,8 @@ func (s *StateDB) Prepare(rules params.Rules, sender, coinbase common.Address, d
if rules.IsShanghai { // EIP-3651: warm coinbase
al.AddAddress(coinbase)
}

if rules.IsAnzeon {
al.AddAddress(params.NativeCoinManagerAddress)
for _, addr := range nativeManagers {
al.AddAddress(addr)
}
}
// Reset transient storage at the beginning of transaction execution
Expand Down
9 changes: 6 additions & 3 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,19 +478,19 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
if rules.IsAnzeon {
// Check sender blacklist
if st.state.IsBlacklisted(msg.From) {
return nil, fmt.Errorf("%w: sender %v", ErrBlacklistedAccount, msg.From.Hex())
return nil, &ErrBlacklistedAccount{Address: msg.From}
}

// Check recipient blacklist (only for transfers, not for contract creation)
if msg.To != nil && st.state.IsBlacklisted(*msg.To) {
return nil, fmt.Errorf("%w: recipient %v", ErrBlacklistedAccount, msg.To.Hex())
return nil, &ErrBlacklistedAccount{Address: *msg.To}
}
}

// Execute the preparatory steps for state transition which includes:
// - prepare accessList(post-berlin)
// - reset transient storage(eip 1153)
st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList)
st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), vm.ActiveNativeManagers(rules), msg.AccessList)

var (
ret []byte
Expand Down Expand Up @@ -533,6 +533,9 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
// fee delegation
if st.msg.FeePayer != nil {
payer = *st.msg.FeePayer
if rules.IsAnzeon && st.state.IsBlacklisted(payer) {
return nil, &ErrBlacklistedAccount{Address: payer}
}
}
st.evm.AddTransferLog(payer, st.evm.Context.Coinbase, fee)
}
Expand Down
2 changes: 2 additions & 0 deletions core/txpool/blobpool/blobpool.go
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,8 @@ func (p *BlobPool) validateTx(tx *types.Transaction) error {
}
// Ensure the transaction adheres to the stateful pool filters (nonce, balance)
stateOpts := &txpool.ValidationOptionsWithState{
Config: p.chain.Config(),

State: p.state,

FirstNonceGap: func(addr common.Address) uint64 {
Expand Down
2 changes: 2 additions & 0 deletions core/txpool/legacypool/legacypool.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,8 @@ func (pool *LegacyPool) validateTxBasics(tx *types.Transaction, local bool) erro
// rules and adheres to some heuristic limits of the local node (price and size).
func (pool *LegacyPool) validateTx(tx *types.Transaction, local bool) error {
opts := &txpool.ValidationOptionsWithState{
Config: pool.chainconfig,

State: pool.currentState,

FirstNonceGap: nil, // Pool allows arbitrary arrival order, don't invalidate nonce gaps
Expand Down
18 changes: 17 additions & 1 deletion core/txpool/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ func validateBlobSidecar(hashes []common.Hash, sidecar *types.BlobTxSidecar) err
// ValidationOptionsWithState define certain differences between stateful transaction
// validation across the different pools without having to duplicate those checks.
type ValidationOptionsWithState struct {
Config *params.ChainConfig // Chain configuration to selectively validate based on current fork rules

State *state.StateDB // State database to check nonces and balances against

// FirstNonceGap is an optional callback to retrieve the first nonce gap in
Expand Down Expand Up @@ -220,6 +222,15 @@ func ValidateTransactionWithState(tx *types.Transaction, signer types.Signer, op
log.Error("Transaction sender recovery failed", "err", err)
return err
}
// Ensure that neither the sender nor the recipient is blacklisted
if opts.Config.AnzeonEnabled() {
if opts.State.IsBlacklisted(from) {
return &core.ErrBlacklistedAccount{Address: from}
}
if to := tx.To(); to != nil && opts.State.IsBlacklisted(*to) {
return &core.ErrBlacklistedAccount{Address: *to}
}
}
next := opts.State.GetNonce(from)
if next > tx.Nonce() {
return fmt.Errorf("%w: next nonce %v, tx nonce %v", core.ErrNonceTooLow, next, tx.Nonce())
Expand All @@ -233,10 +244,15 @@ func ValidateTransactionWithState(tx *types.Transaction, signer types.Signer, op
}
// Ensure the transactor has enough funds to cover the transaction costs
if tx.Type() == types.FeeDelegateDynamicFeeTxType {
feePayer := tx.FeePayer()
// Ensure that the fee payer is not blacklisted
if opts.Config.AnzeonEnabled() && opts.State.IsBlacklisted(*feePayer) {
return &core.ErrBlacklistedAccount{Address: *feePayer}
}
if opts.State.GetBalance(from).ToBig().Cmp(tx.Value()) < 0 {
return ErrSenderInsufficientFunds
}
if opts.State.GetBalance(*tx.FeePayer()).ToBig().Cmp(tx.FeeCost()) < 0 {
if opts.State.GetBalance(*feePayer).ToBig().Cmp(tx.FeeCost()) < 0 {
return ErrFeePayerInsufficientFunds
}
} else {
Expand Down
Loading