diff --git a/core/txpool/localpool/blockchain.go b/core/txpool/localpool/blockchain.go
new file mode 100644
index 000000000000..df5d069baa19
--- /dev/null
+++ b/core/txpool/localpool/blockchain.go
@@ -0,0 +1,24 @@
+package localpool
+
+import (
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/params"
+)
+
+// BlockChain defines the minimal set of methods needed to back a tx pool with
+// a chain. Exists to allow mocking the live chain out of tests.
+type BlockChain interface {
+ // Config retrieves the chain's fork configuration.
+ Config() *params.ChainConfig
+
+ // CurrentBlock returns the current head of the chain.
+ CurrentBlock() *types.Header
+
+ // GetBlock retrieves a specific block, used during pool resets.
+ GetBlock(hash common.Hash, number uint64) *types.Block
+
+ // StateAt returns a state database for a given root hash (generally the head).
+ StateAt(root common.Hash) (*state.StateDB, error)
+}
diff --git a/core/txpool/localpool/blockchain_test.go b/core/txpool/localpool/blockchain_test.go
new file mode 100644
index 000000000000..9ce312634543
--- /dev/null
+++ b/core/txpool/localpool/blockchain_test.go
@@ -0,0 +1,39 @@
+package localpool
+
+import (
+ "errors"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/params"
+)
+
+type MockBC struct {
+ currentBlock *types.Header
+ dbs map[common.Hash]*state.StateDB
+}
+
+func (m *MockBC) Config() *params.ChainConfig {
+ return params.AllDevChainProtocolChanges
+}
+
+func (m *MockBC) CurrentBlock() *types.Header {
+ return m.currentBlock
+}
+
+func (m *MockBC) GetBlock(hash common.Hash, number uint64) *types.Block {
+ return nil
+}
+
+func (m *MockBC) StateAt(root common.Hash) (*state.StateDB, error) {
+ state, ok := m.dbs[root]
+ if !ok {
+ return nil, errors.New("not found")
+ }
+ return state, nil
+}
+
+func (m *MockBC) SetState(root common.Hash, db *state.StateDB) {
+ m.dbs[root] = db
+}
diff --git a/core/txpool/localpool/localpool.go b/core/txpool/localpool/localpool.go
new file mode 100644
index 000000000000..b7772292a8c1
--- /dev/null
+++ b/core/txpool/localpool/localpool.go
@@ -0,0 +1,277 @@
+// Copyright 2023 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// Package localpool implements a transaction pool only for local transactions.
+package localpool
+
+import (
+ "errors"
+ "math/big"
+ "sync/atomic"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core"
+ "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/event"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+var (
+ errNotLocal = errors.New("non-local transaction added to local txpool")
+
+ txMaxSize uint64 = 128 * 1024 // 128KB
+)
+
+var _ txpool.SubPool = new(LocalPool)
+
+type LocalPool struct {
+ allTxs map[common.Hash]*types.Transaction
+ allAccounts map[common.Address]nonceOrderedList
+
+ signer types.Signer
+ chain BlockChain
+ currentState *state.StateDB // Current state in the blockchain head
+ currentHead atomic.Pointer[types.Header]
+
+ txFeed event.Feed
+ reserver txpool.AddressReserver
+}
+
+func NewLocalPool(chain BlockChain, signer types.Signer) (*LocalPool, error) {
+ head := chain.CurrentBlock()
+ currentState, err := chain.StateAt(head.Root)
+ if err != nil {
+ return nil, err
+ }
+ pool := LocalPool{
+ chain: chain,
+ currentState: currentState,
+ signer: signer,
+ allTxs: make(map[common.Hash]*types.Transaction),
+ allAccounts: make(map[common.Address]nonceOrderedList),
+ }
+ pool.currentHead.Store(head)
+ return &pool, nil
+}
+
+// Filter is a selector used to decide whether a transaction would be added
+// to this particular subpool.
+func (l *LocalPool) Filter(tx *types.Transaction) bool {
+ // Only disallow blob txs, all other txs should be allowed
+ return tx.Type() != types.BlobTxType
+}
+
+// Init sets the base parameters of the subpool, allowing it to load any saved
+// transactions from disk and also permitting internal maintenance routines to
+// start up.
+//
+// These should not be passed as a constructor argument - nor should the pools
+// start by themselves - in order to keep multiple subpools in lockstep with
+// one another.
+func (l *LocalPool) Init(gasTip *big.Int, head *types.Header, reserve txpool.AddressReserver) error {
+ l.reserver = reserve
+ // TODO load transactions.rlp
+ l.Reset(nil, head)
+ return nil
+}
+
+func (l *LocalPool) Close() error {
+ // todo shut down subscription
+ return nil
+}
+
+// Reset retrieves the current state of the blockchain and ensures the content
+// of the transaction pool is valid with regard to the chain state.
+func (l *LocalPool) Reset(oldHead, newHead *types.Header) {
+ newState, err := l.chain.StateAt(newHead.Root)
+ if err != nil {
+ log.Error("Could not get new state in LocalPool", "head", newHead.Hash())
+ }
+ l.currentHead.Store(newHead)
+ l.currentState = newState
+ l.runReorg()
+ // todo should I reinsert local transactions that have been mined?
+}
+
+func (l *LocalPool) runReorg() {
+ for addr, list := range l.allAccounts {
+ reorged := list.reorg(l.currentState.GetNonce(addr))
+ for _, hash := range reorged {
+ delete(l.allTxs, hash)
+ }
+ }
+}
+
+// SetGasTip updates the minimum price, since all transactions should
+// be retained, we do nothing here.
+func (l *LocalPool) SetGasTip(tip *big.Int) {}
+
+func (l *LocalPool) Has(hash common.Hash) bool {
+ _, ok := l.allTxs[hash]
+ return ok
+}
+
+func (l *LocalPool) Get(hash common.Hash) *types.Transaction {
+ return l.allTxs[hash]
+}
+
+func (l *LocalPool) Add(txs []*types.Transaction, local bool, sync bool) []error {
+ errs := make([]error, len(txs))
+ if !local {
+ // If the transactions are not local, reject all
+ for i := 0; i < len(txs); i++ {
+ errs[i] = errNotLocal
+ }
+ return errs
+ }
+ var successfulTxs = make([]*types.Transaction, 0, len(txs))
+ for i, tx := range txs {
+ err := l.add(tx)
+ errs[i] = err
+ if err == nil {
+ successfulTxs = append(successfulTxs, tx)
+ }
+ }
+ // notify all listeners about successfully added txs
+ l.txFeed.Send(core.NewTxsEvent{Txs: successfulTxs})
+ return errs
+}
+
+func (l *LocalPool) add(tx *types.Transaction) error {
+ // Ignore already known transactions
+ if _, ok := l.allTxs[tx.Hash()]; ok {
+ log.Info("Ignoring already known transaction", "hash", tx.Hash())
+ return nil
+ }
+ // Validate tx basics
+ opts := &txpool.ValidationOptions{
+ Config: l.chain.Config(),
+ Accept: 0 |
+ 1<