Skip to content

Commit

Permalink
Enforce ETX limits on blocks
Browse files Browse the repository at this point in the history
This commit adds cross-region & cross-prime ETX limits to ensure we do
not build or accept any block which emits more ETXs than the network can
handle.
  • Loading branch information
wizeguyy committed Jul 18, 2023
1 parent 2963248 commit 748e323
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 27 deletions.
8 changes: 4 additions & 4 deletions core/chain_makers.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ func (b *BlockGen) SetDifficulty(diff *big.Int) {
// further limitations on the content of transactions that can be
// added. Notably, contract code relying on the BLOCKHASH instruction
// will panic during execution.
func (b *BlockGen) AddTx(tx *types.Transaction) {
b.AddTxWithChain(nil, tx)
func (b *BlockGen) AddTx(tx *types.Transaction, etxRLimit, etxPLimit *int) {
b.AddTxWithChain(nil, tx, etxRLimit, etxPLimit)
}

// AddTxWithChain adds a transaction to the generated block. If no coinbase has
Expand All @@ -100,14 +100,14 @@ func (b *BlockGen) AddTx(tx *types.Transaction) {
// further limitations on the content of transactions that can be
// added. If contract code relies on the BLOCKHASH instruction,
// the block in chain will be returned.
func (b *BlockGen) AddTxWithChain(hc *HeaderChain, tx *types.Transaction) {
func (b *BlockGen) AddTxWithChain(hc *HeaderChain, tx *types.Transaction, etxRLimit, etxPLimit *int) {
if b.gasPool == nil {
b.SetCoinbase(common.Address{})
}
b.statedb.Prepare(tx.Hash(), len(b.txs))
coinbase := b.header.Coinbase()
gasUsed := b.header.GasUsed()
receipt, err := ApplyTransaction(b.config, hc, &coinbase, b.gasPool, b.statedb, b.header, tx, &gasUsed, vm.Config{})
receipt, err := ApplyTransaction(b.config, hc, &coinbase, b.gasPool, b.statedb, b.header, tx, &gasUsed, vm.Config{}, etxRLimit, etxPLimit)
if err != nil {
panic(err)
}
Expand Down
4 changes: 4 additions & 0 deletions core/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ var (
// by a transaction is higher than what's left in the block.
ErrGasLimitReached = errors.New("gas limit reached")

// ErrEtxLimitReached is returned when the ETXs emitted by a transaction
// would violate the block's ETX limits.
ErrEtxLimitReached = errors.New("etx limit reached")

// ErrInsufficientFundsForTransfer is returned if the transaction sender doesn't
// have enough funds for transfer(topmost call only).
ErrInsufficientFundsForTransfer = errors.New("insufficient funds for transfer")
Expand Down
42 changes: 36 additions & 6 deletions core/state_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,15 @@ func (p *StateProcessor) Process(block *types.Block, etxSet types.EtxSet) (types
vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, p.vmConfig)

// Iterate over and process the individual transactions.
etxRLimit := len(parent.Transactions()) / params.ETXRegionMaxFraction
if etxRLimit < params.ETXRLimitMin {
etxRLimit = params.ETXRLimitMin
}
etxPLimit := len(parent.Transactions()) / params.ETXPrimeMaxFraction
if etxPLimit < params.ETXPLimitMin {
etxPLimit = params.ETXPLimitMin
}
var emittedEtxs types.Transactions
for i, tx := range block.Transactions() {
msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number()), header.BaseFee())
if err != nil {
Expand All @@ -236,7 +245,7 @@ func (p *StateProcessor) Process(block *types.Block, etxSet types.EtxSet) (types
return nil, nil, nil, 0, fmt.Errorf("invalid external transaction: etx %x not found in unspent etx set", tx.Hash())
}
prevZeroBal := prepareApplyETX(statedb, tx)
receipt, err = applyTransaction(msg, p.config, p.hc, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv)
receipt, err = applyTransaction(msg, p.config, p.hc, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, &etxRLimit, &etxPLimit)
statedb.SetBalance(common.ZeroInternal, prevZeroBal) // Reset the balance to what it previously was. Residual balance will be lost

if err != nil {
Expand All @@ -246,10 +255,11 @@ func (p *StateProcessor) Process(block *types.Block, etxSet types.EtxSet) (types
delete(etxSet, tx.Hash()) // This ETX has been spent so remove it from the unspent set

} else if tx.Type() == types.InternalTxType || tx.Type() == types.InternalToExternalTxType {
receipt, err = applyTransaction(msg, p.config, p.hc, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv)
receipt, err = applyTransaction(msg, p.config, p.hc, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, &etxRLimit, &etxPLimit)
if err != nil {
return nil, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
emittedEtxs = append(emittedEtxs, receipt.Etxs...)
} else {
return nil, nil, nil, 0, ErrTxTypeNotSupported
}
Expand All @@ -264,7 +274,7 @@ func (p *StateProcessor) Process(block *types.Block, etxSet types.EtxSet) (types
return receipts, allLogs, statedb, *usedGas, nil
}

func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainContext, author *common.Address, 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 types.Message, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, etxRLimit, etxPLimit *int) (*types.Receipt, error) {
// Create a new context to be used in the EVM environment.
txContext := NewEVMTxContext(msg)
evm.Reset(txContext, statedb)
Expand All @@ -274,6 +284,26 @@ func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainCon
if err != nil {
return nil, err
}
var ETXRCount int
var ETXPCount int
for _, tx := range result.Etxs {
// Count which ETXs are cross-region
if tx.To().Location().CommonDom(common.NodeLocation).Context() == common.REGION_CTX {
ETXRCount++
}
// Count which ETXs are cross-prime
if tx.To().Location().CommonDom(common.NodeLocation).Context() == common.PRIME_CTX {
ETXPCount++
}
}
if ETXRCount > *etxRLimit {
return nil, fmt.Errorf("tx %032x emits too many cross-region ETXs for block. emitted: %d, limit: %d", tx.Hash(), ETXRCount, etxRLimit)
}
if ETXPCount > *etxPLimit {
return nil, fmt.Errorf("tx %032x emits too many cross-prime ETXs for block. emitted: %d, limit: %d", tx.Hash(), ETXPCount, etxPLimit)
}
*etxRLimit -= ETXRCount
*etxPLimit -= ETXPCount

// Update the state with pending changes.
var root []byte
Expand Down Expand Up @@ -417,7 +447,7 @@ func (p *StateProcessor) Apply(batch ethdb.Batch, block *types.Block, newInbound
// and uses the input parameters for its environment. It returns the receipt
// 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) {
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, etxRLimit, etxPLimit *int) (*types.Receipt, error) {
msg, err := tx.AsMessage(types.MakeSigner(config, header.Number()), header.BaseFee())
if err != nil {
return nil, err
Expand All @@ -427,11 +457,11 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo
vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg)
if tx.Type() == types.ExternalTxType {
prevZeroBal := prepareApplyETX(statedb, tx)
receipt, err := applyTransaction(msg, config, bc, author, gp, statedb, header.Number(), header.Hash(), tx, usedGas, vmenv)
receipt, err := applyTransaction(msg, config, bc, author, gp, statedb, header.Number(), header.Hash(), tx, usedGas, vmenv, etxRLimit, etxPLimit)
statedb.SetBalance(common.ZeroInternal, prevZeroBal) // Reset the balance to what it previously was (currently a failed external transaction removes all the sent coins from the supply and any residual balance is gone as well)
return receipt, err
}
return applyTransaction(msg, config, bc, author, gp, statedb, header.Number(), header.Hash(), tx, usedGas, vmenv)
return applyTransaction(msg, config, bc, author, gp, statedb, header.Number(), header.Hash(), tx, usedGas, vmenv, etxRLimit, etxPLimit)
}

// GetVMConfig returns the block chain VM config.
Expand Down
26 changes: 21 additions & 5 deletions core/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ type environment struct {
tcount int // tx count in cycle
gasPool *GasPool // available gas used to pack transactions
coinbase common.Address
etxRLimit int // Remaining number of cross-region ETXs that can be included
etxPLimit int // Remaining number of cross-prime ETXs that can be included

header *types.Header
txs []*types.Transaction
Expand All @@ -77,6 +79,8 @@ func (env *environment) copy() *environment {
family: env.family.Clone(),
tcount: env.tcount,
coinbase: env.coinbase,
etxRLimit: env.etxRLimit,
etxPLimit: env.etxPLimit,
header: types.CopyHeader(env.header),
receipts: copyReceipts(env.receipts),
}
Expand Down Expand Up @@ -529,6 +533,14 @@ func (w *worker) makeEnv(parent *types.Block, header *types.Header, coinbase com
return nil, err
}

etxRLimit := len(parent.Transactions()) / params.ETXRegionMaxFraction
if etxRLimit < params.ETXRLimitMin {
etxRLimit = params.ETXRLimitMin
}
etxPLimit := len(parent.Transactions()) / params.ETXPrimeMaxFraction
if etxPLimit < params.ETXPLimitMin {
etxPLimit = params.ETXPLimitMin
}
// Note the passed coinbase may be different with header.Coinbase.
env := &environment{
signer: types.MakeSigner(w.chainConfig, header.Number()),
Expand All @@ -538,6 +550,8 @@ func (w *worker) makeEnv(parent *types.Block, header *types.Header, coinbase com
family: mapset.NewSet(),
header: header,
uncles: make(map[common.Hash]*types.Header),
etxRLimit: etxRLimit,
etxPLimit: etxPLimit,
}
// when 08 is processed ancestors contain 07 (quick block)
for _, ancestor := range w.hc.GetBlocksFromHash(parent.Hash(), 7) {
Expand Down Expand Up @@ -578,7 +592,7 @@ func (w *worker) commitTransaction(env *environment, tx *types.Transaction) ([]*
snap := env.state.Snapshot()
// retrieve the gas used int and pass in the reference to the ApplyTransaction
gasUsed := env.header.GasUsed()
receipt, err := ApplyTransaction(w.chainConfig, w.hc, &env.coinbase, env.gasPool, env.state, env.header, tx, &gasUsed, *w.hc.bc.processor.GetVMConfig())
receipt, err := ApplyTransaction(w.chainConfig, w.hc, &env.coinbase, env.gasPool, env.state, env.header, tx, &gasUsed, *w.hc.bc.processor.GetVMConfig(), &env.etxRLimit, &env.etxPLimit)
if err != nil {
log.Debug("Error playing transaction in worker", "err", err, "tx", tx.Hash().Hex(), "block", env.header.Number, "gasUsed", gasUsed)
env.state.RevertToSnapshot(snap)
Expand All @@ -588,13 +602,10 @@ func (w *worker) commitTransaction(env *environment, tx *types.Transaction) ([]*
// This extra step is needed because previously the GasUsed was a public method and direct update of the value
// was possible.
env.header.SetGasUsed(gasUsed)

env.txs = append(env.txs, tx)
env.receipts = append(env.receipts, receipt)
if receipt.Status == types.ReceiptStatusSuccessful {
for _, etx := range receipt.Etxs {
env.etxs = append(env.etxs, etx)
}
env.etxs = append(env.etxs, receipt.Etxs...)
}
return receipt.Logs, nil
}
Expand Down Expand Up @@ -654,6 +665,11 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP
log.Trace("Gas limit exceeded for current block", "sender", from)
txs.Pop()

case errors.Is(err, ErrEtxLimitReached):
// Pop the current transaction without shifting in the next from the account
log.Trace("Etx limit exceeded for current block", "sender", from)
txs.Pop()

case errors.Is(err, ErrNonceTooLow):
// New head notification data race between the transaction pool and miner, shift
log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce())
Expand Down
32 changes: 20 additions & 12 deletions params/protocol_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,33 @@

package params

import "math/big"
import (
"math/big"

"github.com/dominant-strategies/go-quai/common"
)

const (
GasLimitBoundDivisor uint64 = 1024 // The bound divisor of the gas limit, used in update calculations.
PercentGasUsedThreshold uint64 = 95 // Percent Gas used threshold at which the gas limit adjusts
MinGasLimit uint64 = 5000000 // Minimum the gas limit may ever be.
GenesisGasLimit uint64 = 5000000 // Gas limit of the Genesis block.

MaximumExtraDataSize uint64 = 32 // Maximum size extra data may be after Genesis.
ExpByteGas uint64 = 10 // Times ceil(log256(exponent)) for the EXP instruction.
CallValueTransferGas uint64 = 9000 // Paid for CALL when the value transfer is non-zero.
CallNewAccountGas uint64 = 25000 // Paid for CALL when the destination address didn't exist prior.
TxGas uint64 = 21000 // Per transaction not creating a contract. NOTE: Not payable on data of calls between transactions.
TxGasContractCreation uint64 = 53000 // Per transaction that creates a contract. NOTE: Not payable on data of calls between transactions.
TxDataZeroGas uint64 = 4 // Per byte of data attached to a transaction that equals zero. NOTE: Not payable on data of calls between transactions.
QuadCoeffDiv uint64 = 512 // Divisor for the quadratic particle of the memory cost equation.
LogDataGas uint64 = 8 // Per byte in a LOG* operation's data.
CallStipend uint64 = 2300 // Free gas given at beginning of call.
ETXGas uint64 = 21000 // Per ETX generated by opETX or normal cross-chain transfer.
MaximumExtraDataSize uint64 = 32 // Maximum size extra data may be after Genesis.
ExpByteGas uint64 = 10 // Times ceil(log256(exponent)) for the EXP instruction.
CallValueTransferGas uint64 = 9000 // Paid for CALL when the value transfer is non-zero.
CallNewAccountGas uint64 = 25000 // Paid for CALL when the destination address didn't exist prior.
TxGas uint64 = 21000 // Per transaction not creating a contract. NOTE: Not payable on data of calls between transactions.
TxGasContractCreation uint64 = 53000 // Per transaction that creates a contract. NOTE: Not payable on data of calls between transactions.
TxDataZeroGas uint64 = 4 // Per byte of data attached to a transaction that equals zero. NOTE: Not payable on data of calls between transactions.
QuadCoeffDiv uint64 = 512 // Divisor for the quadratic particle of the memory cost equation.
LogDataGas uint64 = 8 // Per byte in a LOG* operation's data.
CallStipend uint64 = 2300 // Free gas given at beginning of call.
ETXGas uint64 = 21000 // Per ETX generated by opETX or normal cross-chain transfer.
ETXRegionMaxFraction int = common.NumRegionsInPrime * (common.NumZonesInRegion - 1) // The maximum fraction of transactions for cross-region ETXs
ETXPrimeMaxFraction int = common.NumRegionsInPrime * common.NumZonesInRegion // The maximum fraction of transactions for cross-prime ETXs
ETXRLimitMin int = 10 // Minimum possible cross-region ETX limit
ETXPLimitMin int = 10 // Minimum possible cross-prime ETX limit

Sha3Gas uint64 = 30 // Once per SHA3 operation.
Sha3WordGas uint64 = 6 // Once per word of the SHA3 operation's data.
Expand Down

0 comments on commit 748e323

Please sign in to comment.