From 4d2aa897b50b37de6c4249575d0319730873b7b7 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Tue, 11 Nov 2025 20:55:19 +0900 Subject: [PATCH 01/20] feat: implement temporary authorized account priority transaction ordering (enabled when Anzeon is active) - Authorized accounts are always prioritized over normal accounts. - Among authorized accounts: higher fee first, then FIFO if equal. - Among normal accounts: FIFO only (no fee-based comparison). - When Anzeon is disabled, fallback to existing ordering logic. - Temporarily define AuthorizedAccounts map in params/protocol_params.go for development use. (TODO: remove once StateAccount.Extra field is implemented) --- miner/ordering.go | 110 ++++++++++++++++++++++++++++++++++++---------- miner/worker.go | 14 +++--- 2 files changed, 95 insertions(+), 29 deletions(-) diff --git a/miner/ordering.go b/miner/ordering.go index bcf7af46e..d962397f0 100644 --- a/miner/ordering.go +++ b/miner/ordering.go @@ -21,8 +21,11 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) @@ -56,33 +59,83 @@ func newTxWithMinerFee(tx *txpool.LazyTransaction, from common.Address, baseFee // txByPriceAndTime implements both the sort and the heap interface, making it useful // for all at once sorting as well as individually adding and removing elements. -type txByPriceAndTime []*txWithMinerFee - -func (s txByPriceAndTime) Len() int { return len(s) } -func (s txByPriceAndTime) Less(i, j int) bool { - // If the prices are equal, use the time the transaction was first seen for - // deterministic sorting - cmp := s[i].fees.Cmp(s[j].fees) - if cmp == 0 { - return s[i].tx.Time.Before(s[j].tx.Time) +type txByPriceAndTime struct { + txs []*txWithMinerFee + anzeonEnabled bool // Cached value of chainConfig.AnzeonEnabled() to avoid repeated checks + isAuthorized map[common.Address]bool // Pre-computed authorized status for all accounts to avoid repeated checks in Less() +} + +func (s *txByPriceAndTime) Len() int { return len(s.txs) } +func (s *txByPriceAndTime) Less(i, j int) bool { + // If Anzeon is not enabled, use the original fee-based ordering + if !s.anzeonEnabled { + log.Info("[hmlee] Anzeon is not enabled, using original fee-based ordering") + cmp := s.txs[i].fees.Cmp(s.txs[j].fees) + if cmp == 0 { + return s.txs[i].tx.Time.Before(s.txs[j].tx.Time) + } + return cmp > 0 + } + + // Check if accounts are authorized (using pre-computed map for performance) + iAuthorized := s.isAuthorized[s.txs[i].from] + jAuthorized := s.isAuthorized[s.txs[j].from] + + // If both are authorized or both are not authorized, use fee-based ordering within the same group + if iAuthorized == jAuthorized { + if iAuthorized { + // Both authorized: compare by fee (higher first), then by time (earlier first) + cmp := s.txs[i].fees.Cmp(s.txs[j].fees) + if cmp == 0 { + return s.txs[i].tx.Time.Before(s.txs[j].tx.Time) + } + return cmp > 0 + } else { + // Both not authorized: compare by time only (earlier first, no fee comparison) + // General accounts are ordered by FIFO regardless of fee + return s.txs[i].tx.Time.Before(s.txs[j].tx.Time) + } } - return cmp > 0 + + // If one is authorized and the other is not, authorized always comes first + return iAuthorized } -func (s txByPriceAndTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s *txByPriceAndTime) Swap(i, j int) { s.txs[i], s.txs[j] = s.txs[j], s.txs[i] } func (s *txByPriceAndTime) Push(x interface{}) { - *s = append(*s, x.(*txWithMinerFee)) + s.txs = append(s.txs, x.(*txWithMinerFee)) } func (s *txByPriceAndTime) Pop() interface{} { - old := *s + old := s.txs n := len(old) x := old[n-1] old[n-1] = nil - *s = old[0 : n-1] + s.txs = old[0 : n-1] return x } +// checkIsAuthorized checks if an account is authorized. +func checkIsAuthorized(addr common.Address, anzeonEnabled bool, stateDB *state.StateDB) bool { + // If Anzeon is not enabled, no account is authorized + if !anzeonEnabled { + return false + } + + // TODO: Once StateAccount.Extra field is implemented, read from stateDB: + // example: + // if stateDB != nil { + // account := stateDB.GetAccount(addr) + // if account != nil { + // // Check Extra field for authorized flag + // return account.IsAuthorized() + // } + // } + + // For now, use hardcoded list from protocol_params + return params.AuthorizedAccounts[addr] +} + // transactionsByPriceAndNonce represents a set of transactions that can return // transactions in a profit-maximizing sorted order, while supporting removing // entire batches of transactions for non-executable accounts. @@ -98,21 +151,32 @@ type transactionsByPriceAndNonce struct { // // Note, the input map is reowned so the caller should not interact any more with // if after providing it to the constructor. -func newTransactionsByPriceAndNonce(signer types.Signer, txs map[common.Address][]*txpool.LazyTransaction, baseFee *big.Int) *transactionsByPriceAndNonce { +func newTransactionsByPriceAndNonce(signer types.Signer, txs map[common.Address][]*txpool.LazyTransaction, baseFee *big.Int, anzeonEnabled bool, stateDB *state.StateDB) *transactionsByPriceAndNonce { // Convert the basefee from header format to uint256 format var baseFeeUint *uint256.Int if baseFee != nil { baseFeeUint = uint256.MustFromBig(baseFee) } // Initialize a price and received time based heap with the head transactions - heads := make(txByPriceAndTime, 0, len(txs)) + isAuthorizedMap := make(map[common.Address]bool, len(txs)) + if anzeonEnabled { + for from := range txs { + isAuthorizedMap[from] = checkIsAuthorized(from, anzeonEnabled, stateDB) + } + } + + heads := txByPriceAndTime{ + txs: make([]*txWithMinerFee, 0, len(txs)), + anzeonEnabled: anzeonEnabled, + isAuthorized: isAuthorizedMap, + } for from, accTxs := range txs { wrapped, err := newTxWithMinerFee(accTxs[0], from, baseFeeUint) if err != nil { delete(txs, from) continue } - heads = append(heads, wrapped) + heads.txs = append(heads.txs, wrapped) txs[from] = accTxs[1:] } heap.Init(&heads) @@ -128,18 +192,18 @@ func newTransactionsByPriceAndNonce(signer types.Signer, txs map[common.Address] // Peek returns the next transaction by price. func (t *transactionsByPriceAndNonce) Peek() (*txpool.LazyTransaction, *uint256.Int) { - if len(t.heads) == 0 { + if len(t.heads.txs) == 0 { return nil, nil } - return t.heads[0].tx, t.heads[0].fees + return t.heads.txs[0].tx, t.heads.txs[0].fees } // Shift replaces the current best head with the next one from the same account. func (t *transactionsByPriceAndNonce) Shift() { - acc := t.heads[0].from + acc := t.heads.txs[0].from if txs, ok := t.txs[acc]; ok && len(txs) > 0 { if wrapped, err := newTxWithMinerFee(txs[0], acc, t.baseFee); err == nil { - t.heads[0], t.txs[acc] = wrapped, txs[1:] + t.heads.txs[0], t.txs[acc] = wrapped, txs[1:] heap.Fix(&t.heads, 0) return } @@ -157,10 +221,10 @@ func (t *transactionsByPriceAndNonce) Pop() { // Empty returns if the price heap is empty. It can be used to check it simpler // than calling peek and checking for nil return. func (t *transactionsByPriceAndNonce) Empty() bool { - return len(t.heads) == 0 + return len(t.heads.txs) == 0 } // Clear removes the entire content of the heap. func (t *transactionsByPriceAndNonce) Clear() { - t.heads, t.txs = nil, nil + t.heads.txs, t.txs = nil, nil } diff --git a/miner/worker.go b/miner/worker.go index ca42f7c1f..c2f75a7fa 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -731,8 +731,9 @@ func (w *worker) mainLoop() { BlobGas: tx.BlobGas(), }) } - plainTxs := newTransactionsByPriceAndNonce(w.current.signer, txs, w.current.header.BaseFee) // Mixed bag of everrything, yolo - blobTxs := newTransactionsByPriceAndNonce(w.current.signer, nil, w.current.header.BaseFee) // Empty bag, don't bother optimising + anzeonEnabled := w.chainConfig.AnzeonEnabled() + plainTxs := newTransactionsByPriceAndNonce(w.current.signer, txs, w.current.header.BaseFee, anzeonEnabled, w.current.state) // Mixed bag of everrything, yolo + blobTxs := newTransactionsByPriceAndNonce(w.current.signer, nil, w.current.header.BaseFee, anzeonEnabled, w.current.state) // Empty bag, don't bother optimising tcount := w.current.tcount w.commitTransactions(w.current, plainTxs, blobTxs, nil) @@ -1263,17 +1264,18 @@ func (w *worker) fillTransactions(interrupt *atomic.Int32, env *environment) err } } // Fill the block with all available pending transactions. + anzeonEnabled := w.chainConfig.AnzeonEnabled() if len(localPlainTxs) > 0 || len(localBlobTxs) > 0 { - plainTxs := newTransactionsByPriceAndNonce(env.signer, localPlainTxs, env.header.BaseFee) - blobTxs := newTransactionsByPriceAndNonce(env.signer, localBlobTxs, env.header.BaseFee) + plainTxs := newTransactionsByPriceAndNonce(env.signer, localPlainTxs, env.header.BaseFee, anzeonEnabled, env.state) + blobTxs := newTransactionsByPriceAndNonce(env.signer, localBlobTxs, env.header.BaseFee, anzeonEnabled, env.state) if err := w.commitTransactions(env, plainTxs, blobTxs, interrupt); err != nil { return err } } if len(remotePlainTxs) > 0 || len(remoteBlobTxs) > 0 { - plainTxs := newTransactionsByPriceAndNonce(env.signer, remotePlainTxs, env.header.BaseFee) - blobTxs := newTransactionsByPriceAndNonce(env.signer, remoteBlobTxs, env.header.BaseFee) + plainTxs := newTransactionsByPriceAndNonce(env.signer, remotePlainTxs, env.header.BaseFee, anzeonEnabled, env.state) + blobTxs := newTransactionsByPriceAndNonce(env.signer, remoteBlobTxs, env.header.BaseFee, anzeonEnabled, env.state) if err := w.commitTransactions(env, plainTxs, blobTxs, interrupt); err != nil { return err From aaf4be979f255e9b705800db9183a67d21a6b2b2 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Tue, 11 Nov 2025 20:56:21 +0900 Subject: [PATCH 02/20] test: add test cases for authorized account transaction ordering --- miner/ordering_test.go | 619 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 617 insertions(+), 2 deletions(-) diff --git a/miner/ordering_test.go b/miner/ordering_test.go index 3587a835c..1aaf7f757 100644 --- a/miner/ordering_test.go +++ b/miner/ordering_test.go @@ -24,12 +24,69 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) +var ( + // Test accounts for authorized account tests + testAuthorizedKey1, _ = crypto.GenerateKey() + testAuthorizedAddr1 = crypto.PubkeyToAddress(testAuthorizedKey1.PublicKey) + testAuthorizedKey2, _ = crypto.GenerateKey() + testAuthorizedAddr2 = crypto.PubkeyToAddress(testAuthorizedKey2.PublicKey) + testAuthorizedKey3, _ = crypto.GenerateKey() + testAuthorizedAddr3 = crypto.PubkeyToAddress(testAuthorizedKey3.PublicKey) + + // Test accounts for normal account tests + testNormalKey1, _ = crypto.GenerateKey() + testNormalAddr1 = crypto.PubkeyToAddress(testNormalKey1.PublicKey) + testNormalKey2, _ = crypto.GenerateKey() + testNormalAddr2 = crypto.PubkeyToAddress(testNormalKey2.PublicKey) + testNormalKey3, _ = crypto.GenerateKey() + testNormalAddr3 = crypto.PubkeyToAddress(testNormalKey3.PublicKey) + + // Pre-configured stateDB for tests + testStateDB = setupTestStateDB() +) + +// setupTestStateDB creates a stateDB for testing and sets up accounts. +func setupTestStateDB() *state.StateDB { + // Create stateDB for testing (prepared for future StateAccount.Extra field usage) + db := rawdb.NewMemoryDatabase() + sdb := state.NewDatabase(db) + stateDB, _ := state.New(types.EmptyRootHash, sdb, nil) + + // Include all test accounts in stateDB + allAddrs := []common.Address{ + testAuthorizedAddr1, testAuthorizedAddr2, testAuthorizedAddr3, + testNormalAddr1, testNormalAddr2, testNormalAddr3, + } + for _, addr := range allAddrs { + stateDB.CreateAccount(addr) + } + + // TO DO : Once StateAccount.Extra field is implemented, use stateDB.SetAccountExtra() instead of params.AuthorizedAccounts + // example: + // stateDB.SetAccountExtra(testAuthorizedAddr1, &types.AccountExtra{Authorized: true}) + // stateDB.SetAccountExtra(testAuthorizedAddr2, &types.AccountExtra{Authorized: true}) + // stateDB.SetAccountExtra(testAuthorizedAddr3, &types.AccountExtra{Authorized: true}) + //} + + // TO DO : if Once StateAccount.Extra field is implemented, remove this code + authorizedAddrs := []common.Address{ + testAuthorizedAddr1, testAuthorizedAddr2, testAuthorizedAddr3, + } + for _, addr := range authorizedAddrs { + params.AuthorizedAccounts[addr] = true + } + return stateDB +} + func TestTransactionPriceNonceSortLegacy(t *testing.T) { t.Parallel() testTransactionPriceNonceSort(t, nil) @@ -102,7 +159,7 @@ func testTransactionPriceNonceSort(t *testing.T, baseFee *big.Int) { expectedCount += count } // Sort the transactions and cross check the nonce ordering - txset := newTransactionsByPriceAndNonce(signer, groups, baseFee) + txset := newTransactionsByPriceAndNonce(signer, groups, baseFee, false, nil) txs := types.Transactions{} for tx, _ := txset.Peek(); tx != nil; tx, _ = txset.Peek() { @@ -168,7 +225,7 @@ func TestTransactionTimeSort(t *testing.T) { }) } // Sort the transactions and cross check the nonce ordering - txset := newTransactionsByPriceAndNonce(signer, groups, nil) + txset := newTransactionsByPriceAndNonce(signer, groups, nil, false, nil) txs := types.Transactions{} for tx, _ := txset.Peek(); tx != nil; tx, _ = txset.Peek() { @@ -194,3 +251,561 @@ func TestTransactionTimeSort(t *testing.T) { } } } + +// TestAuthorizedAccountPriority tests the priority ordering for authorized accounts +func TestAuthorizedAccountPriority(t *testing.T) { + t.Parallel() + + signer := types.LatestSignerForChainID(common.Big1) + + // Use pre-configured stateDB with authorized accounts + stateDB := testStateDB + + groups := map[common.Address][]*txpool.LazyTransaction{} + + // Authorized account 1 + tx1, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(20), + GasTipCap: big.NewInt(10), + Data: nil, + }), signer, testAuthorizedKey1) + tx1.SetTime(time.Unix(4, 0)) + groups[testAuthorizedAddr1] = []*txpool.LazyTransaction{{ + Hash: tx1.Hash(), + Tx: tx1, + Time: tx1.Time(), + GasFeeCap: uint256.MustFromBig(tx1.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx1.GasTipCap()), + Gas: tx1.Gas(), + BlobGas: tx1.BlobGas(), + }} + + // Authorized account 2 + tx2, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(15), + GasTipCap: big.NewInt(5), + Data: nil, + }), signer, testAuthorizedKey2) + tx2.SetTime(time.Unix(3, 0)) + groups[testAuthorizedAddr2] = []*txpool.LazyTransaction{{ + Hash: tx2.Hash(), + Tx: tx2, + Time: tx2.Time(), + GasFeeCap: uint256.MustFromBig(tx2.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx2.GasTipCap()), + Gas: tx2.Gas(), + BlobGas: tx2.BlobGas(), + }} + + // Normal account 1 + tx3, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(200), + GasTipCap: big.NewInt(100), + Data: nil, + }), signer, testNormalKey1) + tx3.SetTime(time.Unix(2, 0)) + groups[testNormalAddr1] = []*txpool.LazyTransaction{{ + Hash: tx3.Hash(), + Tx: tx3, + Time: tx3.Time(), + GasFeeCap: uint256.MustFromBig(tx3.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx3.GasTipCap()), + Gas: tx3.Gas(), + BlobGas: tx3.BlobGas(), + }} + + // Normal account 2 + tx4, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(15), + GasTipCap: big.NewInt(5), + Data: nil, + }), signer, testNormalKey2) + tx4.SetTime(time.Unix(1, 0)) + groups[testNormalAddr2] = []*txpool.LazyTransaction{{ + Hash: tx4.Hash(), + Tx: tx4, + Time: tx4.Time(), + GasFeeCap: uint256.MustFromBig(tx4.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx4.GasTipCap()), + Gas: tx4.Gas(), + BlobGas: tx4.BlobGas(), + }} + + txset := newTransactionsByPriceAndNonce(signer, groups, big.NewInt(0), true, stateDB) + + txs := types.Transactions{} + for tx, _ := txset.Peek(); tx != nil; tx, _ = txset.Peek() { + txs = append(txs, tx.Tx) + txset.Shift() + } + + // fee order: tx3 > tx1 > tx4 = tx2 + // time order: tx4 > tx3 > tx2 > tx1 + // tx1(testAuthorizedAddr1), tx2(testAuthorizedAddr2) : Authorized account + // tx3(testNormalAddr1),tx4(testNormalAddr2) : Normal account + + // Expected order: + // tx1 > tx2 > tx4 > tx3 + if len(txs) != 4 { + t.Fatalf("expected 4 transactions, found %d", len(txs)) + } + + from1, _ := types.Sender(signer, txs[0]) + from2, _ := types.Sender(signer, txs[1]) + from3, _ := types.Sender(signer, txs[2]) + from4, _ := types.Sender(signer, txs[3]) + + // First two should be authorized accounts (ordered by fee) + if from1 != testAuthorizedAddr1 { + t.Errorf("expected first tx from authorized account 1, got %x", from1) + } + if from2 != testAuthorizedAddr2 { + t.Errorf("expected second tx from authorized account 2, got %x", from2) + } + + // Last two should be normal accounts (ordered by time/FIFO) + if from3 != testNormalAddr2 { + t.Errorf("expected third tx from normal account 1, got %x", from3) + } + if from4 != testNormalAddr1 { + t.Errorf("expected fourth tx from normal account 2, got %x", from4) + } +} + +// TestAuthorizedAccountPrioritySameFee tests authorized accounts with same fee (should use FIFO) +func TestAuthorizedAccountPrioritySameFee(t *testing.T) { + t.Parallel() + + signer := types.LatestSignerForChainID(common.Big1) + + // Use pre-configured stateDB with authorized accounts + stateDB := testStateDB + + groups := map[common.Address][]*txpool.LazyTransaction{} + + // Authorized account 1 - same fee + tx1, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(20), + GasTipCap: big.NewInt(10), + Data: nil, + }), signer, testAuthorizedKey1) + tx1.SetTime(time.Unix(1, 0)) + groups[testAuthorizedAddr1] = []*txpool.LazyTransaction{{ + Hash: tx1.Hash(), + Tx: tx1, + Time: tx1.Time(), + GasFeeCap: uint256.MustFromBig(tx1.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx1.GasTipCap()), + Gas: tx1.Gas(), + BlobGas: tx1.BlobGas(), + }} + + // Authorized account 2 - same fee + tx2, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(20), + GasTipCap: big.NewInt(10), + Data: nil, + }), signer, testAuthorizedKey2) + tx2.SetTime(time.Unix(3, 0)) + groups[testAuthorizedAddr2] = []*txpool.LazyTransaction{{ + Hash: tx2.Hash(), + Tx: tx2, + Time: tx2.Time(), + GasFeeCap: uint256.MustFromBig(tx2.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx2.GasTipCap()), + Gas: tx2.Gas(), + BlobGas: tx2.BlobGas(), + }} + + // Authorized account 3 - same fee + tx3, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(20), + GasTipCap: big.NewInt(10), + Data: nil, + }), signer, testAuthorizedKey3) + tx3.SetTime(time.Unix(2, 0)) + groups[testAuthorizedAddr3] = []*txpool.LazyTransaction{{ + Hash: tx3.Hash(), + Tx: tx3, + Time: tx3.Time(), + GasFeeCap: uint256.MustFromBig(tx3.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx3.GasTipCap()), + Gas: tx3.Gas(), + BlobGas: tx3.BlobGas(), + }} + + txset := newTransactionsByPriceAndNonce(signer, groups, big.NewInt(0), true, stateDB) + + txs := types.Transactions{} + for tx, _ := txset.Peek(); tx != nil; tx, _ = txset.Peek() { + txs = append(txs, tx.Tx) + txset.Shift() + } + + // fee : same for all authorized accounts + // time order: tx1 > tx3 > tx2 + + // Expected order: + // tx1 > tx3 > tx2 + if len(txs) != 3 { + t.Fatalf("expected 3 transactions, found %d", len(txs)) + } + + from1, _ := types.Sender(signer, txs[0]) + from2, _ := types.Sender(signer, txs[1]) + from3, _ := types.Sender(signer, txs[2]) + + // Should be ordered by time (FIFO) when fees are equal + if from1 != testAuthorizedAddr1 { + t.Errorf("expected first tx from authorized account 1, got %x", from1) + } + if from2 != testAuthorizedAddr3 { + t.Errorf("expected second tx from authorized account 3, got %x", from2) + } + if from3 != testAuthorizedAddr2 { + t.Errorf("expected third tx from authorized account 2, got %x", from3) + } +} + +// TestNotAuthorizedFIFO tests that normal accounts are ordered by FIFO regardless of fee +func TestNotAuthorizedFIFO(t *testing.T) { + t.Parallel() + + signer := types.LatestSignerForChainID(common.Big1) + + // Use pre-configured stateDB + stateDB := testStateDB + + groups := map[common.Address][]*txpool.LazyTransaction{} + + // Normal account 1 + tx1, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(30), + GasTipCap: big.NewInt(10), + Data: nil, + }), signer, testNormalKey1) + tx1.SetTime(time.Unix(2, 0)) + groups[testNormalAddr1] = []*txpool.LazyTransaction{{ + Hash: tx1.Hash(), + Tx: tx1, + Time: tx1.Time(), + GasFeeCap: uint256.MustFromBig(tx1.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx1.GasTipCap()), + Gas: tx1.Gas(), + BlobGas: tx1.BlobGas(), + }} + + // Normal account 2 + tx2, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(40), + GasTipCap: big.NewInt(20), + Data: nil, + }), signer, testNormalKey2) + tx2.SetTime(time.Unix(3, 0)) + groups[testNormalAddr2] = []*txpool.LazyTransaction{{ + Hash: tx2.Hash(), + Tx: tx2, + Time: tx2.Time(), + GasFeeCap: uint256.MustFromBig(tx2.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx2.GasTipCap()), + Gas: tx2.Gas(), + BlobGas: tx2.BlobGas(), + }} + + // Normal account 3 + tx3, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(10), + GasTipCap: big.NewInt(5), + Data: nil, + }), signer, testNormalKey3) + tx3.SetTime(time.Unix(1, 0)) + groups[testNormalAddr3] = []*txpool.LazyTransaction{{ + Hash: tx3.Hash(), + Tx: tx3, + Time: tx3.Time(), + GasFeeCap: uint256.MustFromBig(tx3.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx3.GasTipCap()), + Gas: tx3.Gas(), + BlobGas: tx3.BlobGas(), + }} + + txset := newTransactionsByPriceAndNonce(signer, groups, big.NewInt(0), true, stateDB) + + txs := types.Transactions{} + for tx, _ := txset.Peek(); tx != nil; tx, _ = txset.Peek() { + txs = append(txs, tx.Tx) + txset.Shift() + } + + // fee order : Normal account is not considered, only authorized accounts are considered for fee ordering in anzeon enabled mode + // time order: tx3 > tx1 > tx2 + + // Expected order: + // tx3 > tx1 > tx2 + if len(txs) != 3 { + t.Fatalf("expected 3 transactions, found %d", len(txs)) + } + + from1, _ := types.Sender(signer, txs[0]) + from2, _ := types.Sender(signer, txs[1]) + from3, _ := types.Sender(signer, txs[2]) + + // Should be ordered by time (FIFO) regardless of fee in anzeon enabled mode + if from1 != testNormalAddr3 { + t.Errorf("expected first tx from normal account 3, got %x", from1) + } + if from2 != testNormalAddr1 { + t.Errorf("expected second tx from normal account 1, got %x", from2) + } + if from3 != testNormalAddr2 { + t.Errorf("expected third tx from normal account 2, got %x", from3) + } +} + +// TestAnzeonDisabledAndFIFO tests that normal accounts are ordered by FIFO regardless of fee +func TestAnzeonDisabledAndFIFO(t *testing.T) { + t.Parallel() + + signer := types.LatestSignerForChainID(common.Big1) + + groups := map[common.Address][]*txpool.LazyTransaction{} + + // Normal account 1 + tx1, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(10), + GasTipCap: big.NewInt(5), + Data: nil, + }), signer, testNormalKey1) + tx1.SetTime(time.Unix(3, 0)) + groups[testNormalAddr1] = []*txpool.LazyTransaction{{ + Hash: tx1.Hash(), + Tx: tx1, + Time: tx1.Time(), + GasFeeCap: uint256.MustFromBig(tx1.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx1.GasTipCap()), + Gas: tx1.Gas(), + BlobGas: tx1.BlobGas(), + }} + + // Normal account 2 + tx2, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(10), + GasTipCap: big.NewInt(5), + Data: nil, + }), signer, testNormalKey2) + tx2.SetTime(time.Unix(1, 0)) + groups[testNormalAddr2] = []*txpool.LazyTransaction{{ + Hash: tx2.Hash(), + Tx: tx2, + Time: tx2.Time(), + GasFeeCap: uint256.MustFromBig(tx2.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx2.GasTipCap()), + Gas: tx2.Gas(), + BlobGas: tx2.BlobGas(), + }} + + // Normal account 3 + tx3, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(10), + GasTipCap: big.NewInt(5), + Data: nil, + }), signer, testNormalKey3) + tx3.SetTime(time.Unix(2, 0)) + groups[testNormalAddr3] = []*txpool.LazyTransaction{{ + Hash: tx3.Hash(), + Tx: tx3, + Time: tx3.Time(), + GasFeeCap: uint256.MustFromBig(tx3.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx3.GasTipCap()), + Gas: tx3.Gas(), + BlobGas: tx3.BlobGas(), + }} + + txset := newTransactionsByPriceAndNonce(signer, groups, big.NewInt(0), false, nil) + + txs := types.Transactions{} + for tx, _ := txset.Peek(); tx != nil; tx, _ = txset.Peek() { + txs = append(txs, tx.Tx) + txset.Shift() + } + + // fee order : same for all normal accounts + // time order: tx2 > tx3 > tx1 + + // Expected order: + // tx2 > tx3 > tx1 + if len(txs) != 3 { + t.Fatalf("expected 3 transactions, found %d", len(txs)) + } + + from1, _ := types.Sender(signer, txs[0]) + from2, _ := types.Sender(signer, txs[1]) + from3, _ := types.Sender(signer, txs[2]) + + // Should be ordered by time (FIFO) when fees are equal + if from1 != testNormalAddr2 { + t.Errorf("expected first tx from normal account 2, got %x", from1) + } + if from2 != testNormalAddr3 { + t.Errorf("expected second tx from normal account 3, got %x", from2) + } + if from3 != testNormalAddr1 { + t.Errorf("expected third tx from normal account 1, got %x", from3) + } +} + +// TestAnzeonDisabledAndHigherFeeFirst tests that when Anzeon is disabled, original fee-based ordering is used and higher fee first +func TestAnzeonDisabledAndHigherFeeFirst(t *testing.T) { + t.Parallel() + + signer := types.LatestSignerForChainID(common.Big1) + + groups := map[common.Address][]*txpool.LazyTransaction{} + + // Account 1 + tx1, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(10), + GasTipCap: big.NewInt(5), + Data: nil, + }), signer, testNormalKey1) + tx1.SetTime(time.Unix(1, 0)) + groups[testNormalAddr1] = []*txpool.LazyTransaction{{ + Hash: tx1.Hash(), + Tx: tx1, + Time: tx1.Time(), + GasFeeCap: uint256.MustFromBig(tx1.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx1.GasTipCap()), + Gas: tx1.Gas(), + BlobGas: tx1.BlobGas(), + }} + + // Account 2 + tx2, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(30), + GasTipCap: big.NewInt(20), + Data: nil, + }), signer, testNormalKey2) + tx1.SetTime(time.Unix(2, 0)) + groups[testNormalAddr2] = []*txpool.LazyTransaction{{ + Hash: tx2.Hash(), + Tx: tx2, + Time: tx2.Time(), + GasFeeCap: uint256.MustFromBig(tx2.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx2.GasTipCap()), + Gas: tx2.Gas(), + BlobGas: tx2.BlobGas(), + }} + + // Account 3 + tx3, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: big.NewInt(25), + GasTipCap: big.NewInt(10), + Data: nil, + }), signer, testNormalKey3) + tx3.SetTime(time.Unix(3, 0)) + groups[testNormalAddr3] = []*txpool.LazyTransaction{{ + Hash: tx3.Hash(), + Tx: tx3, + Time: tx3.Time(), + GasFeeCap: uint256.MustFromBig(tx3.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx3.GasTipCap()), + Gas: tx3.Gas(), + BlobGas: tx3.BlobGas(), + }} + + txset := newTransactionsByPriceAndNonce(signer, groups, big.NewInt(0), false, nil) + + txs := types.Transactions{} + for tx, _ := txset.Peek(); tx != nil; tx, _ = txset.Peek() { + txs = append(txs, tx.Tx) + txset.Shift() + } + + // fee order : tx2 > tx3 > tx1 + // time order: tx1 > tx2 > tx3 + + // Expected order: + // tx2 > tx3 > tx1 + if len(txs) != 3 { + t.Fatalf("expected 3 transactions, found %d", len(txs)) + } + + from1, _ := types.Sender(signer, txs[0]) + from2, _ := types.Sender(signer, txs[1]) + from3, _ := types.Sender(signer, txs[2]) + + // Should be ordered by fee (higher first) when Anzeon is disabled + if from1 != testNormalAddr2 { + t.Errorf("expected first tx from account 2, got %x", from1) + } + if from2 != testNormalAddr3 { + t.Errorf("expected second tx from account 3, got %x", from2) + } + if from3 != testNormalAddr1 { + t.Errorf("expected third tx from account 1, got %x", from3) + } +} From b780da4008ecaa4e8255a53bb5e544177699e36f Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Tue, 11 Nov 2025 20:58:44 +0900 Subject: [PATCH 03/20] chore: remove comment --- miner/ordering.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/miner/ordering.go b/miner/ordering.go index d962397f0..10e69207d 100644 --- a/miner/ordering.go +++ b/miner/ordering.go @@ -24,7 +24,6 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) @@ -69,7 +68,6 @@ func (s *txByPriceAndTime) Len() int { return len(s.txs) } func (s *txByPriceAndTime) Less(i, j int) bool { // If Anzeon is not enabled, use the original fee-based ordering if !s.anzeonEnabled { - log.Info("[hmlee] Anzeon is not enabled, using original fee-based ordering") cmp := s.txs[i].fees.Cmp(s.txs[j].fees) if cmp == 0 { return s.txs[i].tx.Time.Before(s.txs[j].tx.Time) From cac112020fb8d389ba68fc33b4626d35bf3b6751 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Wed, 12 Nov 2025 11:58:31 +0900 Subject: [PATCH 04/20] feat: update msg.GasTipCap handling in TransactionToMessage for authorized accounts - If Anzeon is enabled and the sender is an authorized account, use tx.GasTipCap() directly. - Otherwise, use the header's gas tip value. --- core/state_transition.go | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/core/state_transition.go b/core/state_transition.go index d644ee44a..086f30735 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -23,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/common" cmath "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto/kzg4844" @@ -150,11 +151,36 @@ type Message struct { SkipAccountChecks bool } +// checkIsAuthorized checks if an account is authorized. +func checkIsAuthorized(addr common.Address, stateDB *state.StateDB) bool { + // TODO: Once StateAccount.Extra field is implemented, read from stateDB: + // example: + // if stateDB != nil { + // account := stateDB.GetAccount(addr) + // if account != nil { + // // Check Extra field for authorized flag + // return account.IsAuthorized() + // } + // } + + // For now, use hardcoded list from protocol_params + return params.AuthorizedAccounts[addr] +} + // TransactionToMessage converts a transaction into a Message. -func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee, headerGasTip *big.Int) (*Message, error) { +func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee, headerGasTip *big.Int, statedb *state.StateDB) (*Message, error) { + from, err := types.Sender(s, tx) + if err != nil { + return nil, err + } + gasTipCap := new(big.Int).Set(tx.GasTipCap()) if headerGasTip != nil { - gasTipCap = new(big.Int).Set(headerGasTip) + // If Anzeon is enabled, and the sender is authorized, use the tx's tx.GasTipCap() + // Otherwise, use the header's gas tip + if !checkIsAuthorized(from, statedb) { + gasTipCap = new(big.Int).Set(headerGasTip) + } } msg := &Message{ @@ -179,8 +205,8 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee, header if tx.FeePayer() != nil { msg.FeePayer = tx.FeePayer() } - var err error - msg.From, err = types.Sender(s, tx) + + msg.From = from return msg, err } From 04121e51e7965b058ed9f8ecfa05ceb17aaa68de Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Wed, 12 Nov 2025 12:55:38 +0900 Subject: [PATCH 05/20] feat: pass stateDB as input parameter to TransactionToMessag --- cmd/evm/internal/t8ntool/execution.go | 2 +- core/state_prefetcher.go | 2 +- core/state_processor.go | 4 ++-- eth/state_accessor.go | 2 +- eth/tracers/api.go | 12 ++++++------ eth/tracers/api_test.go | 2 +- eth/tracers/internal/tracetest/calltrace_test.go | 4 ++-- .../internal/tracetest/flat_calltrace_test.go | 2 +- eth/tracers/internal/tracetest/prestate_test.go | 2 +- eth/tracers/tracers_test.go | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 9ba93d05f..d8d5ec0bb 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -206,7 +206,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, rejectedTxs = append(rejectedTxs, &rejectedTx{i, errMsg}) continue } - msg, err := core.TransactionToMessage(tx, signer, pre.Env.BaseFee, nil) + msg, err := core.TransactionToMessage(tx, signer, pre.Env.BaseFee, nil, statedb) if err != nil { log.Warn("rejected tx", "index", i, "hash", tx.Hash(), "error", err) rejectedTxs = append(rejectedTxs, &rejectedTx{i, err.Error()}) diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index 5c84e3190..ec1899abb 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -64,7 +64,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c } // Convert the transaction into an executable message and pre-cache its sender - msg, err := TransactionToMessage(tx, signer, header.BaseFee, header.GasTip()) + msg, err := TransactionToMessage(tx, signer, header.BaseFee, header.GasTip(), statedb) if err != nil { return // Also invalid block, bail out } diff --git a/core/state_processor.go b/core/state_processor.go index 0ceaf49c3..bb61f45f4 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -81,7 +81,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg } // Iterate over and process the individual transactions for i, tx := range block.Transactions() { - msg, err := TransactionToMessage(tx, signer, header.BaseFee, header.GasTip()) + msg, err := TransactionToMessage(tx, signer, header.BaseFee, header.GasTip(), statedb) if err != nil { return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } @@ -161,7 +161,7 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta // for the transaction, gas used and an error if the transaction failed, // indicating the block was invalid. func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, error) { - msg, err := TransactionToMessage(tx, types.MakeSigner(config, header.Number, header.Time), header.BaseFee, header.GasTip()) + msg, err := TransactionToMessage(tx, types.MakeSigner(config, header.Number, header.Time), header.BaseFee, header.GasTip(), statedb) if err != nil { return nil, err } diff --git a/eth/state_accessor.go b/eth/state_accessor.go index d67bd81bb..e26df0ec7 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -245,7 +245,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, if block.Header() != nil && block.Header().GasTip() != nil { headerGasTip = block.Header().GasTip() } - msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee(), headerGasTip) + msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee(), headerGasTip, statedb) txContext := core.NewEVMTxContext(msg) context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil) if idx == txIndex { diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 27a2dfd6d..10c17df2e 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -275,7 +275,7 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed if task.block.Header() != nil && task.block.Header().GasTip() != nil { headerGasTip = task.block.Header().GasTip() } - msg, _ := core.TransactionToMessage(tx, signer, task.block.BaseFee(), headerGasTip) + msg, _ := core.TransactionToMessage(tx, signer, task.block.BaseFee(), headerGasTip, task.statedb) txctx := &Context{ BlockHash: task.block.Hash(), BlockNumber: task.block.Number(), @@ -541,7 +541,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config } var ( - msg, _ = core.TransactionToMessage(tx, signer, block.BaseFee(), headerGasTip) + msg, _ = core.TransactionToMessage(tx, signer, block.BaseFee(), headerGasTip, statedb) txContext = core.NewEVMTxContext(msg) vmenv = vm.NewEVM(vmctx, txContext, statedb, chainConfig, vm.Config{}) ) @@ -619,7 +619,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac headerGasTip = block.Header().GasTip() } // Generate the next state snapshot fast without tracing - msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee(), headerGasTip) + msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee(), headerGasTip, statedb) txctx := &Context{ BlockHash: blockHash, BlockNumber: block.Number(), @@ -666,7 +666,7 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat if block.Header() != nil && block.Header().GasTip() != nil { headerGasTip = block.Header().GasTip() } - msg, _ := core.TransactionToMessage(txs[task.index], signer, block.BaseFee(), headerGasTip) + msg, _ := core.TransactionToMessage(txs[task.index], signer, block.BaseFee(), headerGasTip, task.statedb) txctx := &Context{ BlockHash: blockHash, BlockNumber: block.Number(), @@ -702,7 +702,7 @@ txloop: } // Generate the next state snapshot fast without tracing - msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee(), headerGasTip) + msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee(), headerGasTip, statedb) statedb.SetTxContext(tx.Hash(), i) vmenv := vm.NewEVM(blockCtx, core.NewEVMTxContext(msg), statedb, api.backend.ChainConfig(), vm.Config{}) if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.GasLimit)); err != nil { @@ -787,7 +787,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block } var ( - msg, _ = core.TransactionToMessage(tx, signer, block.BaseFee(), headerGasTip) + msg, _ = core.TransactionToMessage(tx, signer, block.BaseFee(), headerGasTip, statedb) txContext = core.NewEVMTxContext(msg) vmConf vm.Config dump *os.File diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index a88ffdf3c..1d3d058e7 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -174,7 +174,7 @@ func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block if block.Header() != nil && block.Header().GasTip() != nil { headerGasTip = block.Header().GasTip() } - msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee(), headerGasTip) + msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee(), headerGasTip, statedb) txContext := core.NewEVMTxContext(msg) context := core.NewEVMBlockContext(block.Header(), b.chain, nil) if idx == txIndex { diff --git a/eth/tracers/internal/tracetest/calltrace_test.go b/eth/tracers/internal/tracetest/calltrace_test.go index 30c7948a8..7dfce0ece 100644 --- a/eth/tracers/internal/tracetest/calltrace_test.go +++ b/eth/tracers/internal/tracetest/calltrace_test.go @@ -141,7 +141,7 @@ func testCallTracer(tracerName string, dirPath string, t *testing.T) { if err != nil { t.Fatalf("failed to create call tracer: %v", err) } - msg, err := core.TransactionToMessage(tx, signer, context.BaseFee, nil) + msg, err := core.TransactionToMessage(tx, signer, context.BaseFee, nil, nil) if err != nil { t.Fatalf("failed to prepare transaction for tracing: %v", err) } @@ -231,7 +231,7 @@ func benchTracer(tracerName string, test *callTracerTest, b *testing.B) { Difficulty: (*big.Int)(test.Context.Difficulty), GasLimit: uint64(test.Context.GasLimit), } - msg, err := core.TransactionToMessage(tx, signer, context.BaseFee, nil) + msg, err := core.TransactionToMessage(tx, signer, context.BaseFee, nil, nil) if err != nil { b.Fatalf("failed to prepare transaction for tracing: %v", err) } diff --git a/eth/tracers/internal/tracetest/flat_calltrace_test.go b/eth/tracers/internal/tracetest/flat_calltrace_test.go index a9baf26af..6d5fe597c 100644 --- a/eth/tracers/internal/tracetest/flat_calltrace_test.go +++ b/eth/tracers/internal/tracetest/flat_calltrace_test.go @@ -103,7 +103,7 @@ func flatCallTracerTestRunner(tracerName string, filename string, dirPath string if err != nil { return fmt.Errorf("failed to create call tracer: %v", err) } - msg, err := core.TransactionToMessage(tx, signer, context.BaseFee, nil) + msg, err := core.TransactionToMessage(tx, signer, context.BaseFee, nil, nil) if err != nil { return fmt.Errorf("failed to prepare transaction for tracing: %v", err) } diff --git a/eth/tracers/internal/tracetest/prestate_test.go b/eth/tracers/internal/tracetest/prestate_test.go index fdd2d0cd5..bc0ed4f90 100644 --- a/eth/tracers/internal/tracetest/prestate_test.go +++ b/eth/tracers/internal/tracetest/prestate_test.go @@ -111,7 +111,7 @@ func testPrestateDiffTracer(tracerName string, dirPath string, t *testing.T) { if err != nil { t.Fatalf("failed to create call tracer: %v", err) } - msg, err := core.TransactionToMessage(tx, signer, context.BaseFee, nil) + msg, err := core.TransactionToMessage(tx, signer, context.BaseFee, nil, nil) if err != nil { t.Fatalf("failed to prepare transaction for tracing: %v", err) } diff --git a/eth/tracers/tracers_test.go b/eth/tracers/tracers_test.go index af7a6a4de..18c4794f6 100644 --- a/eth/tracers/tracers_test.go +++ b/eth/tracers/tracers_test.go @@ -90,7 +90,7 @@ func BenchmarkTransactionTrace(b *testing.B) { //EnableReturnData: false, }) evm := vm.NewEVM(context, txContext, state.StateDB, params.AllEthashProtocolChanges, vm.Config{Tracer: tracer}) - msg, err := core.TransactionToMessage(tx, signer, context.BaseFee, big.NewInt(500)) + msg, err := core.TransactionToMessage(tx, signer, context.BaseFee, big.NewInt(500), state.StateDB) if err != nil { b.Fatalf("failed to prepare transaction for tracing: %v", err) } From 31485b7bc782861d19a4c36e5477bc14660e265e Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Wed, 12 Nov 2025 16:29:40 +0900 Subject: [PATCH 06/20] feat: move effectiveGasPrice calculation for authorized accounts to applyTransaction - DeriveFields() cannot access stateDB, making it impossible to check if the sender is an authorized account. - For authorized accounts, calculate effectiveGasPrice in applyTransaction(). - For normal accounts, keep the existing DeriveFields() logic unchanged. --- core/state_processor.go | 11 ++++++++--- core/types/receipt.go | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/state_processor.go b/core/state_processor.go index bb61f45f4..6eae1d318 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -86,7 +86,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } statedb.SetTxContext(tx.Hash(), i) - receipt, err := applyTransaction(msg, p.config, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv) + receipt, err := applyTransaction(msg, p.config, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, header.BaseFee) if err != nil { return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } @@ -106,7 +106,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg return receipts, allLogs, *usedGas, nil } -func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (*types.Receipt, error) { +func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, baseFee *big.Int) (*types.Receipt, error) { // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(msg) evm.Reset(txContext, statedb) @@ -153,6 +153,11 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta receipt.BlockHash = blockHash receipt.BlockNumber = blockNumber receipt.TransactionIndex = uint(statedb.TxIndex()) + if config.AnzeonEnabled() { + if checkIsAuthorized(msg.From, statedb) { + receipt.EffectiveGasPrice = tx.EffectiveGasPrice(baseFee, nil) + } + } return receipt, err } @@ -169,7 +174,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo blockContext := NewEVMBlockContext(header, bc, author) txContext := NewEVMTxContext(msg) vmenv := vm.NewEVM(blockContext, txContext, statedb, config, cfg) - return applyTransaction(msg, config, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv) + return applyTransaction(msg, config, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv, header.BaseFee) } // ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root diff --git a/core/types/receipt.go b/core/types/receipt.go index da16a3924..2ecd1c571 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -334,7 +334,9 @@ func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, nu // The transaction type and hash can be retrieved from the transaction itself rs[i].Type = txs[i].Type() rs[i].TxHash = txs[i].Hash() - rs[i].EffectiveGasPrice = txs[i].inner.effectiveGasPrice(baseFee, headerGasTip) + if rs[i].EffectiveGasPrice == nil { + rs[i].EffectiveGasPrice = txs[i].inner.effectiveGasPrice(baseFee, headerGasTip) + } // EIP-4844 blob transaction fields if txs[i].Type() == BlobTxType { rs[i].BlobGasUsed = txs[i].BlobGas() From 86ae9005986adaba4f7e275438046159223a7bde Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Wed, 12 Nov 2025 17:20:38 +0900 Subject: [PATCH 07/20] test: add gas fee calculation tests for authorized and normal accounts --- core/state_processor_test.go | 408 +++++++++++++++++++++++++++++++++++ 1 file changed, 408 insertions(+) diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 276a1c321..24835a4a0 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -20,6 +20,7 @@ import ( "crypto/ecdsa" "math/big" "testing" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -29,10 +30,12 @@ import ( "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" "github.com/holiman/uint256" "golang.org/x/crypto/sha3" @@ -422,3 +425,408 @@ func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Tr } return types.NewBlock(header, txs, nil, receipts, trie.NewStackTrie(nil)) } + +var ( + // Test accounts + testTxMsgAuthorizedKey, _ = crypto.GenerateKey() + testTxMsgAuthorizedAddr = crypto.PubkeyToAddress(testTxMsgAuthorizedKey.PublicKey) + + testTxMsgNormalKey, _ = crypto.GenerateKey() + testTxMsgNormalAddr = crypto.PubkeyToAddress(testTxMsgNormalKey.PublicKey) +) + +// setupTestStateDB creates a stateDB for testing. +func setupTestStateDB() *state.StateDB { + // Create stateDB for testing (prepared for future StateAccount.Extra field usage) + db := rawdb.NewMemoryDatabase() + sdb := state.NewDatabase(db) + stateDB, _ := state.New(types.EmptyRootHash, sdb, nil) + + // Include all test accounts in stateDB + allAddrs := []common.Address{ + testTxMsgAuthorizedAddr, + testTxMsgNormalAddr, + } + for _, addr := range allAddrs { + stateDB.CreateAccount(addr) + } + + // TODO: Once StateAccount.Extra field is implemented, use stateDB.SetAccountExtra() instead of params.AuthorizedAccounts + // example: + // stateDB.SetAccountExtra(testTxMsgAuthorizedAddr, &types.AccountExtra{Authorized: true}) + // + // For now, temporarily uses hardcoded list from protocol_params for testing. + params.AuthorizedAccounts[testTxMsgAuthorizedAddr] = true + + return stateDB +} + +// TestTransactionToMessageAuthorizedAccount tests that authorized accounts use their transaction's GasTipCap +func TestTransactionToMessageAuthorizedAccount(t *testing.T) { + t.Parallel() + + stateDB := setupTestStateDB() + signer := types.LatestSignerForChainID(common.Big1) + + // Test parameters + baseFee := big.NewInt(10) + headerGasTip := big.NewInt(5) + txGasTipCap := big.NewInt(20) // Authorized account's gas tip cap (higher than headerGasTip) + txGasFeeCap := big.NewInt(50) + + // Create transaction from authorized account + tx, err := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 21000, + GasTipCap: txGasTipCap, + GasFeeCap: txGasFeeCap, + Data: nil, + }), signer, testTxMsgAuthorizedKey) + if err != nil { + t.Fatalf("failed to sign transaction: %v", err) + } + + // Convert transaction to message + msg, err := TransactionToMessage(tx, signer, baseFee, headerGasTip, stateDB) + if err != nil { + t.Fatalf("failed to convert transaction to message: %v", err) + } + + // Verify that authorized account uses transaction's GasTipCap + expectedGasTipCap := txGasTipCap + if msg.GasTipCap.Cmp(expectedGasTipCap) != 0 { + t.Errorf("expected GasTipCap %v, got %v", expectedGasTipCap, msg.GasTipCap) + } +} + +// TestTransactionToMessageNormalAccount tests that normal accounts use header's gas tip +func TestTransactionToMessageNormalAccount(t *testing.T) { + t.Parallel() + + stateDB := setupTestStateDB() + signer := types.LatestSignerForChainID(common.Big1) + + // Test parameters + baseFee := big.NewInt(10) + headerGasTip := big.NewInt(5) + txGasTipCap := big.NewInt(20) // Normal account's gas tip cap (higher than headerGasTip, but should be ignored) + txGasFeeCap := big.NewInt(50) + + // Create transaction from normal account + tx, err := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 21000, + GasTipCap: txGasTipCap, + GasFeeCap: txGasFeeCap, + Data: nil, + }), signer, testTxMsgNormalKey) + if err != nil { + t.Fatalf("failed to sign transaction: %v", err) + } + + // Convert transaction to message + msg, err := TransactionToMessage(tx, signer, baseFee, headerGasTip, stateDB) + if err != nil { + t.Fatalf("failed to convert transaction to message: %v", err) + } + + // Verify that normal account uses header's gas tip (not transaction's GasTipCap) + expectedGasTipCap := headerGasTip + if msg.GasTipCap.Cmp(expectedGasTipCap) != 0 { + t.Errorf("expected GasTipCap %v (headerGasTip), got %v (tx.GasTipCap was %v but should be ignored)", + expectedGasTipCap, msg.GasTipCap, txGasTipCap) + } +} + +// TestApplyTransactionAuthorizedAccount tests that authorized accounts pay actual gas fee based on their transaction's GasTipCap +func TestApplyTransactionAuthorizedAccount(t *testing.T) { + t.Parallel() + + stateDB := setupTestStateDB() + signer := types.LatestSignerForChainID(common.Big1) + + // Set up account balance for gas payment (enough for gas fee) + stateDB.AddBalance(testTxMsgAuthorizedAddr, uint256.MustFromBig(big.NewInt(10000000))) + + // Test parameters + baseFee := big.NewInt(100) + headerGasTip := big.NewInt(10) // Set the gas tip we want to test + txGasTipCap := big.NewInt(50) // Authorized account's gas tip cap (higher than headerGasTip) + txGasFeeCap := big.NewInt(200) + + // Create transaction from authorized account + tx, err := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(0), + Gas: 21000, + GasTipCap: txGasTipCap, + GasFeeCap: txGasFeeCap, + Data: nil, + }), signer, testTxMsgAuthorizedKey) + if err != nil { + t.Fatalf("failed to sign transaction: %v", err) + } + + // Create test header + header := &types.Header{ + Number: big.NewInt(1), + Time: uint64(time.Now().Unix()), + BaseFee: baseFee, + GasLimit: 30000000, + GasUsed: 0, + Coinbase: common.Address{}, + ParentHash: common.Hash{}, + Root: common.Hash{}, + TxHash: common.Hash{}, + ReceiptHash: common.Hash{}, + UncleHash: common.Hash{}, + Difficulty: big.NewInt(0), + } + + // Set up WBFTExtra in header.Extra for Anzeon + wbftExtra := &types.WBFTExtra{ + VanityData: make([]byte, types.IstanbulExtraVanity), + RandaoReveal: []byte{}, + PrevRound: 0, + PrevPreparedSeal: nil, + PrevCommittedSeal: nil, + Round: 0, + PreparedSeal: nil, + CommittedSeal: nil, + GasTip: new(big.Int).Set(headerGasTip), + EpochInfo: nil, + } + extraData, err := rlp.EncodeToBytes(wbftExtra) + if err != nil { + t.Fatalf("failed to encode WBFTExtra: %v", err) + } + header.Extra = extraData + + // Create chain config + config := ¶ms.ChainConfig{ + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + Anzeon: ¶ms.AnzeonConfig{ + SystemContracts: ¶ms.SystemContracts{ + NativeCoinAdapter: ¶ms.SystemContract{ + Address: params.DefaultNativeCoinAdapterAddress, + Version: params.DefaultNativeCoinAdapterVersion, + }, + }, + }, + } + + // Apply transaction + coinbase := common.Address{0x1} // Use a non-zero address for coinbase + gp := new(GasPool).AddGas(header.GasLimit) + usedGas := uint64(0) + receipt, err := ApplyTransaction(config, nil, &coinbase, gp, stateDB, header, tx, &usedGas, vm.Config{}) + if err != nil { + t.Fatalf("failed to apply transaction: %v", err) + } + + // Verify receipt status + if receipt.Status != types.ReceiptStatusSuccessful { + t.Errorf("expected transaction to succeed, got status %d", receipt.Status) + } + + msg, err := TransactionToMessage(tx, signer, baseFee, headerGasTip, stateDB) + if err != nil { + t.Fatalf("failed to convert transaction to message: %v", err) + } + + // Verify that authorized account uses transaction's GasTipCap + if msg.GasTipCap.Cmp(txGasTipCap) != 0 { + t.Errorf("expected GasTipCap %v, got %v", txGasTipCap, msg.GasTipCap) + } + + // Calculate expected effective gas price for authorized account + // Authorized account uses tx.GasTipCap, so effectiveGasPrice = baseFee + txGasTipCap + expectedEffectiveGasPrice := new(big.Int).Add(baseFee, txGasTipCap) + if txGasFeeCap.Cmp(expectedEffectiveGasPrice) < 0 { + t.Errorf("txGasFeeCap is less than expectedEffectiveGasPrice") + } + + actualEffectiveGasPrice := receipt.EffectiveGasPrice + if actualEffectiveGasPrice == nil { + t.Errorf("actualEffectiveGasPrice is nil") + } + + // Verify effective gas price matches expected value + if actualEffectiveGasPrice.Cmp(expectedEffectiveGasPrice) != 0 { + t.Errorf("expected effective gas price %v (baseFee %v + GasTipCap %v, capped by GasFeeCap %v), got %v", + expectedEffectiveGasPrice, baseFee, txGasTipCap, txGasFeeCap, actualEffectiveGasPrice) + } + + // Calculate and verify total gas fee paid + expectedTotalGasFee := new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), expectedEffectiveGasPrice) + actualTotalGasFee := new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), actualEffectiveGasPrice) + + if actualTotalGasFee.Cmp(expectedTotalGasFee) != 0 { + t.Errorf("expected total gas fee %v (GasUsed %d * effectiveGasPrice %v), got %v", + expectedTotalGasFee, receipt.GasUsed, expectedEffectiveGasPrice, actualTotalGasFee) + } + + t.Logf("Authorized account - GasUsed: %d, EffectiveGasPrice: %v, TotalGasFee: %v", + receipt.GasUsed, actualEffectiveGasPrice, actualTotalGasFee) +} + +// TestApplyTransactionNormalAccount tests that normal accounts pay actual gas fee based on header's gas tip +func TestApplyTransactionNormalAccount(t *testing.T) { + t.Parallel() + + stateDB := setupTestStateDB() + signer := types.LatestSignerForChainID(common.Big1) + + // Set up account balance for gas payment (enough for gas fee) + stateDB.AddBalance(testTxMsgNormalAddr, uint256.MustFromBig(big.NewInt(10000000))) + + // Test parameters + baseFee := big.NewInt(100) + headerGasTip := big.NewInt(10) // Set the gas tip we want to test + txGasTipCap := big.NewInt(50) // Normal account's gas tip cap (higher than headerGasTip, but should be ignored) + txGasFeeCap := big.NewInt(200) + + // Create transaction from normal account + tx, err := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + To: &common.Address{}, + Value: big.NewInt(0), + Gas: 21000, + GasTipCap: txGasTipCap, + GasFeeCap: txGasFeeCap, + Data: nil, + }), signer, testTxMsgNormalKey) + if err != nil { + t.Fatalf("failed to sign transaction: %v", err) + } + + // Create test header + header := &types.Header{ + Number: big.NewInt(1), + Time: uint64(time.Now().Unix()), + BaseFee: baseFee, + GasLimit: 30000000, + GasUsed: 0, + Coinbase: common.Address{}, + ParentHash: common.Hash{}, + Root: common.Hash{}, + TxHash: common.Hash{}, + ReceiptHash: common.Hash{}, + UncleHash: common.Hash{}, + Difficulty: big.NewInt(0), + } + + // Set up WBFTExtra in header.Extra for Anzeon + wbftExtra := &types.WBFTExtra{ + VanityData: make([]byte, types.IstanbulExtraVanity), + RandaoReveal: []byte{}, + PrevRound: 0, + PrevPreparedSeal: nil, + PrevCommittedSeal: nil, + Round: 0, + PreparedSeal: nil, + CommittedSeal: nil, + GasTip: new(big.Int).Set(headerGasTip), + EpochInfo: nil, + } + extraData, err := rlp.EncodeToBytes(wbftExtra) + if err != nil { + t.Fatalf("failed to encode WBFTExtra: %v", err) + } + header.Extra = extraData + + // Create chain config + config := ¶ms.ChainConfig{ + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + Anzeon: ¶ms.AnzeonConfig{ + SystemContracts: ¶ms.SystemContracts{ + NativeCoinAdapter: ¶ms.SystemContract{ + Address: params.DefaultNativeCoinAdapterAddress, + Version: params.DefaultNativeCoinAdapterVersion, + }, + }, + }, + } + + // Apply transaction + coinbase := common.Address{0x1} // Use a non-zero address for coinbase + gp := new(GasPool).AddGas(header.GasLimit) + usedGas := uint64(0) + receipt, err := ApplyTransaction(config, nil, &coinbase, gp, stateDB, header, tx, &usedGas, vm.Config{}) + if err != nil { + t.Fatalf("failed to apply transaction: %v", err) + } + + // Verify receipt status + if receipt.Status != types.ReceiptStatusSuccessful { + t.Errorf("expected transaction to succeed, got status %d", receipt.Status) + } + + msg, err := TransactionToMessage(tx, signer, baseFee, headerGasTip, stateDB) + if err != nil { + t.Fatalf("failed to convert transaction to message: %v", err) + } + + // Verify that normal account uses header's gas tip (not transaction's GasTipCap) + if msg.GasTipCap.Cmp(headerGasTip) != 0 { + t.Errorf("expected GasTipCap %v (headerGasTip), got %v (tx.GasTipCap was %v but should be ignored)", + headerGasTip, msg.GasTipCap, txGasTipCap) + } + + // Calculate expected effective gas price for normal account + // Normal account uses headerGasTip, so effectiveGasPrice = baseFee + headerGasTip + expectedEffectiveGasPrice := new(big.Int).Add(baseFee, headerGasTip) + if txGasFeeCap.Cmp(expectedEffectiveGasPrice) < 0 { + t.Errorf("txGasFeeCap is less than expectedEffectiveGasPrice") + } + + actualEffectiveGasPrice := tx.EffectiveGasPrice(baseFee, headerGasTip) + if actualEffectiveGasPrice == nil { + t.Errorf("actualEffectiveGasPrice is nil") + } + + // Verify effective gas price matches expected value + if actualEffectiveGasPrice.Cmp(expectedEffectiveGasPrice) != 0 { + t.Errorf("expected effective gas price %v (baseFee %v + headerGasTip %v, capped by GasFeeCap %v), got %v", + expectedEffectiveGasPrice, baseFee, headerGasTip, txGasFeeCap, actualEffectiveGasPrice) + } + + // Calculate and verify total gas fee paid + expectedTotalGasFee := new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), expectedEffectiveGasPrice) + actualTotalGasFee := new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), actualEffectiveGasPrice) + + if actualTotalGasFee.Cmp(expectedTotalGasFee) != 0 { + t.Errorf("expected total gas fee %v (GasUsed %d * effectiveGasPrice %v), got %v", + expectedTotalGasFee, receipt.GasUsed, expectedEffectiveGasPrice, actualTotalGasFee) + } + + t.Logf("Normal account - GasUsed: %d, EffectiveGasPrice: %v, TotalGasFee: %v", + receipt.GasUsed, actualEffectiveGasPrice, actualTotalGasFee) +} From b6534ed09eb34b59b0b51eae1f32463ab35df817 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Wed, 12 Nov 2025 17:51:25 +0900 Subject: [PATCH 08/20] chore: change comment --- core/state_processor_test.go | 6 ++++-- core/state_transition.go | 9 +++------ miner/ordering.go | 9 +++------ miner/ordering_test.go | 13 +++++++------ 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 24835a4a0..6312a68fd 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -451,9 +451,11 @@ func setupTestStateDB() *state.StateDB { stateDB.CreateAccount(addr) } - // TODO: Once StateAccount.Extra field is implemented, use stateDB.SetAccountExtra() instead of params.AuthorizedAccounts + // TODO(authorizeAddr): Once StateAccount.Extra field is implemented, use stateDB.SetAccountExtra() instead of params.AuthorizedAccounts // example: - // stateDB.SetAccountExtra(testTxMsgAuthorizedAddr, &types.AccountExtra{Authorized: true}) + // if stateDB != nil { + // stateDB.SetAuthorized(testTxMsgAuthorizedAddr) + // } // // For now, temporarily uses hardcoded list from protocol_params for testing. params.AuthorizedAccounts[testTxMsgAuthorizedAddr] = true diff --git a/core/state_transition.go b/core/state_transition.go index 086f30735..d31455f68 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -153,15 +153,12 @@ type Message struct { // checkIsAuthorized checks if an account is authorized. func checkIsAuthorized(addr common.Address, stateDB *state.StateDB) bool { - // TODO: Once StateAccount.Extra field is implemented, read from stateDB: + // TODO(authorizeAddr): Once StateAccount.Extra field is implemented, read from stateDB: // example: // if stateDB != nil { - // account := stateDB.GetAccount(addr) - // if account != nil { - // // Check Extra field for authorized flag - // return account.IsAuthorized() - // } + // return stateDB.IsAuthorized(addr) // } + // return false // For now, use hardcoded list from protocol_params return params.AuthorizedAccounts[addr] diff --git a/miner/ordering.go b/miner/ordering.go index 10e69207d..1940d4648 100644 --- a/miner/ordering.go +++ b/miner/ordering.go @@ -120,15 +120,12 @@ func checkIsAuthorized(addr common.Address, anzeonEnabled bool, stateDB *state.S return false } - // TODO: Once StateAccount.Extra field is implemented, read from stateDB: + // TODO(authorizeAddr): Once StateAccount.Extra field is implemented, read from stateDB: // example: // if stateDB != nil { - // account := stateDB.GetAccount(addr) - // if account != nil { - // // Check Extra field for authorized flag - // return account.IsAuthorized() - // } + // return stateDB.IsAuthorized(addr) // } + // return false // For now, use hardcoded list from protocol_params return params.AuthorizedAccounts[addr] diff --git a/miner/ordering_test.go b/miner/ordering_test.go index 1aaf7f757..237e534ad 100644 --- a/miner/ordering_test.go +++ b/miner/ordering_test.go @@ -70,14 +70,15 @@ func setupTestStateDB() *state.StateDB { stateDB.CreateAccount(addr) } - // TO DO : Once StateAccount.Extra field is implemented, use stateDB.SetAccountExtra() instead of params.AuthorizedAccounts + // TODO(authorizeAddr) : Once StateAccount.Extra field is implemented, use stateDB.SetAccountExtra() instead of params.AuthorizedAccounts // example: - // stateDB.SetAccountExtra(testAuthorizedAddr1, &types.AccountExtra{Authorized: true}) - // stateDB.SetAccountExtra(testAuthorizedAddr2, &types.AccountExtra{Authorized: true}) - // stateDB.SetAccountExtra(testAuthorizedAddr3, &types.AccountExtra{Authorized: true}) - //} + // if stateDB != nil { + // stateDB.SetAuthorized(testAuthorizedAddr1) + // stateDB.SetAuthorized(testAuthorizedAddr2) + // stateDB.SetAuthorized(testAuthorizedAddr3) + // } - // TO DO : if Once StateAccount.Extra field is implemented, remove this code + // TODO(authorizeAddr) : if Once StateAccount.Extra field is implemented, remove this code authorizedAddrs := []common.Address{ testAuthorizedAddr1, testAuthorizedAddr2, testAuthorizedAddr3, } From d4cff59ab38f0d4ed2e97db3ee4a2488f1c62273 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Wed, 12 Nov 2025 17:54:47 +0900 Subject: [PATCH 09/20] chore: remove comment --- miner/worker.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/miner/worker.go b/miner/worker.go index c2f75a7fa..30c74ca41 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -381,7 +381,6 @@ func (w *worker) setGasTipUnsafe(tip *big.Int) bool { } w.tip = uint256.MustFromBig(tip) - // TODO: This part will be refactored to implement Minter transaction prioritization. // Update txPool's gas tip to filter pending transactions accordingly w.eth.TxPool().SetGasTip(tip) @@ -1472,7 +1471,6 @@ func totalFees(c *params.ChainConfig, block *types.Block, receipts []*types.Rece for i, tx := range block.Transactions() { var minerFee *big.Int if c.AnzeonEnabled() { - // TODO: EffectiveGasPrice will be removed and replaced with the updated EffectiveGasTip function var headerGasTip *big.Int if block.Header() != nil && block.Header().GasTip() != nil { headerGasTip = block.Header().GasTip() From 8116c2812cbd1fc3b8b2bd510b49cb34843e1e09 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Thu, 13 Nov 2025 15:28:24 +0900 Subject: [PATCH 10/20] fix: adjust tip calculation logic --- core/state_processor.go | 9 ++++++++- core/state_transition.go | 25 +++++++++++-------------- eth/gasprice/feehistory.go | 18 +++++++++++++++++- eth/gasprice/gasprice.go | 36 ++++++++++++++++++++++++++++++++---- miner/ordering.go | 27 ++++++++------------------- miner/worker.go | 8 +++----- 6 files changed, 79 insertions(+), 44 deletions(-) diff --git a/core/state_processor.go b/core/state_processor.go index 6eae1d318..96caa78c6 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -154,7 +154,14 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta receipt.BlockNumber = blockNumber receipt.TransactionIndex = uint(statedb.TxIndex()) if config.AnzeonEnabled() { - if checkIsAuthorized(msg.From, statedb) { + // TODO(authorizeAddr): Once StateAccount.Extra field is implemented, read from stateDB: + // example: + // if statedb.IsAuthorized(msg.From) { + // receipt.EffectiveGasPrice = tx.EffectiveGasPrice(baseFee, nil) + // } + + // For now, use hardcoded list from protocol_params + if params.AuthorizedAccounts[msg.From] { receipt.EffectiveGasPrice = tx.EffectiveGasPrice(baseFee, nil) } } diff --git a/core/state_transition.go b/core/state_transition.go index d31455f68..42f4c4e78 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -151,19 +151,6 @@ type Message struct { SkipAccountChecks bool } -// checkIsAuthorized checks if an account is authorized. -func checkIsAuthorized(addr common.Address, stateDB *state.StateDB) bool { - // TODO(authorizeAddr): Once StateAccount.Extra field is implemented, read from stateDB: - // example: - // if stateDB != nil { - // return stateDB.IsAuthorized(addr) - // } - // return false - - // For now, use hardcoded list from protocol_params - return params.AuthorizedAccounts[addr] -} - // TransactionToMessage converts a transaction into a Message. func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee, headerGasTip *big.Int, statedb *state.StateDB) (*Message, error) { from, err := types.Sender(s, tx) @@ -175,7 +162,17 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee, header if headerGasTip != nil { // If Anzeon is enabled, and the sender is authorized, use the tx's tx.GasTipCap() // Otherwise, use the header's gas tip - if !checkIsAuthorized(from, statedb) { + + // TODO(authorizeAddr): Once StateAccount.Extra field is implemented, read from stateDB: + // example: + // if statedb != nil { + // if !statedb.IsAuthorized(from) { + // gasTipCap = new(big.Int).Set(headerGasTip) + // } + // } + + // For now, use hardcoded list from protocol_params + if !params.AuthorizedAccounts[from] { gasTipCap = new(big.Int).Set(headerGasTip) } } diff --git a/eth/gasprice/feehistory.go b/eth/gasprice/feehistory.go index d657eb6d9..239b7d35b 100644 --- a/eth/gasprice/feehistory.go +++ b/eth/gasprice/feehistory.go @@ -106,9 +106,25 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) { return } + var headerGasTip *big.Int + if bf.header != nil && bf.header.GasTip() != nil { + headerGasTip = new(big.Int).Set(bf.header.GasTip()) + } + sorter := make([]txGasAndReward, len(bf.block.Transactions())) for i, tx := range bf.block.Transactions() { - reward, _ := tx.EffectiveGasTip(bf.block.BaseFee()) + var reward *big.Int + // Anzeon enablement cannot be checked here. + // If header.GasTip() is not nil, it indicates Anzeon is enabled; + // otherwise, it’s disabled. + if headerGasTip != nil { + // stateDB unavailable → can't check authorized account + // Therefore, use the receipt's effectiveGasPrice to derive the TIP. + // (i.e., EffectiveGasTip = effectiveGasPrice - baseFee) + reward = new(big.Int).Sub(bf.receipts[i].EffectiveGasPrice, bf.block.BaseFee()) + } else { + reward, _ = tx.EffectiveGasTip(bf.block.BaseFee()) + } sorter[i] = txGasAndReward{gasUsed: bf.receipts[i].GasUsed, reward: reward} } slices.SortStableFunc(sorter, func(a, b txGasAndReward) int { diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index 1a8f0abd2..880f54ef0 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -244,22 +244,50 @@ func (oracle *Oracle) getBlockValues(ctx context.Context, blockNum uint64, limit } signer := types.MakeSigner(oracle.backend.ChainConfig(), block.Number(), block.Time()) + // Get receipts for tip calculation when headerGasTip is available + receipts, err := oracle.backend.GetReceipts(ctx, block.Hash()) + if err != nil { + receipts = nil + } + // Sort the transaction by effective tip in ascending sort. txs := block.Transactions() + baseFee := block.BaseFee() + + var headerGasTip *big.Int + if block.Header() != nil && block.Header().GasTip() != nil { + headerGasTip = new(big.Int).Set(block.Header().GasTip()) + } + + // Create a map to store tip for each transaction + tipMap := make(map[common.Hash]*big.Int) + for i, tx := range txs { + var tip *big.Int + if headerGasTip != nil && receipts != nil && i < len(receipts) { + // stateDB unavailable → can't check authorized account + // Therefore, use the receipt's effectiveGasPrice to derive the TIP. + // (i.e., EffectiveGasTip = effectiveGasPrice - baseFee) + tip = new(big.Int).Sub(receipts[i].EffectiveGasPrice, baseFee) + } else { + tip, _ = tx.EffectiveGasTip(baseFee) + } + tipMap[tx.Hash()] = tip + } + sortedTxs := make([]*types.Transaction, len(txs)) copy(sortedTxs, txs) - baseFee := block.BaseFee() + slices.SortFunc(sortedTxs, func(a, b *types.Transaction) int { // It's okay to discard the error because a tx would never be // accepted into a block with an invalid effective tip. - tip1, _ := a.EffectiveGasTip(baseFee) - tip2, _ := b.EffectiveGasTip(baseFee) + tip1 := tipMap[a.Hash()] + tip2 := tipMap[b.Hash()] return tip1.Cmp(tip2) }) var prices []*big.Int for _, tx := range sortedTxs { - tip, _ := tx.EffectiveGasTip(baseFee) + tip := tipMap[tx.Hash()] if ignoreUnder != nil && tip.Cmp(ignoreUnder) == -1 { continue } diff --git a/miner/ordering.go b/miner/ordering.go index 1940d4648..344dc6cfb 100644 --- a/miner/ordering.go +++ b/miner/ordering.go @@ -113,24 +113,6 @@ func (s *txByPriceAndTime) Pop() interface{} { return x } -// checkIsAuthorized checks if an account is authorized. -func checkIsAuthorized(addr common.Address, anzeonEnabled bool, stateDB *state.StateDB) bool { - // If Anzeon is not enabled, no account is authorized - if !anzeonEnabled { - return false - } - - // TODO(authorizeAddr): Once StateAccount.Extra field is implemented, read from stateDB: - // example: - // if stateDB != nil { - // return stateDB.IsAuthorized(addr) - // } - // return false - - // For now, use hardcoded list from protocol_params - return params.AuthorizedAccounts[addr] -} - // transactionsByPriceAndNonce represents a set of transactions that can return // transactions in a profit-maximizing sorted order, while supporting removing // entire batches of transactions for non-executable accounts. @@ -156,7 +138,14 @@ func newTransactionsByPriceAndNonce(signer types.Signer, txs map[common.Address] isAuthorizedMap := make(map[common.Address]bool, len(txs)) if anzeonEnabled { for from := range txs { - isAuthorizedMap[from] = checkIsAuthorized(from, anzeonEnabled, stateDB) + // TODO(authorizeAddr): Once StateAccount.Extra field is implemented, read from stateDB: + // example: + // if stateDB != nil { + // isAuthorizedMap[from] = stateDB.IsAuthorized(from) + // } + + // For now, use hardcoded list from protocol_params + isAuthorizedMap[from] = params.AuthorizedAccounts[from] } } diff --git a/miner/worker.go b/miner/worker.go index 30c74ca41..ac3371616 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -1471,11 +1471,9 @@ func totalFees(c *params.ChainConfig, block *types.Block, receipts []*types.Rece for i, tx := range block.Transactions() { var minerFee *big.Int if c.AnzeonEnabled() { - var headerGasTip *big.Int - if block.Header() != nil && block.Header().GasTip() != nil { - headerGasTip = block.Header().GasTip() - } - minerFee = tx.EffectiveGasPrice(block.BaseFee(), headerGasTip) + // When Anzeon is enabled, the baseFee is not burned. + // Instead, it is rewarded to the miner, making minerFee = baseFee + gasTip + minerFee = receipts[i].EffectiveGasPrice } else { minerFee, _ = tx.EffectiveGasTip(block.BaseFee()) } From f211825280f899377f2dd55b98e753900bc304f7 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Thu, 13 Nov 2025 15:59:33 +0900 Subject: [PATCH 11/20] fix: check EffectiveGasPrice --- core/types/receipt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/types/receipt.go b/core/types/receipt.go index 2ecd1c571..4d65fd58d 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -334,7 +334,7 @@ func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, nu // The transaction type and hash can be retrieved from the transaction itself rs[i].Type = txs[i].Type() rs[i].TxHash = txs[i].Hash() - if rs[i].EffectiveGasPrice == nil { + if rs[i].EffectiveGasPrice == nil || rs[i].EffectiveGasPrice.Cmp(big.NewInt(0)) == 0 { rs[i].EffectiveGasPrice = txs[i].inner.effectiveGasPrice(baseFee, headerGasTip) } // EIP-4844 blob transaction fields From b0bb2cefefd00281d54073d923b7940f9b7cc197 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Thu, 13 Nov 2025 16:03:24 +0900 Subject: [PATCH 12/20] test: add receipt derivation test for authorized account --- core/types/receipt_test.go | 173 ++++++++++++++++++++++++++++++++++++- eth/gasprice/feehistory.go | 2 +- 2 files changed, 170 insertions(+), 5 deletions(-) diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go index 9aa43f696..4c079ba07 100644 --- a/core/types/receipt_test.go +++ b/core/types/receipt_test.go @@ -125,8 +125,8 @@ var ( Nonce: 5, Value: big.NewInt(5), Gas: 5, - GasTipCap: big.NewInt(1000), - GasFeeCap: big.NewInt(2000), + GasTipCap: big.NewInt(2000), + GasFeeCap: big.NewInt(4000), }), // EIP-4844 transactions. NewTx(&BlobTx{ @@ -144,8 +144,8 @@ var ( Nonce: 7, Value: uint256.NewInt(7), Gas: 7, - GasTipCap: uint256.NewInt(1000), - GasFeeCap: uint256.NewInt(2000), + GasTipCap: uint256.NewInt(1600), + GasFeeCap: uint256.NewInt(3000), BlobFeeCap: uint256.NewInt(100077), BlobHashes: []common.Hash{{}, {}, {}}, }), @@ -294,6 +294,144 @@ var ( TransactionIndex: 6, }, } + authorizedReceipts = Receipts{ + &Receipt{ + Status: ReceiptStatusFailed, + CumulativeGasUsed: 1, + Logs: []*Log{ + { + Address: common.BytesToAddress([]byte{0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + // derived fields: + BlockNumber: blockNumber.Uint64(), + TxHash: txs[0].Hash(), + TxIndex: 0, + BlockHash: blockHash, + Index: 0, + }, + { + Address: common.BytesToAddress([]byte{0x01, 0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + // derived fields: + BlockNumber: blockNumber.Uint64(), + TxHash: txs[0].Hash(), + TxIndex: 0, + BlockHash: blockHash, + Index: 1, + }, + }, + // derived fields: + TxHash: txs[0].Hash(), + ContractAddress: common.HexToAddress("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"), + GasUsed: 1, + EffectiveGasPrice: big.NewInt(11), + BlockHash: blockHash, + BlockNumber: blockNumber, + TransactionIndex: 0, + }, + &Receipt{ + PostState: common.Hash{2}.Bytes(), + CumulativeGasUsed: 3, + Logs: []*Log{ + { + Address: common.BytesToAddress([]byte{0x22}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + // derived fields: + BlockNumber: blockNumber.Uint64(), + TxHash: txs[1].Hash(), + TxIndex: 1, + BlockHash: blockHash, + Index: 2, + }, + { + Address: common.BytesToAddress([]byte{0x02, 0x22}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + // derived fields: + BlockNumber: blockNumber.Uint64(), + TxHash: txs[1].Hash(), + TxIndex: 1, + BlockHash: blockHash, + Index: 3, + }, + }, + // derived fields: + TxHash: txs[1].Hash(), + GasUsed: 2, + EffectiveGasPrice: big.NewInt(22), + BlockHash: blockHash, + BlockNumber: blockNumber, + TransactionIndex: 1, + }, + &Receipt{ + Type: AccessListTxType, + PostState: common.Hash{3}.Bytes(), + CumulativeGasUsed: 6, + Logs: []*Log{}, + // derived fields: + TxHash: txs[2].Hash(), + GasUsed: 3, + EffectiveGasPrice: big.NewInt(33), + BlockHash: blockHash, + BlockNumber: blockNumber, + TransactionIndex: 2, + }, + &Receipt{ + Type: DynamicFeeTxType, + PostState: common.Hash{4}.Bytes(), + CumulativeGasUsed: 10, + Logs: []*Log{}, + // derived fields: + TxHash: txs[3].Hash(), + GasUsed: 4, + EffectiveGasPrice: big.NewInt(2000), + BlockHash: blockHash, + BlockNumber: blockNumber, + TransactionIndex: 3, + }, + &Receipt{ + Type: DynamicFeeTxType, + PostState: common.Hash{5}.Bytes(), + CumulativeGasUsed: 15, + Logs: []*Log{}, + // derived fields: + TxHash: txs[4].Hash(), + GasUsed: 5, + EffectiveGasPrice: big.NewInt(3000), + BlockHash: blockHash, + BlockNumber: blockNumber, + TransactionIndex: 4, + }, + &Receipt{ + Type: BlobTxType, + PostState: common.Hash{6}.Bytes(), + CumulativeGasUsed: 21, + Logs: []*Log{}, + // derived fields: + TxHash: txs[5].Hash(), + GasUsed: 6, + EffectiveGasPrice: big.NewInt(2000), + BlobGasUsed: params.BlobTxBlobGasPerBlob, + BlobGasPrice: big.NewInt(920), + BlockHash: blockHash, + BlockNumber: blockNumber, + TransactionIndex: 5, + }, + &Receipt{ + Type: BlobTxType, + PostState: common.Hash{7}.Bytes(), + CumulativeGasUsed: 28, + Logs: []*Log{}, + // derived fields: + TxHash: txs[6].Hash(), + GasUsed: 7, + EffectiveGasPrice: big.NewInt(2600), + BlobGasUsed: 3 * params.BlobTxBlobGasPerBlob, + BlobGasPrice: big.NewInt(920), + BlockHash: blockHash, + BlockNumber: blockNumber, + TransactionIndex: 6, + }, + } ) func TestDecodeEmptyTypedReceipt(t *testing.T) { @@ -333,6 +471,33 @@ func TestDeriveFields(t *testing.T) { } } +// Tests that receipt data can be correctly derived for authorized accounts +func TestDeriveFieldsAuthorizedAccount(t *testing.T) { + // Re-derive receipts. + basefee := big.NewInt(1000) + blobGasPrice := big.NewInt(920) + derivedReceipts := clearComputedFieldsOnReceipts(authorizedReceipts) + err := Receipts(derivedReceipts).DeriveFields(params.TestChainConfig, blockHash, blockNumber.Uint64(), blockTime, basefee, nil, blobGasPrice, txs) + if err != nil { + t.Fatalf("DeriveFields(...) = %v, want ", err) + } + + // Check diff of receipts against derivedReceipts. + r1, err := json.MarshalIndent(authorizedReceipts, "", " ") + if err != nil { + t.Fatal("error marshaling input receipts:", err) + } + + r2, err := json.MarshalIndent(derivedReceipts, "", " ") + if err != nil { + t.Fatal("error marshaling derived receipts:", err) + } + d := diff.Diff(string(r1), string(r2)) + if d != "" { + t.Fatal("receipts differ:", d) + } +} + // Test that we can marshal/unmarshal receipts to/from json without errors. // This also confirms that our test receipts contain all the required fields. func TestReceiptJSON(t *testing.T) { diff --git a/eth/gasprice/feehistory.go b/eth/gasprice/feehistory.go index 239b7d35b..559315876 100644 --- a/eth/gasprice/feehistory.go +++ b/eth/gasprice/feehistory.go @@ -115,7 +115,7 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) { for i, tx := range bf.block.Transactions() { var reward *big.Int // Anzeon enablement cannot be checked here. - // If header.GasTip() is not nil, it indicates Anzeon is enabled; + // If header.GasTip() is not nil, Anzeon is determined to be enabled. // otherwise, it’s disabled. if headerGasTip != nil { // stateDB unavailable → can't check authorized account From 845d9d45c127fc0869852e5f1f338f8a7f277e6e Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Thu, 13 Nov 2025 16:13:30 +0900 Subject: [PATCH 13/20] refactor: use stateDB to determine authorized accounts instead of temporary code --- core/state_processor.go | 13 ++----------- core/state_processor_test.go | 11 +++-------- core/state_transition.go | 12 +----------- miner/ordering.go | 12 +++--------- miner/ordering_test.go | 19 ++++--------------- 5 files changed, 13 insertions(+), 54 deletions(-) diff --git a/core/state_processor.go b/core/state_processor.go index 96caa78c6..a5f6ac352 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -153,17 +153,8 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta receipt.BlockHash = blockHash receipt.BlockNumber = blockNumber receipt.TransactionIndex = uint(statedb.TxIndex()) - if config.AnzeonEnabled() { - // TODO(authorizeAddr): Once StateAccount.Extra field is implemented, read from stateDB: - // example: - // if statedb.IsAuthorized(msg.From) { - // receipt.EffectiveGasPrice = tx.EffectiveGasPrice(baseFee, nil) - // } - - // For now, use hardcoded list from protocol_params - if params.AuthorizedAccounts[msg.From] { - receipt.EffectiveGasPrice = tx.EffectiveGasPrice(baseFee, nil) - } + if config.AnzeonEnabled() && statedb.IsAuthorized(msg.From) { + receipt.EffectiveGasPrice = tx.EffectiveGasPrice(baseFee, nil) } return receipt, err } diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 6312a68fd..a2214440f 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -451,14 +451,9 @@ func setupTestStateDB() *state.StateDB { stateDB.CreateAccount(addr) } - // TODO(authorizeAddr): Once StateAccount.Extra field is implemented, use stateDB.SetAccountExtra() instead of params.AuthorizedAccounts - // example: - // if stateDB != nil { - // stateDB.SetAuthorized(testTxMsgAuthorizedAddr) - // } - // - // For now, temporarily uses hardcoded list from protocol_params for testing. - params.AuthorizedAccounts[testTxMsgAuthorizedAddr] = true + if stateDB != nil { + stateDB.SetAuthorized(testTxMsgAuthorizedAddr) + } return stateDB } diff --git a/core/state_transition.go b/core/state_transition.go index 42f4c4e78..6f9cdeff6 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -162,17 +162,7 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee, header if headerGasTip != nil { // If Anzeon is enabled, and the sender is authorized, use the tx's tx.GasTipCap() // Otherwise, use the header's gas tip - - // TODO(authorizeAddr): Once StateAccount.Extra field is implemented, read from stateDB: - // example: - // if statedb != nil { - // if !statedb.IsAuthorized(from) { - // gasTipCap = new(big.Int).Set(headerGasTip) - // } - // } - - // For now, use hardcoded list from protocol_params - if !params.AuthorizedAccounts[from] { + if statedb != nil && !statedb.IsAuthorized(from) { gasTipCap = new(big.Int).Set(headerGasTip) } } diff --git a/miner/ordering.go b/miner/ordering.go index 344dc6cfb..23f0e7e17 100644 --- a/miner/ordering.go +++ b/miner/ordering.go @@ -24,7 +24,6 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) @@ -138,14 +137,9 @@ func newTransactionsByPriceAndNonce(signer types.Signer, txs map[common.Address] isAuthorizedMap := make(map[common.Address]bool, len(txs)) if anzeonEnabled { for from := range txs { - // TODO(authorizeAddr): Once StateAccount.Extra field is implemented, read from stateDB: - // example: - // if stateDB != nil { - // isAuthorizedMap[from] = stateDB.IsAuthorized(from) - // } - - // For now, use hardcoded list from protocol_params - isAuthorizedMap[from] = params.AuthorizedAccounts[from] + if stateDB != nil { + isAuthorizedMap[from] = stateDB.IsAuthorized(from) + } } } diff --git a/miner/ordering_test.go b/miner/ordering_test.go index 237e534ad..ba53064fb 100644 --- a/miner/ordering_test.go +++ b/miner/ordering_test.go @@ -29,7 +29,6 @@ import ( "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) @@ -70,20 +69,10 @@ func setupTestStateDB() *state.StateDB { stateDB.CreateAccount(addr) } - // TODO(authorizeAddr) : Once StateAccount.Extra field is implemented, use stateDB.SetAccountExtra() instead of params.AuthorizedAccounts - // example: - // if stateDB != nil { - // stateDB.SetAuthorized(testAuthorizedAddr1) - // stateDB.SetAuthorized(testAuthorizedAddr2) - // stateDB.SetAuthorized(testAuthorizedAddr3) - // } - - // TODO(authorizeAddr) : if Once StateAccount.Extra field is implemented, remove this code - authorizedAddrs := []common.Address{ - testAuthorizedAddr1, testAuthorizedAddr2, testAuthorizedAddr3, - } - for _, addr := range authorizedAddrs { - params.AuthorizedAccounts[addr] = true + if stateDB != nil { + stateDB.SetAuthorized(testAuthorizedAddr1) + stateDB.SetAuthorized(testAuthorizedAddr2) + stateDB.SetAuthorized(testAuthorizedAddr3) } return stateDB } From 54deb6de9e2bcc1b2c5ba1310a35e52a0b2e0936 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Thu, 13 Nov 2025 16:44:30 +0900 Subject: [PATCH 14/20] fix: handle nil minerFee when EffectiveGasPrice is missing --- miner/worker.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/miner/worker.go b/miner/worker.go index ac3371616..72d727ccd 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -1470,10 +1470,20 @@ func totalFees(c *params.ChainConfig, block *types.Block, receipts []*types.Rece feesWei := new(big.Int) for i, tx := range block.Transactions() { var minerFee *big.Int + // When Anzeon is enabled, the baseFee is not burned. + // Instead, it is rewarded to the miner, making minerFee = baseFee + gasTip if c.AnzeonEnabled() { - // When Anzeon is enabled, the baseFee is not burned. - // Instead, it is rewarded to the miner, making minerFee = baseFee + gasTip - minerFee = receipts[i].EffectiveGasPrice + if receipts[i].EffectiveGasPrice == nil || receipts[i].EffectiveGasPrice.Cmp(big.NewInt(0)) == 0 { + // not authorized account + var headerGasTip *big.Int + if block.Header() != nil && block.Header().GasTip() != nil { + headerGasTip = new(big.Int).Set(block.Header().GasTip()) + } + minerFee = tx.EffectiveGasPrice(block.BaseFee(), headerGasTip) + } else { + // authorized account + minerFee = receipts[i].EffectiveGasPrice + } } else { minerFee, _ = tx.EffectiveGasTip(block.BaseFee()) } From d3fff730df83f7e66db9d6d14c4a190f4e16419c Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Fri, 14 Nov 2025 12:11:13 +0900 Subject: [PATCH 15/20] Add StateReader interface for IsAuthorized checks in receipts - Add StateReader interface to check authorized accounts - Pass StateReader to ReadReceipts and DeriveFields - Use IsAuthorized to determine gasTip in receipt derivation - Update all call sites to pass StateReader --- core/bench_test.go | 2 +- core/blockchain.go | 7 ++++++- core/blockchain_reader.go | 8 +++++++- core/blockchain_test.go | 12 ++++++------ core/chain_makers.go | 7 ++++++- core/rawdb/accessors_chain.go | 8 ++++++-- core/rawdb/accessors_chain_test.go | 10 +++++----- core/rawdb/accessors_indexes.go | 4 ++-- core/state_processor.go | 3 --- core/types/receipt.go | 22 +++++++++++++++++++--- core/types/receipt_test.go | 5 +++-- eth/filters/filter_system_test.go | 9 ++++++++- internal/ethapi/api_test.go | 6 +++++- miner/worker.go | 29 ++++++++++++++++++----------- 14 files changed, 92 insertions(+), 40 deletions(-) diff --git a/core/bench_test.go b/core/bench_test.go index 97713868a..4351814b2 100644 --- a/core/bench_test.go +++ b/core/bench_test.go @@ -322,7 +322,7 @@ func benchReadChain(b *testing.B, full bool, count uint64) { if full { hash := header.Hash() rawdb.ReadBody(db, hash, n) - rawdb.ReadReceipts(db, hash, n, header.Time, chain.Config()) + rawdb.ReadReceipts(db, hash, n, header.Time, chain.Config(), nil) } } chain.Stop() diff --git a/core/blockchain.go b/core/blockchain.go index 767dcc074..11c9e5bbe 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2120,7 +2120,12 @@ func (bc *BlockChain) collectLogs(b *types.Block, removed bool) []*types.Log { headerGasTip = b.Header().GasTip() } - if err := receipts.DeriveFields(bc.chainConfig, b.Hash(), b.NumberU64(), b.Time(), b.BaseFee(), headerGasTip, blobGasPrice, b.Transactions()); err != nil { + var stateReader rawdb.StateReader + if statedb, err := bc.StateAt(b.Root()); err == nil { + stateReader = statedb + } + + if err := receipts.DeriveFields(bc.chainConfig, b.Hash(), b.NumberU64(), b.Time(), b.BaseFee(), headerGasTip, blobGasPrice, b.Transactions(), stateReader); err != nil { log.Error("Failed to derive block receipts fields", "hash", b.Hash(), "number", b.NumberU64(), "err", err) } var logs []*types.Log diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index b16fa2b82..25040ee8b 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -227,7 +227,13 @@ func (bc *BlockChain) GetReceiptsByHash(hash common.Hash) types.Receipts { if header == nil { return nil } - receipts := rawdb.ReadReceipts(bc.db, hash, *number, header.Time, bc.chainConfig) + + var stateReader rawdb.StateReader + if statedb, err := bc.StateAt(header.Root); err == nil { + stateReader = statedb + } + + receipts := rawdb.ReadReceipts(bc.db, hash, *number, header.Time, bc.chainConfig, stateReader) if receipts == nil { return nil } diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 0b96667b1..782fc9cb4 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -927,9 +927,9 @@ func testFastVsFullChains(t *testing.T, scheme string) { } // Check receipts. - freceipts := rawdb.ReadReceipts(fastDb, hash, num, time, fast.Config()) - anreceipts := rawdb.ReadReceipts(ancientDb, hash, num, time, fast.Config()) - areceipts := rawdb.ReadReceipts(archiveDb, hash, num, time, fast.Config()) + freceipts := rawdb.ReadReceipts(fastDb, hash, num, time, fast.Config(), nil) + anreceipts := rawdb.ReadReceipts(ancientDb, hash, num, time, fast.Config(), nil) + areceipts := rawdb.ReadReceipts(archiveDb, hash, num, time, fast.Config(), nil) if types.DeriveSha(freceipts, trie.NewStackTrie(nil)) != types.DeriveSha(areceipts, trie.NewStackTrie(nil)) { t.Errorf("block #%d [%x]: receipts mismatch: fastdb %v, ancientdb %v, archivedb %v", num, hash, freceipts, anreceipts, areceipts) } @@ -1171,7 +1171,7 @@ func testChainTxReorgs(t *testing.T, scheme string) { if txn, _, _, _ := rawdb.ReadTransaction(db, tx.Hash()); txn != nil { t.Errorf("drop %d: tx %v found while shouldn't have been", i, txn) } - if rcpt, _, _, _ := rawdb.ReadReceipt(db, tx.Hash(), blockchain.Config()); rcpt != nil { + if rcpt, _, _, _ := rawdb.ReadReceipt(db, tx.Hash(), blockchain.Config(), nil); rcpt != nil { t.Errorf("drop %d: receipt %v found while shouldn't have been", i, rcpt) } } @@ -1180,7 +1180,7 @@ func testChainTxReorgs(t *testing.T, scheme string) { if txn, _, _, _ := rawdb.ReadTransaction(db, tx.Hash()); txn == nil { t.Errorf("add %d: expected tx to be found", i) } - if rcpt, _, _, _ := rawdb.ReadReceipt(db, tx.Hash(), blockchain.Config()); rcpt == nil { + if rcpt, _, _, _ := rawdb.ReadReceipt(db, tx.Hash(), blockchain.Config(), nil); rcpt == nil { t.Errorf("add %d: expected receipt to be found", i) } } @@ -1189,7 +1189,7 @@ func testChainTxReorgs(t *testing.T, scheme string) { if txn, _, _, _ := rawdb.ReadTransaction(db, tx.Hash()); txn == nil { t.Errorf("share %d: expected tx to be found", i) } - if rcpt, _, _, _ := rawdb.ReadReceipt(db, tx.Hash(), blockchain.Config()); rcpt == nil { + if rcpt, _, _, _ := rawdb.ReadReceipt(db, tx.Hash(), blockchain.Config(), nil); rcpt == nil { t.Errorf("share %d: expected receipt to be found", i) } } diff --git a/core/chain_makers.go b/core/chain_makers.go index da594b9dd..0715b23c0 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -414,7 +414,12 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse headerGasTip = block.Header().GasTip() } - if err := receipts.DeriveFields(config, block.Hash(), block.NumberU64(), block.Time(), block.BaseFee(), headerGasTip, blobGasPrice, txs); err != nil { + var stateReader rawdb.StateReader + if statedb, err := cm.StateAt(block.Root()); err == nil { + stateReader = statedb + } + + if err := receipts.DeriveFields(config, block.Hash(), block.NumberU64(), block.Time(), block.BaseFee(), headerGasTip, blobGasPrice, txs, stateReader); err != nil { panic(err) } diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 93546b75d..b50753d27 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -35,6 +35,10 @@ import ( "golang.org/x/exp/slices" ) +// StateReader is an alias for types.StateReader to maintain backward compatibility. +// The interface is defined in types package to avoid circular dependencies. +type StateReader = types.StateReader + // ReadCanonicalHash retrieves the hash assigned to a canonical block number. func ReadCanonicalHash(db ethdb.Reader, number uint64) common.Hash { var data []byte @@ -623,7 +627,7 @@ func ReadRawReceipts(db ethdb.Reader, hash common.Hash, number uint64) types.Rec // The current implementation populates these metadata fields by reading the receipts' // corresponding block body, so if the block body is not found it will return nil even // if the receipt itself is stored. -func ReadReceipts(db ethdb.Reader, hash common.Hash, number uint64, time uint64, config *params.ChainConfig) types.Receipts { +func ReadReceipts(db ethdb.Reader, hash common.Hash, number uint64, time uint64, config *params.ChainConfig, stateReader StateReader) types.Receipts { // We're deriving many fields from the block body, retrieve beside the receipt receipts := ReadRawReceipts(db, hash, number) if receipts == nil { @@ -654,7 +658,7 @@ func ReadReceipts(db ethdb.Reader, hash common.Hash, number uint64, time uint64, headerGasTip = new(big.Int).Set(header.GasTip()) } - if err := receipts.DeriveFields(config, hash, number, time, baseFee, headerGasTip, blobGasPrice, body.Transactions); err != nil { + if err := receipts.DeriveFields(config, hash, number, time, baseFee, headerGasTip, blobGasPrice, body.Transactions, stateReader); err != nil { log.Error("Failed to derive block receipts fields", "hash", hash, "number", number, "err", err) return nil } diff --git a/core/rawdb/accessors_chain_test.go b/core/rawdb/accessors_chain_test.go index a7ceb7299..51a76d076 100644 --- a/core/rawdb/accessors_chain_test.go +++ b/core/rawdb/accessors_chain_test.go @@ -379,7 +379,7 @@ func TestBlockReceiptStorage(t *testing.T) { // Check that no receipt entries are in a pristine database hash := common.BytesToHash([]byte{0x03, 0x14}) - if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig); len(rs) != 0 { + if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig, nil); len(rs) != 0 { t.Fatalf("non existent receipts returned: %v", rs) } // Insert the body that corresponds to the receipts @@ -387,7 +387,7 @@ func TestBlockReceiptStorage(t *testing.T) { // Insert the receipt slice into the database and check presence WriteReceipts(db, hash, 0, receipts) - if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig); len(rs) == 0 { + if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig, nil); len(rs) == 0 { t.Fatalf("no receipts returned") } else { if err := checkReceiptsRLP(rs, receipts); err != nil { @@ -396,7 +396,7 @@ func TestBlockReceiptStorage(t *testing.T) { } // Delete the body and ensure that the receipts are no longer returned (metadata can't be recomputed) DeleteBody(db, hash, 0) - if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig); rs != nil { + if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig, nil); rs != nil { t.Fatalf("receipts returned when body was deleted: %v", rs) } // Ensure that receipts without metadata can be returned without the block body too @@ -407,7 +407,7 @@ func TestBlockReceiptStorage(t *testing.T) { WriteBody(db, hash, 0, body) DeleteReceipts(db, hash, 0) - if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig); len(rs) != 0 { + if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig, nil); len(rs) != 0 { t.Fatalf("deleted receipts returned: %v", rs) } } @@ -727,7 +727,7 @@ func TestReadLogs(t *testing.T) { hash := common.BytesToHash([]byte{0x03, 0x14}) // Check that no receipt entries are in a pristine database - if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig); len(rs) != 0 { + if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig, nil); len(rs) != 0 { t.Fatalf("non existent receipts returned: %v", rs) } // Insert the body that corresponds to the receipts diff --git a/core/rawdb/accessors_indexes.go b/core/rawdb/accessors_indexes.go index 4f2ef0a88..8cd161ace 100644 --- a/core/rawdb/accessors_indexes.go +++ b/core/rawdb/accessors_indexes.go @@ -120,7 +120,7 @@ func ReadTransaction(db ethdb.Reader, hash common.Hash) (*types.Transaction, com // ReadReceipt retrieves a specific transaction receipt from the database, along with // its added positional metadata. -func ReadReceipt(db ethdb.Reader, hash common.Hash, config *params.ChainConfig) (*types.Receipt, common.Hash, uint64, uint64) { +func ReadReceipt(db ethdb.Reader, hash common.Hash, config *params.ChainConfig, stateReader StateReader) (*types.Receipt, common.Hash, uint64, uint64) { // Retrieve the context of the receipt based on the transaction hash blockNumber := ReadTxLookupEntry(db, hash) if blockNumber == nil { @@ -135,7 +135,7 @@ func ReadReceipt(db ethdb.Reader, hash common.Hash, config *params.ChainConfig) return nil, common.Hash{}, 0, 0 } // Read all the receipts from the block and return the one with the matching hash - receipts := ReadReceipts(db, blockHash, *blockNumber, blockHeader.Time, config) + receipts := ReadReceipts(db, blockHash, *blockNumber, blockHeader.Time, config, stateReader) for receiptIndex, receipt := range receipts { if receipt.TxHash == hash { return receipt, blockHash, *blockNumber, uint64(receiptIndex) diff --git a/core/state_processor.go b/core/state_processor.go index a5f6ac352..b120770eb 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -153,9 +153,6 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta receipt.BlockHash = blockHash receipt.BlockNumber = blockNumber receipt.TransactionIndex = uint(statedb.TxIndex()) - if config.AnzeonEnabled() && statedb.IsAuthorized(msg.From) { - receipt.EffectiveGasPrice = tx.EffectiveGasPrice(baseFee, nil) - } return receipt, err } diff --git a/core/types/receipt.go b/core/types/receipt.go index 4d65fd58d..03476e86d 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -31,6 +31,12 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) +// StateReader is an interface for reading state data. +type StateReader interface { + // IsAuthorized returns true if the account is marked as authorized + IsAuthorized(addr common.Address) bool +} + //go:generate go run github.com/fjl/gencodec -type Receipt -field-override receiptMarshaling -out gen_receipt_json.go var ( @@ -323,7 +329,7 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) { // DeriveFields fills the receipts with their computed fields based on consensus // data and contextual infos like containing block and transactions. -func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, number uint64, time uint64, baseFee, headerGasTip *big.Int, blobGasPrice *big.Int, txs []*Transaction) error { +func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, number uint64, time uint64, baseFee, headerGasTip *big.Int, blobGasPrice *big.Int, txs []*Transaction, stateReader StateReader) error { signer := MakeSigner(config, new(big.Int).SetUint64(number), time) logIndex := uint(0) @@ -334,9 +340,19 @@ func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, nu // The transaction type and hash can be retrieved from the transaction itself rs[i].Type = txs[i].Type() rs[i].TxHash = txs[i].Hash() - if rs[i].EffectiveGasPrice == nil || rs[i].EffectiveGasPrice.Cmp(big.NewInt(0)) == 0 { - rs[i].EffectiveGasPrice = txs[i].inner.effectiveGasPrice(baseFee, headerGasTip) + + var gasTip *big.Int + isAuthorized := false + if stateReader != nil { + if from, err := Sender(signer, txs[i]); err == nil { + isAuthorized = stateReader.IsAuthorized(from) + } } + if !isAuthorized && headerGasTip != nil { + gasTip = new(big.Int).Set(headerGasTip) + } + rs[i].EffectiveGasPrice = txs[i].inner.effectiveGasPrice(baseFee, gasTip) + // EIP-4844 blob transaction fields if txs[i].Type() == BlobTxType { rs[i].BlobGasUsed = txs[i].BlobGas() diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go index 4c079ba07..df2ab5f96 100644 --- a/core/types/receipt_test.go +++ b/core/types/receipt_test.go @@ -450,7 +450,7 @@ func TestDeriveFields(t *testing.T) { blobGasPrice := big.NewInt(920) headerGasTip := big.NewInt(1000) derivedReceipts := clearComputedFieldsOnReceipts(receipts) - err := Receipts(derivedReceipts).DeriveFields(params.TestChainConfig, blockHash, blockNumber.Uint64(), blockTime, basefee, headerGasTip, blobGasPrice, txs) + err := Receipts(derivedReceipts).DeriveFields(params.TestChainConfig, blockHash, blockNumber.Uint64(), blockTime, basefee, headerGasTip, blobGasPrice, txs, nil) if err != nil { t.Fatalf("DeriveFields(...) = %v, want ", err) } @@ -477,7 +477,8 @@ func TestDeriveFieldsAuthorizedAccount(t *testing.T) { basefee := big.NewInt(1000) blobGasPrice := big.NewInt(920) derivedReceipts := clearComputedFieldsOnReceipts(authorizedReceipts) - err := Receipts(derivedReceipts).DeriveFields(params.TestChainConfig, blockHash, blockNumber.Uint64(), blockTime, basefee, nil, blobGasPrice, txs) + // headerGasTip nil means authorized; stateDB not needed. + err := Receipts(derivedReceipts).DeriveFields(params.TestChainConfig, blockHash, blockNumber.Uint64(), blockTime, basefee, nil, blobGasPrice, txs, nil) if err != nil { t.Fatalf("DeriveFields(...) = %v, want ", err) } diff --git a/eth/filters/filter_system_test.go b/eth/filters/filter_system_test.go index 99c012cc8..0853891ba 100644 --- a/eth/filters/filter_system_test.go +++ b/eth/filters/filter_system_test.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/bloombits" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" @@ -114,7 +115,13 @@ func (b *testBackend) GetBody(ctx context.Context, hash common.Hash, number rpc. func (b *testBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { if number := rawdb.ReadHeaderNumber(b.db, hash); number != nil { if header := rawdb.ReadHeader(b.db, hash, *number); header != nil { - return rawdb.ReadReceipts(b.db, hash, *number, header.Time, params.TestChainConfig), nil + var stateReader rawdb.StateReader + if stateDatabase := state.NewDatabase(b.db); stateDatabase != nil { + if statedb, err := state.New(header.Root, stateDatabase, nil); err == nil { + stateReader = statedb + } + } + return rawdb.ReadReceipts(b.db, hash, *number, header.Time, params.TestChainConfig, stateReader), nil } } return nil, nil diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 3c0dc6bad..187a6b074 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -558,7 +558,11 @@ func (b testBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.R if header == nil || err != nil { return nil, err } - receipts := rawdb.ReadReceipts(b.db, hash, header.Number.Uint64(), header.Time, b.chain.Config()) + var stateReader rawdb.StateReader + if statedb, err := b.chain.StateAt(header.Root); err == nil { + stateReader = statedb + } + receipts := rawdb.ReadReceipts(b.db, hash, header.Number.Uint64(), header.Time, b.chain.Config(), stateReader) return receipts, nil } func (b testBackend) GetTd(ctx context.Context, hash common.Hash) *big.Int { diff --git a/miner/worker.go b/miner/worker.go index 72d727ccd..0a28522d7 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -1309,7 +1309,7 @@ func (w *worker) generateWork(params *generateParams) *newPayloadResult { } return &newPayloadResult{ block: block, - fees: totalFees(w.chainConfig, block, work.receipts), + fees: totalFees(w.chainConfig, block, work.receipts, work.state), sidecars: work.sidecars, } } @@ -1405,7 +1405,7 @@ func (w *worker) commit(env *environment, interval func(), update bool, start ti if !w.isTTDReached(block.Header()) { select { case w.taskCh <- &task{receipts: env.receipts, state: env.state, block: block, createdAt: time.Now()}: - fees := totalFees(w.chainConfig, block, env.receipts) + fees := totalFees(w.chainConfig, block, env.receipts, env.state) feesInEther := new(big.Float).Quo(new(big.Float).SetInt(fees), big.NewFloat(params.Ether)) log.Info("Commit new sealing work", "number", block.Number(), "sealhash", w.engine.SealHash(block.Header()), "txs", env.tcount, "gas", block.GasUsed(), "fees", feesInEther, @@ -1466,24 +1466,31 @@ func copyReceipts(receipts []*types.Receipt) []*types.Receipt { } // totalFees computes total consumed miner fees in Wei. Block transactions and receipts have to have the same order. -func totalFees(c *params.ChainConfig, block *types.Block, receipts []*types.Receipt) *big.Int { +func totalFees(c *params.ChainConfig, block *types.Block, receipts []*types.Receipt, stateReader types.StateReader) *big.Int { feesWei := new(big.Int) + signer := types.MakeSigner(c, block.Number(), block.Time()) + for i, tx := range block.Transactions() { var minerFee *big.Int // When Anzeon is enabled, the baseFee is not burned. - // Instead, it is rewarded to the miner, making minerFee = baseFee + gasTip + // Instead, it is rewarded to the miner, making minerFee = baseFee + tip if c.AnzeonEnabled() { - if receipts[i].EffectiveGasPrice == nil || receipts[i].EffectiveGasPrice.Cmp(big.NewInt(0)) == 0 { - // not authorized account - var headerGasTip *big.Int + var headerGasTip *big.Int + isAuthorized := false + + if stateReader != nil { + if from, err := types.Sender(signer, tx); err == nil { + isAuthorized = stateReader.IsAuthorized(from) + } + } + + if !isAuthorized { if block.Header() != nil && block.Header().GasTip() != nil { headerGasTip = new(big.Int).Set(block.Header().GasTip()) } - minerFee = tx.EffectiveGasPrice(block.BaseFee(), headerGasTip) - } else { - // authorized account - minerFee = receipts[i].EffectiveGasPrice } + + minerFee = tx.EffectiveGasPrice(block.BaseFee(), headerGasTip) } else { minerFee, _ = tx.EffectiveGasTip(block.BaseFee()) } From a5011778124f60f35bba3beffefe51a1d5a4b824 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Fri, 14 Nov 2025 12:48:10 +0900 Subject: [PATCH 16/20] fix: test failure --- core/state_processor_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/state_processor_test.go b/core/state_processor_test.go index a2214440f..c6e98adbe 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -659,7 +659,7 @@ func TestApplyTransactionAuthorizedAccount(t *testing.T) { t.Errorf("txGasFeeCap is less than expectedEffectiveGasPrice") } - actualEffectiveGasPrice := receipt.EffectiveGasPrice + actualEffectiveGasPrice := tx.EffectiveGasPrice(baseFee, nil) if actualEffectiveGasPrice == nil { t.Errorf("actualEffectiveGasPrice is nil") } From 038e1e7a76a0b09161a82f16aadf3733af1419c0 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Fri, 14 Nov 2025 15:35:10 +0900 Subject: [PATCH 17/20] test: add tx ordering benchmarks --- miner/ordering_test.go | 161 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/miner/ordering_test.go b/miner/ordering_test.go index ba53064fb..a6c2011ad 100644 --- a/miner/ordering_test.go +++ b/miner/ordering_test.go @@ -799,3 +799,164 @@ func TestAnzeonDisabledAndHigherFeeFirst(t *testing.T) { t.Errorf("expected third tx from account 1, got %x", from3) } } + +// Benchmark helpers +func generateTransactions(numAccounts, txsPerAccount int, authorizedRatio float64, rng *rand.Rand) (map[common.Address][]*txpool.LazyTransaction, *state.StateDB) { + signer := types.LatestSignerForChainID(common.Big1) + db := rawdb.NewMemoryDatabase() + sdb := state.NewDatabase(db) + stateDB, _ := state.New(types.EmptyRootHash, sdb, nil) + + groups := make(map[common.Address][]*txpool.LazyTransaction) + authorizedCount := int(float64(numAccounts) * authorizedRatio) + + for i := 0; i < numAccounts; i++ { + key, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(key.PublicKey) + stateDB.CreateAccount(addr) + + isAuthorized := i < authorizedCount + if isAuthorized { + stateDB.SetAuthorized(addr) + } + + for j := 0; j < txsPerAccount; j++ { + gasFeeCap := big.NewInt(int64(10 + rng.Intn(100))) + gasTipCap := big.NewInt(int64(1 + rng.Intn(int(gasFeeCap.Int64())))) + txTime := time.Unix(int64(rng.Intn(1000000)), 0) + + tx, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: uint64(j), + To: &common.Address{}, + Value: big.NewInt(100), + Gas: 100, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Data: nil, + }), signer, key) + tx.SetTime(txTime) + + groups[addr] = append(groups[addr], &txpool.LazyTransaction{ + Hash: tx.Hash(), + Tx: tx, + Time: tx.Time(), + GasFeeCap: uint256.MustFromBig(tx.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx.GasTipCap()), + Gas: tx.Gas(), + BlobGas: tx.BlobGas(), + }) + } + } + + return groups, stateDB +} + +func benchmarkOrdering(b *testing.B, numAccounts, txsPerAccount int, authorizedRatio float64, anzeonEnabled bool) { + // Generate transactions once before starting the timer + rng := rand.New(rand.NewSource(42)) + groups, stateDB := generateTransactions(numAccounts, txsPerAccount, authorizedRatio, rng) + signer := types.LatestSignerForChainID(common.Big1) + baseFee := big.NewInt(0) + + // Prepare groups copy template (will be copied in each iteration) + groupsTemplate := make(map[common.Address][]*txpool.LazyTransaction) + for addr, txs := range groups { + txsCopy := make([]*txpool.LazyTransaction, len(txs)) + copy(txsCopy, txs) + groupsTemplate[addr] = txsCopy + } + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + // Recreate groups for each iteration + groupsCopy := make(map[common.Address][]*txpool.LazyTransaction) + for addr, txs := range groupsTemplate { + txsCopy := make([]*txpool.LazyTransaction, len(txs)) + copy(txsCopy, txs) + groupsCopy[addr] = txsCopy + } + + txset := newTransactionsByPriceAndNonce(signer, groupsCopy, baseFee, anzeonEnabled, stateDB) + + // Consume all transactions to measure full sorting performance + for tx, _ := txset.Peek(); tx != nil; tx, _ = txset.Peek() { + txset.Shift() + } + } +} + +// Benchmark_10Accounts_10Txs benchmarks ordering with 10 accounts, 10 transactions per account +// Compares different account types and configurations +func Benchmark_10Accounts_10Txs(b *testing.B) { + b.Run("AuthorizedOnly", func(b *testing.B) { + benchmarkOrdering(b, 10, 10, 1.0, true) + }) + b.Run("NormalOnly", func(b *testing.B) { + benchmarkOrdering(b, 10, 10, 0.0, true) + }) + b.Run("Mixed_50Percent", func(b *testing.B) { + benchmarkOrdering(b, 10, 10, 0.5, true) + }) + b.Run("AnzeonDisabled", func(b *testing.B) { + benchmarkOrdering(b, 10, 10, 0.5, false) + }) +} + +// Benchmark_50Accounts_10Txs benchmarks ordering with 50 accounts, 10 transactions per account +// Compares different account types and configurations +func Benchmark_50Accounts_10Txs(b *testing.B) { + b.Run("AuthorizedOnly", func(b *testing.B) { + benchmarkOrdering(b, 50, 10, 1.0, true) + }) + b.Run("NormalOnly", func(b *testing.B) { + benchmarkOrdering(b, 50, 10, 0.0, true) + }) + b.Run("Mixed_50Percent", func(b *testing.B) { + benchmarkOrdering(b, 50, 10, 0.5, true) + }) + b.Run("AnzeonDisabled", func(b *testing.B) { + benchmarkOrdering(b, 50, 10, 0.5, false) + }) +} + +// Benchmark_100Accounts_10Txs benchmarks ordering with 100 accounts, 10 transactions per account +// Compares different account types and configurations +func Benchmark_100Accounts_10Txs(b *testing.B) { + b.Run("AuthorizedOnly", func(b *testing.B) { + benchmarkOrdering(b, 100, 10, 1.0, true) + }) + b.Run("NormalOnly", func(b *testing.B) { + benchmarkOrdering(b, 100, 10, 0.0, true) + }) + b.Run("Mixed_50Percent", func(b *testing.B) { + benchmarkOrdering(b, 100, 10, 0.5, true) + }) + b.Run("Mixed_20Percent", func(b *testing.B) { + benchmarkOrdering(b, 100, 10, 0.2, true) + }) + b.Run("Mixed_80Percent", func(b *testing.B) { + benchmarkOrdering(b, 100, 10, 0.8, true) + }) + b.Run("AnzeonDisabled", func(b *testing.B) { + benchmarkOrdering(b, 100, 10, 0.5, false) + }) +} + +// Benchmark_100Accounts_50Txs benchmarks ordering with 100 accounts, 50 transactions per account +// Compares different account types and configurations +func Benchmark_100Accounts_50Txs(b *testing.B) { + b.Run("AuthorizedOnly", func(b *testing.B) { + benchmarkOrdering(b, 100, 50, 1.0, true) + }) + b.Run("NormalOnly", func(b *testing.B) { + benchmarkOrdering(b, 100, 50, 0.0, true) + }) + b.Run("Mixed_50Percent", func(b *testing.B) { + benchmarkOrdering(b, 100, 50, 0.5, true) + }) + b.Run("AnzeonDisabled", func(b *testing.B) { + benchmarkOrdering(b, 100, 50, 0.5, false) + }) +} From 55a4ba8b3b1c79e88f506c9bdc42430fb71c9a08 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Mon, 17 Nov 2025 14:24:49 +0900 Subject: [PATCH 18/20] refactor: remove unused baseFee argument from applyTransaction --- core/state_processor.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/state_processor.go b/core/state_processor.go index b120770eb..bb61f45f4 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -86,7 +86,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } statedb.SetTxContext(tx.Hash(), i) - receipt, err := applyTransaction(msg, p.config, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, header.BaseFee) + receipt, err := applyTransaction(msg, p.config, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv) if err != nil { return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } @@ -106,7 +106,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg return receipts, allLogs, *usedGas, nil } -func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, baseFee *big.Int) (*types.Receipt, error) { +func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (*types.Receipt, error) { // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(msg) evm.Reset(txContext, statedb) @@ -169,7 +169,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo blockContext := NewEVMBlockContext(header, bc, author) txContext := NewEVMTxContext(msg) vmenv := vm.NewEVM(blockContext, txContext, statedb, config, cfg) - return applyTransaction(msg, config, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv, header.BaseFee) + return applyTransaction(msg, config, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv) } // ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root From cb18cc86d85d4dcf70be3425519f67f71f98daf5 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Tue, 18 Nov 2025 13:50:47 +0900 Subject: [PATCH 19/20] feat: implement Anzeon fee policy in priceHeap --- core/txpool/legacypool/legacypool.go | 31 +- core/txpool/legacypool/list.go | 101 +++- core/txpool/legacypool/list_test.go | 710 +++++++++++++++++++++++++++ 3 files changed, 820 insertions(+), 22 deletions(-) diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 7c0a06b14..36d1c40ae 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -762,7 +762,13 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e // Add all transactions back to the priced queue if replacesPending { for _, dropTx := range drop { - pool.priced.Put(dropTx, false) + if pool.chainconfig.AnzeonEnabled() { + dropSender, _ := types.Sender(pool.signer, dropTx) + isAuthorized := pool.currentState.IsAuthorized(dropSender) + pool.priced.PutAnzeon(dropTx, false, isAuthorized) + } else { + pool.priced.Put(dropTx, false) + } } log.Trace("Discarding future transaction replacing pending tx", "hash", hash) return false, txpool.ErrFutureReplacePending @@ -796,7 +802,12 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e pendingReplaceMeter.Mark(1) } pool.all.Add(tx, isLocal) - pool.priced.Put(tx, isLocal) + if pool.chainconfig.AnzeonEnabled() { + isAuthorized := pool.currentState.IsAuthorized(from) + pool.priced.PutAnzeon(tx, isLocal, isAuthorized) + } else { + pool.priced.Put(tx, isLocal) + } pool.journalTx(from, tx) pool.queueTxEvent(tx) log.Trace("Pooled new executable transaction", "hash", hash, "from", from, "to", tx.To()) @@ -880,7 +891,12 @@ func (pool *LegacyPool) enqueueTx(hash common.Hash, tx *types.Transaction, local } if addAll { pool.all.Add(tx, local) - pool.priced.Put(tx, local) + if pool.chainconfig.AnzeonEnabled() { + isAuthorized := pool.currentState.IsAuthorized(from) + pool.priced.PutAnzeon(tx, local, isAuthorized) + } else { + pool.priced.Put(tx, local) + } } // If we never record the heartbeat, do it right now. if _, exist := pool.beats[from]; !exist { @@ -1309,9 +1325,14 @@ func (pool *LegacyPool) runReorg(done chan struct{}, reset *txpoolResetRequest, if pool.chainconfig.IsLondon(new(big.Int).Add(reset.newHead.Number, big.NewInt(1))) { pendingBaseFee := eip1559.CalcBaseFee(pool.chainconfig, reset.newHead) pool.priced.SetBaseFee(pendingBaseFee) - } else { - pool.priced.Reheap() } + + if pool.chainconfig.AnzeonEnabled() { + headerGasTip := reset.newHead.GasTip() + pool.priced.SetHeaderGasTip(headerGasTip) + } + + pool.priced.Reheap() } // Update all accounts to the latest known pending nonce nonces := make(map[common.Address]uint64, len(pool.pending)) diff --git a/core/txpool/legacypool/list.go b/core/txpool/legacypool/list.go index 489f94fe4..678be30c4 100644 --- a/core/txpool/legacypool/list.go +++ b/core/txpool/legacypool/list.go @@ -25,9 +25,9 @@ import ( "sync/atomic" "time" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/common" + cmmath "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/holiman/uint256" "golang.org/x/exp/slices" @@ -480,8 +480,10 @@ func (l *list) subTotalCost(txs []*types.Transaction) { // then the heap is sorted based on the effective tip based on the given base fee. // If baseFee is nil then the sorting is based on gasFeeCap. type priceHeap struct { - baseFee *big.Int // heap should always be re-sorted after baseFee is changed - list []*types.Transaction + baseFee *big.Int // heap should always be re-sorted after baseFee is changed + list []*types.Transaction + isAuthorized map[common.Hash]bool // authorized status map + headerGasTip *big.Int // heap should always be re-sorted after headerGasTip is changed } func (h *priceHeap) Len() int { return len(h.list) } @@ -498,19 +500,52 @@ func (h *priceHeap) Less(i, j int) bool { } } +// getEffectiveTip returns the effective tip for a transaction based on its authorization status. +// Authorized accounts use their own GasTipCap, while normal accounts use the fixed headerGasTip. +func (h *priceHeap) getEffectiveTip(tx *types.Transaction) *big.Int { + if h.isAuthorized[tx.Hash()] { + return tx.GasTipCap() + } + if h.headerGasTip == nil { + return big.NewInt(0) + } + return h.headerGasTip +} + func (h *priceHeap) cmp(a, b *types.Transaction) int { + // Get fee caps + aFeeCap := a.GasFeeCap() + bFeeCap := b.GasFeeCap() + + // Get tip caps + var aTipCap, bTipCap *big.Int + if h.headerGasTip != nil { + // Anzeon enabled: authorized accounts use GasTipCap, normal accounts use headerGasTip + aTipCap = h.getEffectiveTip(a) + bTipCap = h.getEffectiveTip(b) + } else { + // Anzeon disabled: all accounts use their own GasTipCap + aTipCap = a.GasTipCap() + bTipCap = b.GasTipCap() + } + + // Compare effective tips: min(tipCap, feeCap - baseFee) if h.baseFee != nil { - // Compare effective tips if baseFee is specified - if c := a.EffectiveGasTipCmp(b, h.baseFee); c != 0 { + aEffectiveGasTip := cmmath.BigMin(aTipCap, new(big.Int).Sub(aFeeCap, h.baseFee)) + bEffectiveGasTip := cmmath.BigMin(bTipCap, new(big.Int).Sub(bFeeCap, h.baseFee)) + + if c := aEffectiveGasTip.Cmp(bEffectiveGasTip); c != 0 { return c } } - // Compare fee caps if baseFee is not specified or effective tips are equal - if c := a.GasFeeCapCmp(b); c != 0 { + + // compare fee caps + if c := aFeeCap.Cmp(bFeeCap); c != 0 { return c } - // Compare tips if effective tips and fee caps are equal - return a.GasTipCapCmp(b) + + // compare tip caps + return aTipCap.Cmp(bTipCap) } func (h *priceHeap) Push(x interface{}) { @@ -569,6 +604,21 @@ func (l *pricedList) Put(tx *types.Transaction, local bool) { heap.Push(&l.urgent, tx) } +// PutAnzeon inserts a new transaction into the heap with authorization status. +func (l *pricedList) PutAnzeon(tx *types.Transaction, local bool, isAuthorized bool) { + if local { + return + } + + // isAuthorized 맵 초기화 (필요시) + if l.urgent.isAuthorized == nil { + l.urgent.isAuthorized = make(map[common.Hash]bool) + } + + l.urgent.isAuthorized[tx.Hash()] = isAuthorized + heap.Push(&l.urgent, tx) +} + // Removed notifies the prices transaction list that an old transaction dropped // from the pool. The list will just keep a counter of stale objects and update // the heap if a large enough ratio of transactions go stale. @@ -664,10 +714,24 @@ func (l *pricedList) Reheap() { start := time.Now() l.stales.Store(0) l.urgent.list = make([]*types.Transaction, 0, l.all.RemoteCount()) - l.all.Range(func(hash common.Hash, tx *types.Transaction, local bool) bool { - l.urgent.list = append(l.urgent.list, tx) - return true - }, false, true) // Only iterate remotes + + if l.urgent.headerGasTip != nil { + newMap := make(map[common.Hash]bool, l.all.RemoteCount()) + l.all.Range(func(hash common.Hash, tx *types.Transaction, local bool) bool { + l.urgent.list = append(l.urgent.list, tx) + // Preserve authorization status if it exists + if authorized, exists := l.urgent.isAuthorized[hash]; exists { + newMap[hash] = authorized + } + return true + }, false, true) // Only iterate remotes + l.urgent.isAuthorized = newMap + } else { + l.all.Range(func(hash common.Hash, tx *types.Transaction, local bool) bool { + l.urgent.list = append(l.urgent.list, tx) + return true + }, false, true) // Only iterate remotes + } heap.Init(&l.urgent) // balance out the two heaps by moving the worse half of transactions into the @@ -684,9 +748,12 @@ func (l *pricedList) Reheap() { reheapTimer.Update(time.Since(start)) } -// SetBaseFee updates the base fee and triggers a re-heap. Note that Removed is not -// necessary to call right before SetBaseFee when processing a new block. +// SetBaseFee updates the base fee. Note that Reheap must be called separately. func (l *pricedList) SetBaseFee(baseFee *big.Int) { l.urgent.baseFee = baseFee - l.Reheap() +} + +// SetHeaerGasTip updates the header gas tip. Note that Reheap must be called separately. +func (l *pricedList) SetHeaderGasTip(headerGasTip *big.Int) { + l.urgent.headerGasTip = headerGasTip } diff --git a/core/txpool/legacypool/list_test.go b/core/txpool/legacypool/list_test.go index 44a42cff3..4e63b92c5 100644 --- a/core/txpool/legacypool/list_test.go +++ b/core/txpool/legacypool/list_test.go @@ -17,6 +17,7 @@ package legacypool import ( + "container/heap" "math/big" "math/rand" "testing" @@ -110,3 +111,712 @@ func BenchmarkListCapOneTx(b *testing.B) { b.StopTimer() } } + +// TestPricedListIntegration tests the complete pricedList functionality including +// Put, PutAnzeon, Pop, Underpriced, SetBaseFee, SetHeaderGasTip, and all cmp/Less conditions +func TestPricedListIntegration(t *testing.T) { + lookup := newLookup() + priced := newPricedList(lookup) + + // Generate keys for different accounts + authorizedKey1, _ := crypto.GenerateKey() + authorizedKey2, _ := crypto.GenerateKey() + normalKey1, _ := crypto.GenerateKey() + normalKey2, _ := crypto.GenerateKey() + normalKey3, _ := crypto.GenerateKey() + + // ============================================ + // Test Case 1: Anzeon Disabled, baseFee = nil, headerGasTip = nil + // ============================================ + t.Logf("=== Test Case 1: Anzeon Disabled, baseFee = nil, headerGasTip = nil ===") + + // Create transactions with different feeCaps (no baseFee, no headerGasTip, so compare by feeCap) + tx1 := dynamicFeeTx(0, 21000, big.NewInt(50000), big.NewInt(20000), authorizedKey1) // feeCap=50000 + tx2 := dynamicFeeTx(0, 21000, big.NewInt(40000), big.NewInt(15000), authorizedKey2) // feeCap=40000 + tx3 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(10000), normalKey1) // feeCap=30000 + + lookup.Add(tx1, false) + lookup.Add(tx2, false) + lookup.Add(tx3, false) + + priced.Put(tx1, false) + priced.Put(tx2, false) + priced.Put(tx3, false) + + // Reheap to initialize heap + priced.Reheap() + + // Verify order: tx1 (50000) > tx2 (40000) > tx3 (30000) + popped1_1 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped1_1.Hash() != tx3.Hash() { + t.Errorf("Expected tx3 (feeCap=30000) to be popped first, got tx with feeCap=%s", popped1_1.GasFeeCap().String()) + } + + popped1_2 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped1_2.Hash() != tx2.Hash() { + t.Errorf("Expected tx2 (feeCap=40000) to be popped second, got tx with feeCap=%s", popped1_2.GasFeeCap().String()) + } + + popped1_3 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped1_3.Hash() != tx1.Hash() { + t.Errorf("Expected tx1 (feeCap=50000) to be popped last, got tx with feeCap=%s", popped1_3.GasFeeCap().String()) + } + + // ============================================ + // Test Case 2: Anzeon Disabled, baseFee exists, headerGasTip = nil + // ============================================ + t.Logf("=== Test Case 2: Anzeon Disabled, baseFee = 10000, headerGasTip = nil ===") + + // Clear and reset + lookup = newLookup() + priced = newPricedList(lookup) + + // Set baseFee + baseFee := big.NewInt(10000) + priced.SetBaseFee(baseFee) + + // effectiveTip = min(tipCap, feeCap - baseFee) + + // Create transactions with same feeCap but different effectiveTips + // tx4: feeCap=30000, tipCap=20000 -> effectiveTip=min(20000, 20000)=20000 + tx4 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(20000), authorizedKey1) + // tx5: feeCap=30000, tipCap=15000 -> effectiveTip=min(15000, 20000)=15000 + tx5 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(15000), authorizedKey2) + // tx6: feeCap=30000, tipCap=10000 -> effectiveTip=min(10000, 20000)=10000 + tx6 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(10000), normalKey1) + + lookup.Add(tx4, false) + lookup.Add(tx5, false) + lookup.Add(tx6, false) + + priced.Put(tx4, false) + priced.Put(tx5, false) + priced.Put(tx6, false) + + priced.Reheap() + + // Verify order: tx4 (effectiveTip=20000) > tx5 (15000) > tx6 (10000) + popped2_1 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped2_1.Hash() != tx6.Hash() { + t.Errorf("Expected tx6 (effectiveTip=10000) to be popped first") + } + + popped2_2 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped2_2.Hash() != tx5.Hash() { + t.Errorf("Expected tx5 (effectiveTip=15000) to be popped second") + } + + popped2_3 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped2_3.Hash() != tx4.Hash() { + t.Errorf("Expected tx4 (effectiveTip=20000) to be popped last") + } + + // ============================================ + // Test Case 3: Anzeon Enabled, baseFee exists, headerGasTip exists + // ============================================ + t.Logf("=== Test Case 3: Anzeon Enabled, baseFee = 10000, headerGasTip = 10000 ===") + + // Clear and reset + lookup = newLookup() + priced = newPricedList(lookup) + + baseFee = big.NewInt(10000) + headerGasTip := big.NewInt(10000) + priced.SetBaseFee(baseFee) + priced.SetHeaderGasTip(headerGasTip) + + //effectiveTip = min(tipCap, feeCap - baseFee) + + // Authorized transactions use their own GasTipCap + // authTx1: feeCap=30000, tipCap=20000 -> effectiveTip=min(20000, 20000)=20000 + authTx1 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(20000), authorizedKey1) + // authTx2: feeCap=30000, tipCap=15000 -> effectiveTip=min(15000, 20000)=15000 + authTx2 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(15000), authorizedKey2) + + // Normal transactions use headerGasTip (10000) + // normTx1: feeCap=30000, tipCap=25000 (ignored) -> effectiveTip=min(10000, 20000)=10000 + normTx1 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(25000), normalKey1) + // normTx2: feeCap=25000, tipCap=20000 (ignored) -> effectiveTip=min(10000, 15000)=10000 + normTx2 := dynamicFeeTx(0, 21000, big.NewInt(25000), big.NewInt(20000), normalKey2) + + lookup.Add(authTx1, false) + lookup.Add(authTx2, false) + lookup.Add(normTx1, false) + lookup.Add(normTx2, false) + + priced.PutAnzeon(authTx1, false, true) + priced.PutAnzeon(authTx2, false, true) + priced.PutAnzeon(normTx1, false, false) + priced.PutAnzeon(normTx2, false, false) + + priced.Reheap() + + // Verify order: authTx1 (20000) > authTx2 (15000) > normTx1 (10000, feeCap=30000) > normTx2 (10000, feeCap=25000) + popped3_1 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped3_1.Hash() != normTx2.Hash() { + t.Errorf("Expected normTx2 (effectiveTip=10000, feeCap=25000) to be popped first") + } + + popped3_2 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped3_2.Hash() != normTx1.Hash() { + t.Errorf("Expected normTx1 (effectiveTip=10000, feeCap=30000) to be popped second") + } + + popped3_3 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped3_3.Hash() != authTx2.Hash() { + t.Errorf("Expected authTx2 (effectiveTip=15000) to be popped third") + } + + popped3_4 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped3_4.Hash() != authTx1.Hash() { + t.Errorf("Expected authTx1 (effectiveTip=20000) to be popped last") + } + + // ============================================ + // Test Case 4: Same effectiveTip, compare by feeCap + // ============================================ + t.Logf("=== Test Case 4: Same effectiveTip, compare by feeCap ===") + + lookup = newLookup() + priced = newPricedList(lookup) + + priced.SetBaseFee(big.NewInt(10000)) + priced.SetHeaderGasTip(big.NewInt(10000)) + + // effectiveTip = min(tipCap, feeCap - baseFee) + + // Normal transactions use headerGasTip (10000) + // All have effectiveTip=10000, but different feeCaps + // normTx3: feeCap=40000, tipCap=30000(ignored) -> effectiveTip=min(10000, 40000-10000)=10000 + normTx3 := dynamicFeeTx(0, 21000, big.NewInt(40000), big.NewInt(30000), normalKey3) + // normTx4: feeCap=35000, tipCap=30000(ignored) -> effectiveTip=min(10000, 35000-10000)=10000 + normTx4 := dynamicFeeTx(0, 21000, big.NewInt(35000), big.NewInt(30000), normalKey1) + // normTx5: feeCap=30000, tipCap=30000(ignored) -> effectiveTip=min(10000, 30000-10000)=10000 + normTx5 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(30000), normalKey2) + + lookup.Add(normTx3, false) + lookup.Add(normTx4, false) + lookup.Add(normTx5, false) + + priced.PutAnzeon(normTx3, false, false) + priced.PutAnzeon(normTx4, false, false) + priced.PutAnzeon(normTx5, false, false) + + priced.Reheap() + + // Verify order: normTx5 (30000) < normTx4 (35000) < normTx3 (40000) + popped4_1 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped4_1.Hash() != normTx5.Hash() { + t.Errorf("Expected normTx5 (feeCap=30000) to be popped first") + } + + popped4_2 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped4_2.Hash() != normTx4.Hash() { + t.Errorf("Expected normTx4 (feeCap=35000) to be popped second") + } + + popped4_3 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped4_3.Hash() != normTx3.Hash() { + t.Errorf("Expected normTx3 (feeCap=40000) to be popped last") + } + + // ============================================ + // Test Case 5: Same effectiveTip and feeCap, compare by tipCap + // ============================================ + t.Logf("=== Test Case 5: Same effectiveTip and feeCap, compare by tipCap ===") + + lookup = newLookup() + priced = newPricedList(lookup) + + priced.SetBaseFee(big.NewInt(10000)) + priced.SetHeaderGasTip(big.NewInt(10000)) + + // effectiveTip = min(tipCap, feeCap - baseFee) + // All have same feeCap=30000, but different tipCaps (for authorized accounts) + // authTx3: tipCap=20000 -> effectiveTip=min(20000, 30000-10000)=min(20000, 20000)=20000 + authTx3 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(20000), authorizedKey1) + // authTx4: tipCap=15000 -> effectiveTip=min(15000, 30000-10000)=min(15000, 20000)=15000 + authTx4 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(15000), authorizedKey2) + + lookup.Add(authTx3, false) + lookup.Add(authTx4, false) + + priced.PutAnzeon(authTx3, false, true) + priced.PutAnzeon(authTx4, false, true) + + priced.Reheap() + + // Verify order: authTx4 (effectiveTip=15000) < authTx3 (effectiveTip=20000) + popped5_1 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped5_1.Hash() != authTx4.Hash() { + t.Errorf("Expected authTx4 (effectiveTip=15000) to be popped first") + } + + popped5_2 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped5_2.Hash() != authTx3.Hash() { + t.Errorf("Expected authTx3 (effectiveTip=20000) to be popped last") + } + + // ============================================ + // Test Case 6a: Same effectiveTip, feeCap, tipCap - compare by nonce (reverse) + // ============================================ + t.Logf("=== Test Case 6: Same effectiveTip, feeCap, tipCap - compare by nonce (reverse) ===") + + lookup = newLookup() + priced = newPricedList(lookup) + + priced.SetBaseFee(big.NewInt(10000)) + priced.SetHeaderGasTip(big.NewInt(10000)) + + // All values identical, different nonces + // sameTx1: nonce=0 + sameTx1 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(20000), authorizedKey1) + // sameTx2: nonce=1 + sameTx2 := dynamicFeeTx(1, 21000, big.NewInt(30000), big.NewInt(20000), authorizedKey1) + // sameTx3: nonce=2 + sameTx3 := dynamicFeeTx(2, 21000, big.NewInt(30000), big.NewInt(20000), authorizedKey1) + + lookup.Add(sameTx1, false) + lookup.Add(sameTx2, false) + lookup.Add(sameTx3, false) + + priced.PutAnzeon(sameTx1, false, true) + priced.PutAnzeon(sameTx2, false, true) + priced.PutAnzeon(sameTx3, false, true) + + priced.Reheap() + + // Verify order: nonce reverse (higher nonce first) + // sameTx3 (nonce=2) < sameTx2 (nonce=1) < sameTx1 (nonce=0) + popped6a_1 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped6a_1.Hash() != sameTx3.Hash() { + t.Errorf("Expected sameTx3 (nonce=2) to be popped first") + } + + popped6a_2 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped6a_2.Hash() != sameTx2.Hash() { + t.Errorf("Expected sameTx2 (nonce=1) to be popped second") + } + + popped6a_3 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped6a_3.Hash() != sameTx1.Hash() { + t.Errorf("Expected sameTx1 (nonce=0) to be popped last") + } + + // ============================================ + // Test Case 6b: nonce comparison (Anzeon disabled, no baseFee) + // ============================================ + t.Logf("=== Test Case 6b: nonce comparison (Anzeon disabled, no baseFee) ===") + + lookup = newLookup() + priced = newPricedList(lookup) + + // No baseFee, no headerGasTip (Anzeon disabled) + // All transactions have same feeCap and tipCap + // nonceTx1: nonce=0, feeCap=30000, tipCap=20000 + nonceTx1 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(20000), normalKey1) + // nonceTx2: nonce=1, feeCap=30000, tipCap=20000 + nonceTx2 := dynamicFeeTx(1, 21000, big.NewInt(30000), big.NewInt(20000), normalKey1) + // nonceTx3: nonce=2, feeCap=30000, tipCap=20000 + nonceTx3 := dynamicFeeTx(2, 21000, big.NewInt(30000), big.NewInt(20000), normalKey1) + + lookup.Add(nonceTx1, false) + lookup.Add(nonceTx2, false) + lookup.Add(nonceTx3, false) + + priced.Put(nonceTx1, false) + priced.Put(nonceTx2, false) + priced.Put(nonceTx3, false) + + priced.Reheap() + + // Verify order: nonce reverse (higher nonce first) + // nonceTx3 (nonce=2) < nonceTx2 (nonce=1) < nonceTx1 (nonce=0) + popped6b_1 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped6b_1.Hash() != nonceTx3.Hash() { + t.Errorf("Expected nonceTx3 (nonce=2) to be popped first") + } + + popped6b_2 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped6b_2.Hash() != nonceTx2.Hash() { + t.Errorf("Expected nonceTx2 (nonce=1) to be popped second") + } + + popped6b_3 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped6b_3.Hash() != nonceTx1.Hash() { + t.Errorf("Expected nonceTx1 (nonce=0) to be popped last") + } + + // ============================================ + // Test Case 6c: nonce comparison (Anzeon disabled, with baseFee) + // ============================================ + t.Logf("=== Test Case 6c: nonce comparison (Anzeon disabled, with baseFee) ===") + + lookup = newLookup() + priced = newPricedList(lookup) + + priced.SetBaseFee(big.NewInt(10000)) + // No headerGasTip (Anzeon disabled) + + // All transactions have same feeCap, tipCap, and effectiveTip + // baseNonceTx1: nonce=0, feeCap=30000, tipCap=20000 -> effectiveTip=min(20000, 20000)=20000 + baseNonceTx1 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(20000), normalKey1) + // baseNonceTx2: nonce=1, feeCap=30000, tipCap=20000 -> effectiveTip=min(20000, 20000)=20000 + baseNonceTx2 := dynamicFeeTx(1, 21000, big.NewInt(30000), big.NewInt(20000), normalKey1) + // baseNonceTx3: nonce=2, feeCap=30000, tipCap=20000 -> effectiveTip=min(20000, 20000)=20000 + baseNonceTx3 := dynamicFeeTx(2, 21000, big.NewInt(30000), big.NewInt(20000), normalKey1) + + lookup.Add(baseNonceTx1, false) + lookup.Add(baseNonceTx2, false) + lookup.Add(baseNonceTx3, false) + + priced.Put(baseNonceTx1, false) + priced.Put(baseNonceTx2, false) + priced.Put(baseNonceTx3, false) + + priced.Reheap() + + // Verify order: nonce reverse (higher nonce first) + // baseNonceTx3 (nonce=2) < baseNonceTx2 (nonce=1) < baseNonceTx1 (nonce=0) + popped6c_1 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped6c_1.Hash() != baseNonceTx3.Hash() { + t.Errorf("Expected baseNonceTx3 (nonce=2) to be popped first") + } + + popped6c_2 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped6c_2.Hash() != baseNonceTx2.Hash() { + t.Errorf("Expected baseNonceTx2 (nonce=1) to be popped second") + } + + popped6c_3 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped6c_3.Hash() != baseNonceTx1.Hash() { + t.Errorf("Expected baseNonceTx1 (nonce=0) to be popped last") + } + + // ============================================ + // Test Case 6d: nonce comparison (Anzeon enabled, mixed accounts) + // ============================================ + t.Logf("=== Test Case 6d: All nonce comparison (Anzeon enabled, mixed accounts) ===") + + lookup = newLookup() + priced = newPricedList(lookup) + + priced.SetBaseFee(big.NewInt(10000)) + priced.SetHeaderGasTip(big.NewInt(20000)) // Same as tipCap for authorized + + // All transactions have same effectiveTip, feeCap + // mixedNonceTx1: nonce=0, authorized, feeCap=40000, tipCap=20000 -> effectiveTip=min(20000, 40000-10000)=20000 + mixedNonceTx1 := dynamicFeeTx(0, 21000, big.NewInt(40000), big.NewInt(20000), authorizedKey1) + // mixedNonceTx2: nonce=1, normal, feeCap=40000, tipCap=30000(ignored) -> effectiveTip=min(20000, 40000-10000)=20000 + mixedNonceTx2 := dynamicFeeTx(1, 21000, big.NewInt(40000), big.NewInt(30000), normalKey1) + // mixedNonceTx3: nonce=2, authorized, feeCap=40000, tipCap=20000 -> effectiveTip=min(20000, 40000-10000)=20000 + mixedNonceTx3 := dynamicFeeTx(2, 21000, big.NewInt(40000), big.NewInt(20000), authorizedKey2) + + lookup.Add(mixedNonceTx1, false) + lookup.Add(mixedNonceTx2, false) + lookup.Add(mixedNonceTx3, false) + + priced.PutAnzeon(mixedNonceTx1, false, true) + priced.PutAnzeon(mixedNonceTx2, false, false) + priced.PutAnzeon(mixedNonceTx3, false, true) + + priced.Reheap() + + // All have same effectiveTip (20000), feeCap (40000), tipCap (20000), normal account uses headerGasTip=20000) + // Should compare by nonce reverse: nonce=2 < nonce=1 < nonce=0 + popped6d_1 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped6d_1.Hash() != mixedNonceTx3.Hash() { + t.Errorf("Expected mixedNonceTx3 (nonce=2) to be popped first") + } + + popped6d_2 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped6d_2.Hash() != mixedNonceTx2.Hash() { + t.Errorf("Expected mixedNonceTx2 (nonce=1) to be popped second") + } + + popped6d_3 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped6d_3.Hash() != mixedNonceTx1.Hash() { + t.Errorf("Expected mixedNonceTx1 (nonce=0) to be popped last") + } + + // ============================================ + // Test Case 7: Underpriced check + // ============================================ + t.Logf("=== Test Case 7: Underpriced check ===") + + lookup = newLookup() + priced = newPricedList(lookup) + + priced.SetBaseFee(big.NewInt(10000)) + priced.SetHeaderGasTip(big.NewInt(10000)) + + // Add multiple transactions to ensure both urgent and floating heaps have items + // highTx: effectiveTip=min(30000, 40000)=30000 + highTx := dynamicFeeTx(0, 21000, big.NewInt(50000), big.NewInt(30000), authorizedKey1) + // midTx: effectiveTip=min(20000, 40000)=20000 + midTx := dynamicFeeTx(0, 21000, big.NewInt(45000), big.NewInt(20000), authorizedKey2) + // lowTx: effectiveTip=min(10000, 30000)=10000 + veryLowTx := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(10000), normalKey1) + + lookup.Add(highTx, false) + lookup.Add(midTx, false) + lookup.Add(veryLowTx, false) + + priced.PutAnzeon(highTx, false, true) + priced.PutAnzeon(midTx, false, true) + priced.PutAnzeon(veryLowTx, false, false) + + priced.Reheap() + + // After Reheap, some transactions may be in floating heap + // The minimum priority transaction (worst) should be in one of the heaps + + // Create a lower-priority transaction than the worst one + // lowTx: effectiveTip=min(5000, 10000)=5000 + lowTx := dynamicFeeTx(0, 21000, big.NewInt(20000), big.NewInt(5000), normalKey2) + + // lowTx should be underpriced (worse than the worst transaction in heaps) + if !priced.Underpriced(lowTx) { + t.Errorf("Expected lowTx to be underpriced") + } + + // Create a much higher-priority transaction + // higherTx: effectiveTip=min(40000, 50000)=40000, feeCap=60000 + higherTx := dynamicFeeTx(0, 21000, big.NewInt(60000), big.NewInt(40000), authorizedKey1) + + // higherTx should NOT be underpriced (better than all transactions in heaps) + if priced.Underpriced(higherTx) { + t.Errorf("Expected higherTx NOT to be underpriced (effectiveTip=40000 > all others)") + } + + // ============================================ + // Test Case 8: Change headerGasTip + // ============================================ + t.Logf("=== Test Case 8: Change headerGasTip ===") + + lookup = newLookup() + priced = newPricedList(lookup) + + // Initial settings: baseFee=10000, headerGasTip=10000 + baseFee = big.NewInt(10000) + headerGasTip = big.NewInt(10000) + priced.SetBaseFee(baseFee) + priced.SetHeaderGasTip(headerGasTip) + + // Add multiple transactions to see sorting effects + // Conditions: + // - GasTipCap >= headerGasTip (all accounts) + // - GasFeeCap >= headerGasTip + baseFee (normal accounts) + // - GasFeeCap >= GasTipCap + baseFee (authorized accounts) + + // Authorized transactions + // dynAuthTx1: effectiveTip=min(20000, 40000-10000)=min(20000, 30000)=20000 + dynAuthTx1 := dynamicFeeTx(0, 21000, big.NewInt(40000), big.NewInt(20000), authorizedKey1) + // dynAuthTx2: effectiveTip=min(15000, 30000-10000)=min(15000, 20000)=15000 + dynAuthTx2 := dynamicFeeTx(1, 21000, big.NewInt(30000), big.NewInt(15000), authorizedKey2) + + // Normal transactions (use headerGasTip=10000) + // dynNormTx1: effectiveTip=min(10000, 30000-10000)=min(10000, 20000)=10000 + dynNormTx1 := dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(25000), normalKey1) + // dynNormTx2: effectiveTip=min(10000, 35000-10000)=min(10000, 25000)=10000 + dynNormTx2 := dynamicFeeTx(0, 21000, big.NewInt(35000), big.NewInt(30000), normalKey2) + // dynNormTx3: effectiveTip=min(10000, 25000-10000)=min(10000, 15000)=10000 + dynNormTx3 := dynamicFeeTx(0, 21000, big.NewInt(25000), big.NewInt(20000), normalKey3) + + lookup.Add(dynAuthTx1, false) + lookup.Add(dynAuthTx2, false) + lookup.Add(dynNormTx1, false) + lookup.Add(dynNormTx2, false) + lookup.Add(dynNormTx3, false) + + priced.PutAnzeon(dynAuthTx1, false, true) + priced.PutAnzeon(dynAuthTx2, false, true) + priced.PutAnzeon(dynNormTx1, false, false) + priced.PutAnzeon(dynNormTx2, false, false) + priced.PutAnzeon(dynNormTx3, false, false) + + priced.Reheap() + + // Note: Reheap() moves some transactions to floating heap + // With 5 transactions: floatingCount = 5 * 1 / (4 + 1) = 1 + // So urgent has 4, floating has 1 + + // Initial order (headerGasTip=10000, min-heap: smallest first): + // All normal transactions have effectiveTip=10000, compare by feeCap + // 1. dynNormTx3 (effectiveTip=10000, feeCap=35000) - smallest (lowest priority, moved to floating) + // 2. dynNormTx1 (effectiveTip=10000, feeCap=40000) + // 3. dynNormTx2 (effectiveTip=10000, feeCap=45000) + // 4. dynAuthTx2 (effectiveTip=15000) + // 5. dynAuthTx1 (effectiveTip=20000) - largest (highest priority) + + // Verify initial order in urgent heap (4 transactions remain) + // Note: Reheap() balances heaps, so some transactions move to floating + urgentCount := priced.urgent.Len() + floatingCount := priced.floating.Len() + if urgentCount != 4 { + t.Errorf("Initial: expected 4 transactions in urgent, got %d (floating has %d)", urgentCount, floatingCount) + } + + // Verify floating heap has the smallest transaction (moved by Reheap) + if priced.floating.Len() != 1 { + t.Errorf("Initial: expected 1 transaction in floating, got %d", priced.floating.Len()) + } + + // Pop from urgent and verify sorting order + // After Reheap(), urgent has 4 transactions, floating has 1 + // The smallest transaction (dynNormTx3) is moved to floating + // Expected order in urgent heap (after smallest moved to floating): + // 1. dynNormTx1 (effectiveTip=10000, feeCap=40000) - smallest remaining + // 2. dynNormTx2 (effectiveTip=10000, feeCap=45000) + // 3. dynAuthTx2 (effectiveTip=15000) + // 4. dynAuthTx1 (effectiveTip=20000) + popped1 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped1.Hash() != dynNormTx1.Hash() { + t.Errorf("Initial: expected dynNormTx1 (effectiveTip=10000, feeCap=40000) to be popped first, got feeCap=%s", popped1.GasFeeCap().String()) + } + + popped2 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped2.Hash() != dynNormTx2.Hash() { + t.Errorf("Initial: expected dynNormTx2 (effectiveTip=10000, feeCap=45000) to be popped second, got feeCap=%s", popped2.GasFeeCap().String()) + } + + popped3 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped3.Hash() != dynAuthTx2.Hash() { + t.Errorf("Initial: expected dynAuthTx2 (effectiveTip=15000) to be popped third") + } + + popped4 := heap.Pop(&priced.urgent).(*types.Transaction) + if popped4.Hash() != dynAuthTx1.Hash() { + t.Errorf("Initial: expected dynAuthTx1 (effectiveTip=20000) to be popped fourth") + } + + floatingTx := heap.Pop(&priced.floating).(*types.Transaction) + if floatingTx.Hash() != dynNormTx3.Hash() { + t.Errorf("Initial: expected dynNormTx3 (effectiveTip=10000, feeCap=35000) to be in floating heap") + } + + t.Logf("Initial sorting verified: urgent has 4, floating has 1, all in correct order") + + // Re-add transactions for headerGasTip change test + // Need to create new transactions that satisfy conditions for headerGasTip=25000 + lookup = newLookup() + priced = newPricedList(lookup) + + baseFee = big.NewInt(10000) + headerGasTip = big.NewInt(10000) + priced.SetBaseFee(baseFee) + priced.SetHeaderGasTip(headerGasTip) + + // Create transactions that satisfy conditions for both headerGasTip=10000 and 25000 + // Authorized transactions (unchanged) + // dynAuthTx1: tipCap=20000 >= headerGasTip(10000, 25000) ✓, feeCap=40000 >= tipCap(20000) + baseFee(10000) = 30000 ✓ + dynAuthTx1 = dynamicFeeTx(0, 21000, big.NewInt(40000), big.NewInt(20000), authorizedKey1) + // dynAuthTx2: tipCap=15000 >= headerGasTip(10000) ✓, feeCap=30000 >= tipCap(15000) + baseFee(10000) = 25000 ✓ + dynAuthTx2 = dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(15000), authorizedKey2) + + // Normal transactions - must satisfy: feeCap >= headerGasTip + baseFee for both 10000 and 25000 + // For headerGasTip=25000: feeCap >= 25000 + 10000 = 35000 + // dynNormTx1: tipCap=25000 >= headerGasTip(10000, 25000) ✓, feeCap=40000 >= headerGasTip(25000) + baseFee(10000) = 35000 ✓ + // headerGasTip=10000: effectiveTip=min(10000, 40000-10000)=min(10000, 30000)=10000 + // headerGasTip=25000: effectiveTip=min(25000, 40000-10000)=min(25000, 30000)=25000 + dynNormTx1 = dynamicFeeTx(0, 21000, big.NewInt(40000), big.NewInt(25000), normalKey1) + // dynNormTx2: tipCap=30000 >= headerGasTip(10000, 25000) ✓, feeCap=45000 >= headerGasTip(25000) + baseFee(10000) = 35000 ✓ + // headerGasTip=10000: effectiveTip=min(10000, 45000-10000)=min(10000, 35000)=10000 + // headerGasTip=25000: effectiveTip=min(25000, 45000-10000)=min(25000, 35000)=25000 + dynNormTx2 = dynamicFeeTx(0, 21000, big.NewInt(45000), big.NewInt(30000), normalKey2) + // dynNormTx3: tipCap=20000 >= headerGasTip(10000) ✓, feeCap=35000 >= headerGasTip(25000) + baseFee(10000) = 35000 ✓ + // headerGasTip=10000: effectiveTip=min(10000, 35000-10000)=min(10000, 25000)=10000 + // headerGasTip=25000: effectiveTip=min(25000, 35000-10000)=min(25000, 25000)=25000 + dynNormTx3 = dynamicFeeTx(0, 21000, big.NewInt(35000), big.NewInt(20000), normalKey3) + + lookup.Add(dynAuthTx1, false) + lookup.Add(dynAuthTx2, false) + lookup.Add(dynNormTx1, false) + lookup.Add(dynNormTx2, false) + lookup.Add(dynNormTx3, false) + + priced.PutAnzeon(dynAuthTx1, false, true) + priced.PutAnzeon(dynAuthTx2, false, true) + priced.PutAnzeon(dynNormTx1, false, false) + priced.PutAnzeon(dynNormTx2, false, false) + priced.PutAnzeon(dynNormTx3, false, false) + + priced.Reheap() + + // Initial order (headerGasTip=10000, min-heap: smallest first): + // All normal transactions have effectiveTip=10000, compare by feeCap + // 1. dynNormTx3 (effectiveTip=10000, feeCap=35000) - smallest (lowest priority) + // 2. dynNormTx1 (effectiveTip=10000, feeCap=40000) + // 3. dynNormTx2 (effectiveTip=10000, feeCap=45000) + // 4. dynAuthTx2 (effectiveTip=15000) + // 5. dynAuthTx1 (effectiveTip=20000) - largest (highest priority) + + // Change headerGasTip from 10000 to 15000 + priced.SetHeaderGasTip(big.NewInt(15000)) + priced.Reheap() + + // After headerGasTip change to 15000: + // Normal transactions now use headerGasTip=15000 instead of 10000 + // dynAuthTx1: effectiveTip=min(20000, 40000-10000)=min(20000, 30000)=20000 (unchanged) + // dynAuthTx2: effectiveTip=min(15000, 30000-10000)=min(15000, 20000)=15000 (unchanged) + // dynNormTx1: effectiveTip=min(15000, 40000-10000)=min(15000, 30000)=15000 (changed from 10000!) + // dynNormTx2: effectiveTip=min(15000, 45000-10000)=min(15000, 35000)=15000 (changed from 10000!) + // dynNormTx3: effectiveTip=min(15000, 35000-10000)=min(15000, 25000)=15000 (changed from 10000!) + + // New order should be (urgent has 4, floating has 1): + // 1. dynAuthTx2 (effectiveTip=15000, feeCap=30000, nonce=1) - smallest (moved to floating) + // 2. dynNormTx3 (effectiveTip=15000, feeCap=35000, nonce=0) + // 3. dynNormTx1 (effectiveTip=15000, feeCap=40000, nonce=0) + // 4. dynNormTx2 (effectiveTip=15000, feeCap=45000, nonce=0) + // 5. dynAuthTx1 (effectiveTip=20000, feeCap=40000, nonce=0) - largest (highest priority) + + // Verify urgent heap has 4 transactions after headerGasTip change + urgentCountAfter := priced.urgent.Len() + floatingCountAfter := priced.floating.Len() + if urgentCountAfter != 4 { + t.Errorf("After headerGasTip change: expected 4 transactions in urgent, got %d (floating has %d)", urgentCountAfter, floatingCountAfter) + } + + // Verify floating heap has 1 transaction + if priced.floating.Len() != 1 { + t.Errorf("After headerGasTip change: expected 1 transaction in floating, got %d", priced.floating.Len()) + } + + // Pop from urgent and verify sorting order (min-heap: smallest first) + // Note: Reheap() moves the smallest transaction to floating heap using heap.Pop() + // After headerGasTip=25000, the smallest transaction (dynAuthTx2) is moved to floating + // Expected order in urgent heap (after smallest moved to floating): + // 1. dynNormTx3 (effectiveTip=15000, feeCap=35000, nonce=0) + // 2. dynNormTx1 (effectiveTip=15000, feeCap=40000, nonce=0) + // 3. dynNormTx2 (effectiveTip=15000, feeCap=45000, nonce=0) + // 4. dynAuthTx1 (effectiveTip=20000, feeCap=40000, nonce=0) - largest (highest priority) + popped1 = heap.Pop(&priced.urgent).(*types.Transaction) + if popped1.Hash() != dynNormTx3.Hash() { + t.Errorf("After headerGasTip change: expected dynNormTx3 (effectiveTip=15000, feeCap=35000) to be popped first, got feeCap=%s", popped1.GasFeeCap().String()) + } + + popped2 = heap.Pop(&priced.urgent).(*types.Transaction) + if popped2.Hash() != dynNormTx1.Hash() { + t.Errorf("After headerGasTip change: expected dynNormTx1 (effectiveTip=15000, feeCap=40000) to be popped second, got feeCap=%s", popped2.GasFeeCap().String()) + } + + popped3 = heap.Pop(&priced.urgent).(*types.Transaction) + if popped3.Hash() != dynNormTx2.Hash() { + t.Errorf("After headerGasTip change: expected dynNormTx2 (effectiveTip=15000, feeCap=45000) to be popped third, got feeCap=%s", popped3.GasFeeCap().String()) + } + + popped4 = heap.Pop(&priced.urgent).(*types.Transaction) + if popped4.Hash() != dynAuthTx1.Hash() { + t.Errorf("After headerGasTip change: expected dynAuthTx1 (effectiveTip=20000, feeCap=40000) to be popped fourth, got feeCap=%s", popped4.GasFeeCap().String()) + } + + floatingTx = heap.Pop(&priced.floating).(*types.Transaction) + if floatingTx.Hash() != dynAuthTx2.Hash() { + t.Errorf("After headerGasTip change: expected dynAuthTx2 (effectiveTip=15000, feeCap=30000) to be in floating heap") + } + + t.Logf("After headerGasTip change: urgent has 4, floating has 1, all in correct order") + t.Logf("HeaderGasTip change from 10000 to 25000 successfully affected normal transactions' effectiveTip") + + t.Logf("=== All test cases passed! ===") +} From 0d525079ae908c0912a19f5910f4a315535eb2e3 Mon Sep 17 00:00:00 2001 From: "hmlee(WM)" Date: Tue, 18 Nov 2025 16:19:01 +0900 Subject: [PATCH 20/20] chore: change comment --- core/txpool/legacypool/list.go | 2 +- core/txpool/legacypool/list_test.go | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/core/txpool/legacypool/list.go b/core/txpool/legacypool/list.go index 678be30c4..cea5df33f 100644 --- a/core/txpool/legacypool/list.go +++ b/core/txpool/legacypool/list.go @@ -610,7 +610,7 @@ func (l *pricedList) PutAnzeon(tx *types.Transaction, local bool, isAuthorized b return } - // isAuthorized 맵 초기화 (필요시) + // Initialize isAuthorized map if needed if l.urgent.isAuthorized == nil { l.urgent.isAuthorized = make(map[common.Hash]bool) } diff --git a/core/txpool/legacypool/list_test.go b/core/txpool/legacypool/list_test.go index 4e63b92c5..75ad94ac4 100644 --- a/core/txpool/legacypool/list_test.go +++ b/core/txpool/legacypool/list_test.go @@ -714,19 +714,11 @@ func TestPricedListIntegration(t *testing.T) { // dynAuthTx2: tipCap=15000 >= headerGasTip(10000) ✓, feeCap=30000 >= tipCap(15000) + baseFee(10000) = 25000 ✓ dynAuthTx2 = dynamicFeeTx(0, 21000, big.NewInt(30000), big.NewInt(15000), authorizedKey2) - // Normal transactions - must satisfy: feeCap >= headerGasTip + baseFee for both 10000 and 25000 - // For headerGasTip=25000: feeCap >= 25000 + 10000 = 35000 - // dynNormTx1: tipCap=25000 >= headerGasTip(10000, 25000) ✓, feeCap=40000 >= headerGasTip(25000) + baseFee(10000) = 35000 ✓ - // headerGasTip=10000: effectiveTip=min(10000, 40000-10000)=min(10000, 30000)=10000 - // headerGasTip=25000: effectiveTip=min(25000, 40000-10000)=min(25000, 30000)=25000 + // dynNormTx1: headerGasTip=25000: effectiveTip=min(25000, 40000-10000)=min(25000, 30000)=25000 dynNormTx1 = dynamicFeeTx(0, 21000, big.NewInt(40000), big.NewInt(25000), normalKey1) - // dynNormTx2: tipCap=30000 >= headerGasTip(10000, 25000) ✓, feeCap=45000 >= headerGasTip(25000) + baseFee(10000) = 35000 ✓ - // headerGasTip=10000: effectiveTip=min(10000, 45000-10000)=min(10000, 35000)=10000 - // headerGasTip=25000: effectiveTip=min(25000, 45000-10000)=min(25000, 35000)=25000 + // dynNormTx2: headerGasTip=25000: effectiveTip=min(25000, 45000-10000)=min(25000, 35000)=25000 dynNormTx2 = dynamicFeeTx(0, 21000, big.NewInt(45000), big.NewInt(30000), normalKey2) - // dynNormTx3: tipCap=20000 >= headerGasTip(10000) ✓, feeCap=35000 >= headerGasTip(25000) + baseFee(10000) = 35000 ✓ - // headerGasTip=10000: effectiveTip=min(10000, 35000-10000)=min(10000, 25000)=10000 - // headerGasTip=25000: effectiveTip=min(25000, 35000-10000)=min(25000, 25000)=25000 + // dynNormTx3: headerGasTip=25000: effectiveTip=min(25000, 35000-10000)=min(25000, 25000)=25000 dynNormTx3 = dynamicFeeTx(0, 21000, big.NewInt(35000), big.NewInt(20000), normalKey3) lookup.Add(dynAuthTx1, false)