blobpool ticket implementation

This commit is contained in:
healthykim 2026-01-27 20:15:59 +09:00
parent 1022c7637d
commit 3cac28491c
16 changed files with 638 additions and 44 deletions

138
core/TicketAllocator.sol Normal file
View file

@ -0,0 +1,138 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.33;
contract TicketAllocator {
address constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE;
error NotEnoughTickets();
error NotSystemContract();
struct Ticket {
uint16 amount;
uint256 blockNumber;
}
mapping(address => Ticket[]) public queue;
mapping(address => uint256) public head;
mapping(address => uint16) public balance;
address[] public senders; // list of senders with >0 balance
uint16 public constant TOTAL_TICKETS = 21;
uint16 public constant TIMEOUT = 2;
uint256 private leftTickets;
uint256 private lastRequestBlock;
function requestTickets(address sender, uint16 numTickets) external {
uint256 currentBlock = block.number;
if (currentBlock != lastRequestBlock) {
leftTickets = TOTAL_TICKETS;
lastRequestBlock = currentBlock;
}
if (numTickets > leftTickets) {
revert NotEnoughTickets();
}
if (balance[sender] == 0) {
senders.push(sender);
}
queue[sender].push(Ticket({
amount: numTickets,
blockNumber: currentBlock
}));
balance[sender] += numTickets;
leftTickets -= numTickets;
}
function checkBalance(address sender) public view returns(uint) {
return balance[sender];
}
// System call:
// 1. remove expired and used tickets
// - for used tickets, the oldest ticket is removed first
// 3. return the overall sender balances
fallback(bytes calldata input) external returns (bytes memory) {
if (msg.sender != SYSTEM_ADDRESS) revert NotSystemContract();
address[] memory usedSenders;
uint16[] memory usedAmounts;
if (input.length > 0) {
(usedSenders, usedAmounts) = abi.decode(input, (address[], uint16[]));
}
for (uint256 s = 0; s < senders.length; ) {
address sender = senders[s];
Ticket[] storage senderQueue = queue[sender];
uint256 senderHead = head[sender];
// find the number of used tickets of this sender
uint16 used = 0;
for (uint16 i = 0; i < usedSenders.length; i++) {
if (usedSenders[i] == sender) {
used = usedAmounts[i];
break;
}
}
while (senderHead < senderQueue.length) {
Ticket storage ticket = senderQueue[senderHead];
if (ticket.amount == 0) {
senderHead++;
continue;
}
if (ticket.blockNumber + TIMEOUT < block.number) {
// remove expired tickets (comsume used tickets if possible)
balance[sender] -= ticket.amount;
if (used > 0) {
// used = max(0, used - t.amount)
uint16 deduct = used > ticket.amount ? ticket.amount : used;
used -= deduct;
}
ticket.amount = 0;
senderHead++;
} else if (used > 0) {
// remove used ticekts (for now we don't revert when the ticket amount is less than used tickets)
uint16 deduct = used > ticket.amount ? ticket.amount : used;
ticket.amount -= uint16(deduct);
balance[sender] -= deduct;
used -= deduct;
if (ticket.amount == 0) senderHead++;
} else {
break;
}
}
head[sender] = senderHead;
// remove the sender from balance map if its balance became 0
if (balance[sender] == 0) {
senders[s] = senders[senders.length - 1];
senders.pop();
continue; // process swapped-in sender at same index (no s++)
}
s++;
}
// return the active senders and their balances
address[] memory activeSenders = new address[](senders.length);
uint16[] memory activeBalances = new uint16[](senders.length);
for (uint256 i = 0; i < senders.length; i++) {
activeSenders[i] = senders[i];
activeBalances[i] = balance[senders[i]];
}
return abi.encode(activeSenders, activeBalances);
}
}

View file

@ -350,6 +350,9 @@ type BlockChain struct {
txLookupLock sync.RWMutex
txLookupCache *lru.Cache[common.Hash, txLookup]
ticketBlocks map[*big.Int][]common.Hash // `ticketBlocks` is block hashes mapped by its number, whose ticket information is cached.
tickets map[common.Hash]map[common.Address]uint16 // `tickets` is a cache of ticket balances processed for each leaf block.
stopping atomic.Bool // false if chain is running, true when stopped
procInterrupt atomic.Bool // interrupt signaler for block processing
@ -407,6 +410,8 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine,
receiptsCache: lru.NewCache[common.Hash, []*types.Receipt](receiptsCacheLimit),
blockCache: lru.NewCache[common.Hash, *types.Block](blockCacheLimit),
txLookupCache: lru.NewCache[common.Hash, txLookup](txLookupCacheLimit),
ticketBlocks: make(map[*big.Int][]common.Hash),
tickets: make(map[common.Hash]map[common.Address]uint16),
engine: engine,
logger: cfg.VmConfig.Tracer,
slowBlockThreshold: cfg.SlowBlockThreshold,
@ -801,9 +806,23 @@ func (bc *BlockChain) SetFinalized(header *types.Header) {
if header != nil {
rawdb.WriteFinalizedBlockHash(bc.db, header.Hash())
headFinalizedBlockGauge.Update(int64(header.Number.Uint64()))
// remove ticket balances whose block number is smaller than
// finalized block
for n, hashes := range bc.ticketBlocks {
if n.Cmp(header.Number) < 0 {
for _, hash := range hashes {
delete(bc.tickets, hash)
}
delete(bc.ticketBlocks, n)
}
}
} else {
rawdb.WriteFinalizedBlockHash(bc.db, common.Hash{})
headFinalizedBlockGauge.Update(0)
bc.ticketBlocks = make(map[*big.Int][]common.Hash)
bc.tickets = make(map[common.Hash]map[common.Address]uint16)
}
}
@ -1111,6 +1130,8 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha
bc.receiptsCache.Purge()
bc.blockCache.Purge()
bc.txLookupCache.Purge()
bc.tickets = make(map[common.Hash]map[common.Address]uint16)
bc.ticketBlocks = make(map[*big.Int][]common.Hash)
// Clear safe block, finalized block if needed
headBlock := bc.CurrentBlock()
@ -2257,6 +2278,26 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s
if err != nil {
return nil, err
}
// Add ticket balances of this block and remove the parent block data
bc.tickets[block.Hash()] = res.Tickets
if _, ok := bc.ticketBlocks[block.Number()]; !ok {
bc.ticketBlocks[block.Number()] = make([]common.Hash, 0)
}
bc.ticketBlocks[block.Number()] = append(bc.ticketBlocks[block.Number()], block.Hash())
if _, ok := bc.tickets[block.ParentHash()]; ok {
delete(bc.tickets, block.ParentHash())
if hashes := bc.ticketBlocks[block.Number().Sub(block.Number(), big.NewInt(1))]; len(hashes) != 0 {
for i, hash := range hashes {
if hash.Cmp(block.ParentHash()) == 0 {
hashes[i] = hashes[len(hashes)-1]
hashes = hashes[:len(hashes)-1]
}
}
}
}
// Report the collected witness statistics
if witnessStats != nil {
witnessStats.ReportMetrics(block.NumberU64())

View file

@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/core/state/snapshot"
"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/event"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
@ -353,6 +354,35 @@ func (bc *BlockChain) GetCanonicalTransaction(hash common.Hash) (*rawdb.LegacyTx
return lookup, tx
}
func (bc *BlockChain) GetTicketBalance(hash common.Hash, statedb *state.StateDB) map[common.Address]uint16 {
if bc.tickets[hash] != nil {
return bc.tickets[hash]
}
result := make(map[common.Address]uint16)
senders := statedb.GetState(params.BlobTicketAllocationAddress, common.BigToHash(params.BlobTicketSenderSlot)).Big().Int64()
base := crypto.Keccak256Hash(common.LeftPadBytes(params.BlobTicketSenderSlot.Bytes(), 32)).Big()
for i := range senders {
key := common.BigToHash(base.Add(base, big.NewInt(i)))
sender := common.BytesToAddress(statedb.GetState(params.BlobTicketAllocationAddress, key).Bytes())
key = crypto.Keccak256Hash(
append(
common.LeftPadBytes(sender.Bytes(), 32),
common.LeftPadBytes(params.BlobTicketBalanceSlot.Bytes(), 32)...,
),
)
balance := statedb.GetState(params.BlobTicketAllocationAddress, key).Big().Uint64()
result[sender] = uint16(balance)
}
return result
}
// TxIndexDone returns true if the transaction indexer has finished indexing.
func (bc *BlockChain) TxIndexDone() bool {
progress, err := bc.TxIndexProgress()

View file

@ -31,6 +31,7 @@ import (
"time"
"github.com/davecgh/go-spew/spew"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/beacon"
@ -42,6 +43,7 @@ import (
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/core/vm/program"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/eth/tracers/logger"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/ethdb/pebble"
@ -4188,6 +4190,128 @@ func TestEIP7702(t *testing.T) {
}
}
func TestBlobTickets(t *testing.T) {
var (
key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
config = *params.MergedTestChainConfig
signer = types.LatestSigner(&config)
engine = beacon.New(ethash.NewFaker())
ticketAmount = uint16(3)
)
gspec := &Genesis{
Config: &config,
Alloc: types.GenesisAlloc{
addr1: {Balance: big.NewInt(9999900000000000)},
params.BlobTicketAllocationAddress: {Code: params.BlobTicketAllocationCode},
},
}
_, blocks, _ := GenerateChainWithGenesis(gspec, engine, 4, func(i int, bg *BlockGen) {
switch i {
case 0: // create ticket request
selector := crypto.Keccak256([]byte("requestTickets(address,uint16)"))[:4]
uint16Type, _ := abi.NewType("uint16", "", nil)
addressType, _ := abi.NewType("address", "", nil)
args := abi.Arguments{{Type: addressType}, {Type: uint16Type}}
data, _ := args.Pack(addr1, ticketAmount)
data = append(selector, data...)
txdata := &types.DynamicFeeTx{
ChainID: gspec.Config.ChainID,
Nonce: 0,
To: &params.BlobTicketAllocationAddress,
Gas: 500000,
GasFeeCap: big.NewInt(875000000),
GasTipCap: big.NewInt(1),
Data: data,
}
tx := types.MustSignNewTx(key1, signer, txdata)
bg.AddTx(tx)
case 1: // create ticket usage
var (
blobs = make([]kzg4844.Blob, 1)
commitments []kzg4844.Commitment
proofs []kzg4844.Proof
)
blobs[0][0] = 0x1
c, _ := kzg4844.BlobToCommitment(&blobs[0])
p, _ := kzg4844.ComputeBlobProof(&blobs[0], c)
commitments = append(commitments, c)
proofs = append(proofs, p)
sidecar := types.NewBlobTxSidecar(types.BlobSidecarVersion0, blobs, commitments, proofs)
txdata := &types.BlobTx{
ChainID: uint256.MustFromBig(config.ChainID),
Nonce: 1,
Gas: 500000,
GasFeeCap: uint256.NewInt(875000000),
GasTipCap: uint256.NewInt(1),
BlobFeeCap: uint256.NewInt(1),
BlobHashes: sidecar.BlobHashes(),
}
tx := types.MustSignNewTx(key1, signer, txdata)
bg.AddTx(tx)
}
})
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), gspec, engine, nil)
if err != nil {
t.Fatalf("failed to create tester chain: %v", err)
}
defer chain.Stop()
// check the balance of requester
if n, err := chain.InsertChain(blocks[:1]); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
balance, _ := chain.tickets[blocks[0].Hash()]
amount := balance[addr1]
if amount != ticketAmount {
t.Fatalf("wrong ticket amount: expected %d, got %d", ticketAmount, amount)
}
// test ticket usage
if n, err := chain.InsertChain(blocks[1:2]); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
balance, _ = chain.tickets[blocks[1].Hash()]
amount = balance[addr1]
if amount != ticketAmount-1 {
t.Fatalf("wrong ticket amount: expected %d, got %d", ticketAmount-1, amount)
}
// test expiry
if n, err := chain.InsertChain(blocks[2:]); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
balance, _ = chain.tickets[blocks[3].Hash()]
amount = balance[addr1]
if amount != 0 {
t.Fatalf("wrong ticket amount: expected %d, got %d", 0, amount)
}
// previous block ticket balance
state, _ := chain.StateAt(blocks[0].Root())
balance = chain.GetTicketBalance(blocks[0].Hash(), state)
amount = balance[addr1]
if amount != ticketAmount {
t.Fatalf("wrong ticket amount: expected %d, got %d", ticketAmount, amount)
}
state, _ = chain.StateAt(blocks[1].Root())
balance = chain.GetTicketBalance(blocks[1].Hash(), state)
amount = balance[addr1]
if amount != ticketAmount-1 {
t.Fatalf("wrong ticket amount: expected %d, got %d", ticketAmount-1, amount)
}
}
// Tests the scenario that the synchronization target in snap sync has been changed
// with a chain reorg at the tip. In this case the reorg'd segment should be unmarked
// with canonical flags.

View file

@ -408,6 +408,22 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
b.header.RequestsHash = &reqHash
}
if config.IsPrague(b.header.Number, b.header.Time) {
usedAddress := make([]common.Address, 0)
usedAmount := make([]uint16, 0)
signer := b.Signer()
for i, tx := range b.txs {
if tx.Type() == types.BlobTxType && b.receipts[i].Status == types.ReceiptStatusSuccessful {
from, _ := types.Sender(signer, tx)
usedAddress = append(usedAddress, from)
usedAmount = append(usedAmount, uint16(len(tx.BlobHashes())))
}
}
blockContext := NewEVMBlockContext(b.header, cm, &b.header.Coinbase)
evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{})
ProcessTickets(usedAddress, usedAmount, evm)
}
body := types.Body{Transactions: b.txs, Uncles: b.uncles, Withdrawals: b.withdrawals}
block, err := b.engine.FinalizeAndAssemble(cm, b.header, statedb, &body, b.receipts)
if err != nil {

View file

@ -20,6 +20,7 @@ import (
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc"
"github.com/ethereum/go-ethereum/core/state"
@ -94,6 +95,8 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
ProcessParentBlockHash(block.ParentHash(), evm)
}
var usedAddress []common.Address
var usedAmount []uint16
// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
msg, err := TransactionToMessage(tx, signer, header.BaseFee)
@ -108,9 +111,15 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
}
receipts = append(receipts, receipt)
allLogs = append(allLogs, receipt.Logs...)
if tx.Type() == types.BlobTxType && receipt.Status == types.ReceiptStatusSuccessful {
usedAddress = append(usedAddress, msg.From)
usedAmount = append(usedAmount, uint16(len(msg.BlobHashes)))
}
}
// Read requests if Prague is enabled.
var requests [][]byte
var tickets map[common.Address]uint16
if config.IsPrague(block.Number(), block.Time()) {
requests = [][]byte{}
// EIP-6110
@ -125,6 +134,9 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
if err := ProcessConsolidationQueue(&requests, evm); err != nil {
return nil, fmt.Errorf("failed to process consolidation queue: %w", err)
}
// This should be moved to after having fork
tickets = ProcessTickets(usedAddress, usedAmount, evm)
}
// Finalize the block, applying any consensus engine specific extras (e.g. block rewards)
@ -135,6 +147,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
Requests: requests,
Logs: allLogs,
GasUsed: *usedGas,
Tickets: tickets,
}, nil
}
@ -337,6 +350,72 @@ func ParseDepositLogs(requests *[][]byte, logs []*types.Log, config *params.Chai
return nil
}
// ProcessTickets updates ticket balances and return the balances
func ProcessTickets(usedAddress []common.Address, usedAmount []uint16, evm *vm.EVM) map[common.Address]uint16 {
if tracer := evm.Config.Tracer; tracer != nil {
onSystemCallStart(tracer, evm.GetVMContext())
if tracer.OnSystemCallEnd != nil {
defer tracer.OnSystemCallEnd()
}
}
addressType, _ := abi.NewType("address[]", "", nil)
uint16Type, _ := abi.NewType("uint16[]", "", nil)
var data []byte
if len(usedAddress) > 0 {
// abi.encode(address[], uint16[])
arguments := abi.Arguments{
{Type: addressType},
{Type: uint16Type},
}
data, _ = arguments.Pack(usedAddress, usedAmount)
}
msg := &Message{
From: params.SystemAddress,
GasLimit: 30_000_000,
GasPrice: common.Big0,
GasFeeCap: common.Big0,
GasTipCap: common.Big0,
To: &params.BlobTicketAllocationAddress,
Data: data,
}
evm.SetTxContext(NewEVMTxContext(msg))
res, _, _ := evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560)
evm.StateDB.Finalise(true)
arguments := abi.Arguments{
{Type: addressType},
{Type: uint16Type},
}
values, err := arguments.Unpack(res)
if err != nil {
return nil
}
if len(values) < 2 {
return nil
}
senders, ok1 := values[0].([]common.Address)
amounts, ok2 := values[1].([]uint16)
if !ok1 || !ok2 || len(senders) != len(amounts) {
return nil
}
result := make(map[common.Address]uint16)
for i, sender := range senders {
result[sender] = amounts[i]
}
return result
}
func onSystemCallStart(tracer *tracing.Hooks, ctx *tracing.VMContext) {
if tracer.OnSystemCallStartV2 != nil {
tracer.OnSystemCallStartV2(ctx)

View file

@ -75,14 +75,6 @@ const (
// Note: if you increase this, validation will fail on txMaxSize.
maxBlobsPerTx = params.BlobTxMaxBlobs
// maxTxsPerAccount is the maximum number of blob transactions admitted from
// a single account. The limit is enforced to minimize the DoS potential of
// a private tx cancelling publicly propagated blobs.
//
// Note, transactions resurrected by a reorg are also subject to this limit,
// so pushing it down too aggressively might make resurrections non-functional.
maxTxsPerAccount = 16
// pendingTransactionStore is the subfolder containing the currently queued
// blob transactions.
pendingTransactionStore = "queue"
@ -358,6 +350,8 @@ type BlobPool struct {
discoverFeed event.Feed // Event feed to send out new tx events on pool discovery (reorg excluded)
insertFeed event.Feed // Event feed to send out new tx events on pool inclusion (reorg included)
tickets map[common.Address]uint16
lock sync.RWMutex // Mutex protecting the pool during reorg handling
}
@ -378,6 +372,7 @@ func New(config Config, chain BlockChain, hasPendingAuth func(common.Address) bo
spent: make(map[common.Address]*uint256.Int),
gapped: make(map[common.Address][]*types.Transaction),
gappedSource: make(map[common.Hash]common.Address),
tickets: make(map[common.Address]uint16),
}
}
@ -424,6 +419,8 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
p.head.Store(head)
p.state = state
p.tickets = p.chain.GetTicketBalance(head.Hash(), state)
// Create new slotter for pre-Osaka blob configuration.
slotter := newSlotter(params.BlobTxMaxBlobs)
@ -563,7 +560,7 @@ func (p *BlobPool) parseTransaction(id uint64, size uint32, blob []byte) error {
}
// recheck verifies the pool's content for a specific account and drops anything
// that does not fit anymore (dangling or filled nonce, overdraft).
// that does not fit anymore (dangling or filled nonce, overdraft, ticketless).
func (p *BlobPool) recheck(addr common.Address, inclusions map[common.Hash]uint64) {
// Sort the transactions belonging to the account so reinjects can be simpler
txs := p.index[addr]
@ -769,16 +766,19 @@ func (p *BlobPool) recheck(addr common.Address, inclusions map[common.Hash]uint6
}
}
}
// Sanity check that no account can have more queued transactions than the
// DoS protection threshold.
if len(txs) > maxTxsPerAccount {
// Evict the highest nonce transactions until the pending set falls under
// the account's transaction cap
// If the number of txs is bigger than the sender's ticket balance,
// drop overflown amount from the sender
var blobs int
for _, tx := range txs {
blobs += len(tx.vhashes)
}
if ticket, ok := p.tickets[addr]; ok && blobs > int(ticket) {
var (
ids []uint64
nonces []uint64
)
for len(txs) > maxTxsPerAccount {
for blobs > int(ticket) {
last := txs[len(txs)-1]
txs[len(txs)-1] = nil
txs = txs[:len(txs)-1]
@ -789,10 +789,12 @@ func (p *BlobPool) recheck(addr common.Address, inclusions map[common.Hash]uint6
p.spent[addr] = new(uint256.Int).Sub(p.spent[addr], last.costCap)
p.stored -= uint64(last.storageSize)
p.lookup.untrack(last)
blobs -= len(last.vhashes)
}
p.index[addr] = txs
log.Warn("Dropping overcapped blob transactions", "from", addr, "kept", len(txs), "drop", nonces, "ids", ids)
log.Trace("Dropping ticketless blob transactions", "from", addr, "dropped", len(ids), "left", len(txs))
dropOvercappedMeter.Mark(int64(len(ids)))
for _, id := range ids {
@ -860,6 +862,8 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) {
p.head.Store(newHead)
p.state = statedb
p.tickets = p.chain.GetTicketBalance(newHead.Hash(), p.state)
// Run the reorg between the old and new head and figure out which accounts
// need to be rechecked and which transactions need to be readded
if reinject, inclusions := p.reorg(oldHead, newHead); reinject != nil {
@ -913,6 +917,10 @@ func (p *BlobPool) reorg(oldHead, newHead *types.Header) (map[common.Address][]*
oldNum := oldHead.Number.Uint64()
newNum := newHead.Number.Uint64()
// todo: This can happen by chain rewind (e.g. setHead)
// todo: Why don't we clear the txpool in here, given that
// todo: most txs could be gapped ?
// todo: Where those txs is being rechecked ?
if depth := uint64(math.Abs(float64(oldNum) - float64(newNum))); depth > 64 {
return nil, nil
}
@ -1222,11 +1230,15 @@ func (p *BlobPool) validateTx(tx *types.Transaction) error {
return p.state.GetNonce(addr) + uint64(len(p.index[addr]))
},
UsedAndLeftSlots: func(addr common.Address) (int, int) {
have := len(p.index[addr])
if have >= maxTxsPerAccount {
have := 0
for _, meta := range p.index[addr] {
have += len(meta.vhashes)
}
if have >= int(p.tickets[addr]) {
return have, 0
}
return have, maxTxsPerAccount - have
return have, int(p.tickets[addr]) - have
},
ExistingExpenditure: func(addr common.Address) *big.Int {
if spent := p.spent[addr]; spent != nil {
@ -1970,7 +1982,9 @@ func (p *BlobPool) gappedAllowance(addr common.Address) int {
nonce := p.state.GetNonce(addr)
allowance := int(math.Log10(float64(nonce + 1)))
// Cap the allowance to the remaining pool space
return min(allowance, maxTxsPerAccount-len(p.index[addr])) - len(p.gapped[addr])
// We consider the ticket balance here, where one ticket is treated as a tx with a single blob.
// The reason is that we cannot know how many tickets filling txs will use.
return min(allowance, int(p.tickets[addr])-len(p.index[addr])) - len(p.gapped[addr])
}
// evictGapped removes the old transactions from the gapped reorder buffer.

View file

@ -90,6 +90,8 @@ type testBlockChain struct {
blocks map[uint64]*types.Block
blockTime *uint64
tickets map[common.Address]uint16
}
func (bc *testBlockChain) Config() *params.ChainConfig {
@ -184,6 +186,10 @@ func (bc *testBlockChain) StateAt(common.Hash) (*state.StateDB, error) {
return bc.statedb, nil
}
func (bc *testBlockChain) GetTicketBalance(common.Hash, *state.StateDB) map[common.Address]uint16 {
return bc.tickets
}
// reserver is a utility struct to sanity check that accounts are
// properly reserved by the blobpool (no duplicate reserves or unreserves).
type reserver struct {
@ -478,7 +484,7 @@ func verifyBlobRetrievals(t *testing.T, pool *BlobPool) {
// - 4. All transactions after an already included nonce must be dropped
// - 5. All transactions after an underpriced one (including it) must be dropped
// - 6. All transactions after an overdrafting sequence must be dropped
// - 7. All transactions exceeding the per-account limit must be dropped
// - 7. All transactions exceeding the ticket limit must be dropped
//
// Furthermore, some strange corner-cases can also occur after a crash, as Billy's
// simplicity also allows it to resurrect past deleted entities:
@ -515,6 +521,7 @@ func TestOpenDrops(t *testing.T) {
blob, _ := rlp.EncodeToBytes(tx)
badsig, _ := store.Put(blob)
tickets := make(map[common.Address]uint16)
// Insert a sequence of transactions with a nonce gap in between to verify
// that anything gapped will get evicted (case 3).
var (
@ -533,6 +540,7 @@ func TestOpenDrops(t *testing.T) {
} else {
gapped[id] = struct{}{}
}
tickets[crypto.PubkeyToAddress(gapper.PublicKey)] += 1
}
// Insert a sequence of transactions with a gapped starting nonce to verify
// that the entire set will get dropped (case 3).
@ -546,6 +554,7 @@ func TestOpenDrops(t *testing.T) {
id, _ := store.Put(blob)
dangling[id] = struct{}{}
tickets[crypto.PubkeyToAddress(dangler.PublicKey)] += 1
}
// Insert a sequence of transactions with already passed nonces to veirfy
// that the entire set will get dropped (case 4).
@ -559,6 +568,7 @@ func TestOpenDrops(t *testing.T) {
id, _ := store.Put(blob)
filled[id] = struct{}{}
tickets[crypto.PubkeyToAddress(filler.PublicKey)] += 1
}
// Insert a sequence of transactions with partially passed nonces to verify
// that the included part of the set will get dropped (case 4).
@ -576,6 +586,7 @@ func TestOpenDrops(t *testing.T) {
} else {
overlapped[id] = struct{}{}
}
tickets[crypto.PubkeyToAddress(overlapper.PublicKey)] += 1
}
// Insert a sequence of transactions with an underpriced first to verify that
// the entire set will get dropped (case 5).
@ -594,6 +605,7 @@ func TestOpenDrops(t *testing.T) {
id, _ := store.Put(blob)
underpaid[id] = struct{}{}
tickets[crypto.PubkeyToAddress(underpayer.PublicKey)] += 1
}
// Insert a sequence of transactions with an underpriced in between to verify
@ -617,6 +629,7 @@ func TestOpenDrops(t *testing.T) {
} else {
outpriced[id] = struct{}{}
}
tickets[crypto.PubkeyToAddress(outpricer.PublicKey)] += 1
}
// Insert a sequence of transactions fully overdrafted to verify that the
// entire set will get invalidated (case 6).
@ -635,6 +648,7 @@ func TestOpenDrops(t *testing.T) {
id, _ := store.Put(blob)
exceeded[id] = struct{}{}
tickets[crypto.PubkeyToAddress(exceeder.PublicKey)] += 1
}
// Insert a sequence of transactions partially overdrafted to verify that part
// of the set will get invalidated (case 6).
@ -657,18 +671,24 @@ func TestOpenDrops(t *testing.T) {
} else {
overdrafted[id] = struct{}{}
}
tickets[crypto.PubkeyToAddress(overdrafter.PublicKey)] += 1
}
// Insert a sequence of transactions overflowing the account cap to verify
// that part of the set will get invalidated (case 7).
var (
overcapper, _ = crypto.GenerateKey()
overcapped = make(map[uint64]struct{})
valid = uint16(10)
)
for nonce := uint64(0); nonce < maxTxsPerAccount+3; nonce++ {
// Initialise tickets to be used for validation
tickets[crypto.PubkeyToAddress(overcapper.PublicKey)] = valid
for nonce := uint64(0); nonce < uint64(valid)+3; nonce++ {
blob, _ := rlp.EncodeToBytes(makeTx(nonce, 1, 1, 1, overcapper))
id, _ := store.Put(blob)
if nonce < maxTxsPerAccount {
if nonce < uint64(valid) {
valids[id] = struct{}{}
} else {
overcapped[id] = struct{}{}
@ -691,6 +711,7 @@ func TestOpenDrops(t *testing.T) {
duplicated[id] = struct{}{}
}
}
tickets[crypto.PubkeyToAddress(duplicater.PublicKey)] += 1
}
// Insert a batch of duplicated nonces to verify that only one of each will
// remain (case 9).
@ -708,6 +729,7 @@ func TestOpenDrops(t *testing.T) {
} else {
repeated[id] = struct{}{}
}
tickets[crypto.PubkeyToAddress(repeater.PublicKey)] += 1
}
}
store.Close()
@ -734,6 +756,7 @@ func TestOpenDrops(t *testing.T) {
basefee: uint256.NewInt(params.InitialBaseFee),
blobfee: uint256.NewInt(params.BlobTxMinBlobGasprice),
statedb: statedb,
tickets: tickets,
}
pool := New(Config{Datadir: storage}, chain, nil)
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
@ -1310,6 +1333,7 @@ func TestBlobCountLimit(t *testing.T) {
basefee: uint256.NewInt(1050),
blobfee: uint256.NewInt(105),
statedb: statedb,
tickets: map[common.Address]uint16{addr1: 6, addr2: 7},
}
pool := New(Config{Datadir: t.TempDir()}, chain, nil)
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
@ -1325,10 +1349,10 @@ func TestBlobCountLimit(t *testing.T) {
// Check that first succeeds second fails.
if errs[0] != nil {
t.Fatalf("expected tx with 7 blobs to succeed, got %v", errs[0])
t.Fatalf("expected tx with 6 blobs to succeed, got %v", errs[0])
}
if !errors.Is(errs[1], txpool.ErrTxBlobLimitExceeded) {
t.Fatalf("expected tx with 8 blobs to fail, got: %v", errs[1])
t.Fatalf("expected tx with 7 blobs to fail, got: %v", errs[1])
}
verifyPoolInternals(t, pool)
@ -1587,11 +1611,6 @@ func TestAdd(t *testing.T) {
tx: makeUnsignedTx(15, 10, 10, 10),
err: nil,
},
{ // New account, 16 pooled tx, 0 slots left: reject nonce 16 with overcap
from: "alice",
tx: makeUnsignedTx(16, 1, 1, 1),
err: txpool.ErrAccountLimitExceeded,
},
},
},
// Previously existing transactions should be allowed to be replaced iff
@ -1754,6 +1773,10 @@ func TestAdd(t *testing.T) {
basefee: uint256.NewInt(1050),
blobfee: uint256.NewInt(105),
statedb: statedb,
tickets: make(map[common.Address]uint16),
}
for _, add := range tt.adds {
chain.tickets[addrs[add.from]] += uint16(len(add.tx.BlobHashes))
}
pool := New(Config{Datadir: storage}, chain, nil)
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
@ -1851,6 +1874,8 @@ func TestGetBlobs(t *testing.T) {
blob1, _ = rlp.EncodeToBytes(tx1)
blob2, _ = rlp.EncodeToBytes(tx2)
blob3, _ = rlp.EncodeToBytes(tx3)
tickets = map[common.Address]uint16{addr1: 6, addr2: 6, addr3: 6}
)
// Write the two safely sized txs to store. note: although the store is
@ -1889,6 +1914,7 @@ func TestGetBlobs(t *testing.T) {
basefee: uint256.NewInt(1050),
blobfee: uint256.NewInt(105),
statedb: statedb,
tickets: tickets,
}
pool := New(Config{Datadir: storage}, chain, nil)
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
@ -2089,6 +2115,7 @@ func benchmarkPoolPending(b *testing.B, datacap uint64) {
basefee: uint256.NewInt(basefee),
blobfee: uint256.NewInt(blobfee),
statedb: statedb,
tickets: make(map[common.Address]uint16),
}
pool = New(Config{Datadir: ""}, chain, nil)
)
@ -2112,6 +2139,7 @@ func benchmarkPoolPending(b *testing.B, datacap uint64) {
b.Fatal(err)
}
statedb.AddBalance(addr, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified)
chain.tickets[addr] += 1
pool.add(tx)
}
statedb.Commit(0, true, false)

View file

@ -41,4 +41,6 @@ type BlockChain interface {
// StateAt returns a state database for a given root hash (generally the head).
StateAt(root common.Hash) (*state.StateDB, error)
GetTicketBalance(hash common.Hash, statedb *state.StateDB) map[common.Address]uint16
}

View file

@ -292,8 +292,14 @@ func ValidateTransactionWithState(tx *types.Transaction, signer types.Signer, op
// overflow the number of permitted transactions from a single account
// (i.e. max cancellable via out-of-bound transaction).
if opts.UsedAndLeftSlots != nil {
if used, left := opts.UsedAndLeftSlots(from); left <= 0 {
return fmt.Errorf("%w: pooled %d txs", ErrAccountLimitExceeded, used)
if tx.Type() == types.BlobTxType {
if used, left := opts.UsedAndLeftSlots(from); left < len(tx.BlobHashes()) {
return fmt.Errorf("%w: pooled %d blobs", ErrAccountLimitExceeded, used)
}
} else {
if used, left := opts.UsedAndLeftSlots(from); left <= 0 {
return fmt.Errorf("%w: pooled %d txs", ErrAccountLimitExceeded, used)
}
}
}
}

View file

@ -19,6 +19,7 @@ package core
import (
"sync/atomic"
"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/core/vm"
@ -57,4 +58,5 @@ type ProcessResult struct {
Requests [][]byte
Logs []*types.Log
GasUsed uint64
Tickets map[common.Address]uint16
}

View file

@ -31,6 +31,7 @@ import (
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
@ -1897,16 +1898,57 @@ func newGetBlobEnv(t testing.TB, version byte) (*node.Node, *ConsensusAPI) {
addr1: {Balance: testBalance},
addr2: {Balance: testBalance},
addr3: {Balance: testBalance},
params.BlobTicketAllocationAddress: {Code: params.BlobTicketAllocationCode},
},
Difficulty: common.Big0,
}
n, ethServ := startEthService(t, gspec, nil)
// Generate ticket request blocks
engine := beacon.New(ethash.NewFaker())
_, ticketBlocks, _ := core.GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *core.BlockGen) {
signers := []struct {
addr common.Address
key *ecdsa.PrivateKey
}{
{addr1, key1},
{addr2, key2},
{addr3, key3},
}
for _, signer := range signers {
selector := crypto.Keccak256([]byte("requestTickets(address,uint16)"))[:4]
uint16Type, _ := abi.NewType("uint16", "", nil)
addressType, _ := abi.NewType("address", "", nil)
args := abi.Arguments{{Type: addressType}, {Type: uint16Type}}
data, _ := args.Pack(signer.addr, uint16(2))
data = append(selector, data...)
txdata := &types.DynamicFeeTx{
ChainID: config.ChainID,
Nonce: b.TxNonce(signer.addr),
To: &params.BlobTicketAllocationAddress,
Gas: 500000,
GasFeeCap: big.NewInt(params.InitialBaseFee * 2),
GasTipCap: big.NewInt(params.GWei),
Data: data,
}
tx := types.MustSignNewTx(signer.key, types.LatestSigner(&config), txdata)
b.AddTx(tx)
}
})
n, ethServ := startEthService(t, gspec, ticketBlocks)
// fill blob txs into the pool
tx1 := makeMultiBlobTx(&config, 0, 2, 0, key1, version) // blob[0, 2)
tx2 := makeMultiBlobTx(&config, 0, 2, 2, key2, version) // blob[2, 4)
tx3 := makeMultiBlobTx(&config, 0, 2, 4, key3, version) // blob[4, 6)
ethServ.TxPool().Add([]*types.Transaction{tx1, tx2, tx3}, true)
tx1 := makeMultiBlobTx(&config, 1, 2, 0, key1, version) // blob[0, 2)
tx2 := makeMultiBlobTx(&config, 1, 2, 2, key2, version) // blob[2, 4)
tx3 := makeMultiBlobTx(&config, 1, 2, 4, key3, version) // blob[4, 6)
errs := ethServ.TxPool().Add([]*types.Transaction{tx1, tx2, tx3}, true)
for _, err := range errs {
if err != nil {
t.Fatal("GetBlob environment generation failed", err)
}
}
api := newConsensusAPIWithoutHeartbeat(ethServ)
return n, api

View file

@ -31,6 +31,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core"
"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/txpool/blobpool"
"github.com/ethereum/go-ethereum/core/txpool/legacypool"
@ -69,6 +70,14 @@ func newTestBackend(blocks int) *testBackend {
return newTestBackendWithGenerator(blocks, false, false, nil)
}
type testChain struct {
*core.BlockChain
}
func (c *testChain) GetTicketBalance(hash common.Hash, statedb *state.StateDB) map[common.Address]uint16 {
return map[common.Address]uint16{testAddr: 1000}
}
// newTestBackendWithGenerator creates a chain with a number of explicitly defined blocks and
// wraps it into a mock backend.
func newTestBackendWithGenerator(blocks int, shanghai bool, cancun bool, generator func(int, *core.BlockGen)) *testBackend {
@ -134,9 +143,9 @@ func newTestBackendWithGenerator(blocks int, shanghai bool, cancun bool, generat
storage, _ := os.MkdirTemp("", "blobpool-")
defer os.RemoveAll(storage)
blobPool := blobpool.New(blobpool.Config{Datadir: storage}, chain, nil)
legacyPool := legacypool.New(txconfig, chain)
txpool, _ := txpool.New(txconfig.PriceLimit, chain, []txpool.SubPool{legacyPool, blobPool})
blobPool := blobpool.New(blobpool.Config{Datadir: storage}, &testChain{chain}, nil)
legacyPool := legacypool.New(txconfig, &testChain{chain})
txpool, _ := txpool.New(txconfig.PriceLimit, &testChain{chain}, []txpool.SubPool{legacyPool, blobPool})
return &testBackend{
db: db,

View file

@ -23,7 +23,10 @@ import (
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
)
// TestTransactionRollbackBehavior tests that calling Rollback on the simulated backend doesn't prevent subsequent
@ -31,14 +34,16 @@ import (
func TestTransactionRollbackBehavior(t *testing.T) {
sim := NewBackend(
types.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000000000)},
testAddr2: {Balance: big.NewInt(10000000000000000)},
testAddr: {Balance: big.NewInt(10000000000000000)},
testAddr2: {Balance: big.NewInt(10000000000000000)},
params.BlobTicketAllocationAddress: {Code: params.BlobTicketAllocationCode},
},
)
defer sim.Close()
client := sim.Client()
btx0 := testSendSignedTx(t, testKey, sim, true, 0)
allocateTickets(t, testKey, sim, 0, 10)
btx0 := testSendSignedTx(t, testKey, sim, true, 1)
tx0 := testSendSignedTx(t, testKey2, sim, false, 0)
tx1 := testSendSignedTx(t, testKey2, sim, false, 1)
@ -48,7 +53,7 @@ func TestTransactionRollbackBehavior(t *testing.T) {
t.Fatalf("all transactions were not rolled back")
}
btx2 := testSendSignedTx(t, testKey, sim, true, 0)
btx2 := testSendSignedTx(t, testKey, sim, true, 1)
tx2 := testSendSignedTx(t, testKey2, sim, false, 0)
tx3 := testSendSignedTx(t, testKey2, sim, false, 1)
@ -86,6 +91,43 @@ func testSendSignedTx(t *testing.T, key *ecdsa.PrivateKey, sim *Backend, isBlobT
return signedTx
}
func allocateTickets(t *testing.T, key *ecdsa.PrivateKey, sim *Backend, nonce uint64, amount uint16) {
t.Helper()
client := sim.Client()
ctx := context.Background()
addr := crypto.PubkeyToAddress(key.PublicKey)
chainid, _ := client.ChainID(ctx)
signer := types.LatestSignerForChainID(chainid)
selector := crypto.Keccak256([]byte("requestTickets(address,uint16)"))[:4]
uint16Type, _ := abi.NewType("uint16", "", nil)
addressType, _ := abi.NewType("address", "", nil)
args := abi.Arguments{{Type: addressType}, {Type: uint16Type}}
data, _ := args.Pack(addr, amount)
data = append(selector, data...)
// Get current head
head, _ := client.HeaderByNumber(ctx, nil)
txdata := &types.DynamicFeeTx{
ChainID: chainid,
Nonce: nonce,
To: &params.BlobTicketAllocationAddress,
Gas: 500000,
GasFeeCap: new(big.Int).Add(head.BaseFee, big.NewInt(params.GWei)),
GasTipCap: big.NewInt(params.GWei),
Data: data,
}
tx := types.MustSignNewTx(key, signer, txdata)
if err := client.SendTransaction(ctx, tx); err != nil {
t.Fatalf("failed to request tickets: %v", err)
}
sim.Commit()
}
// pendingStateHasTx returns true if a given transaction was successfully included as of the latest pending state.
func pendingStateHasTx(client Client, tx *types.Transaction) bool {
ctx := context.Background()

View file

@ -70,6 +70,9 @@ type environment struct {
sidecars []*types.BlobTxSidecar
blobs int
usedAddress []common.Address
usedAmount []uint16
witness *stateless.Witness
}
@ -167,6 +170,8 @@ func (miner *Miner) generateWork(genParam *generateParams, witness bool) *newPay
return &newPayloadResult{err: err}
}
}
core.ProcessTickets(work.usedAddress, work.usedAmount, work.evm)
if requests != nil {
reqHash := types.CalcRequestsHash(requests)
work.header.RequestsHash = &reqHash
@ -335,6 +340,15 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio
env.size += txNoBlob.Size()
*env.header.BlobGasUsed += receipt.BlobGasUsed
env.tcount++
if receipt.Status == types.ReceiptStatusSuccessful {
sender, err := types.Sender(env.signer, tx)
if err != nil {
return err
}
env.usedAddress = append(env.usedAddress, sender)
env.usedAmount = append(env.usedAmount, uint16(len(tx.BlobHashes())))
}
return nil
}

File diff suppressed because one or more lines are too long