Skip to content

all: implement inclusion list for epbs #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions beacon/engine/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ var (
// VALID is returned by the engine API in the following calls:
// - newPayloadV1: if the payload was already known or was just validated and executed
// - forkchoiceUpdateV1: if the chain accepted the reorg (might ignore if it's stale)
// - newInclusionListV1: if the inclusion list is valid and executable on current state
VALID = "VALID"

// INVALID is returned by the engine API in the following calls:
// - newPayloadV1: if the payload failed to execute on top of the local chain
// - forkchoiceUpdateV1: if the new head is unknown, pre-merge, or reorg to it fails
// - newInclusionListV1: if the inclusion list is invalid
INVALID = "INVALID"

// SYNCING is returned by the engine API in the following calls:
Expand Down
20 changes: 20 additions & 0 deletions beacon/engine/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ type ExecutableData struct {
Withdrawals []*types.Withdrawal `json:"withdrawals"`
BlobGasUsed *uint64 `json:"blobGasUsed"`
ExcessBlobGas *uint64 `json:"excessBlobGas"`

// ePBS
InclusionListSummary InclusionListSummaryV1 `json:"inclusionListSummary"`
InclusionListExclusions ExclusionList `json:"inclusionListExclusions"`
}

// JSON type overrides for executableData.
Expand Down Expand Up @@ -277,3 +281,19 @@ type ExecutionPayloadBodyV1 struct {
TransactionData []hexutil.Bytes `json:"transactions"`
Withdrawals []*types.Withdrawal `json:"withdrawals"`
}

type VerifiableInclusionList struct {
ParentHash common.Hash `json:"parentHash"`
InclusionList types.InclusionList `json:"inclusionList"`
}

type InclusionListV1 types.InclusionList

type InclusionListSummaryV1 []*types.InclusionListEntry

type ExclusionList []uint64

type InclusionListStatusV1 struct {
Status string `json:"status"`
ValidatorError error `json:"validatorError"`
}
10 changes: 10 additions & 0 deletions core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2587,3 +2587,13 @@ func (bc *BlockChain) SetTrieFlushInterval(interval time.Duration) {
func (bc *BlockChain) GetTrieFlushInterval() time.Duration {
return time.Duration(bc.flushInterval.Load())
}

// VerifyInclusionList validates an inclusion list to make sure it satisfies all the condition based on a `parent` header.
func (bc *BlockChain) VerifyInclusionList(list types.InclusionList, parent *types.Header, getStateNonce func(common.Address) uint64) (bool, error) {
return verifyInclusionList(list, parent, bc.Config(), getStateNonce)
}

// VerifyInclusionListInBlock verifies the block solely based on the inclusion list conditions based on `parent` block's data.
func (bc *BlockChain) VerifyInclusionListInBlock(summary []*types.InclusionListEntry, exclusionList []uint64, currentTxs types.Transactions, parent *types.Block) (bool, error) {
return verifyInclusionListInBlock(summary, exclusionList, parent.Body().Transactions, currentTxs, bc.Config())
}
191 changes: 191 additions & 0 deletions core/inclusion_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package core

import (
"errors"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)

var (
ErrSizeMismatch = errors.New("summary and transactions length mismatch in IL")
ErrSizeExceeded = errors.New("transactions exceeds maximum limit in IL")
ErrUnsupportedTxType = errors.New("unsupported tx type in IL")
ErrInvalidTx = errors.New("invalid tx in IL")
ErrGasLimitExceeded = errors.New("gas limit exceeds maximum allowed in IL")
ErrSenderMismatch = errors.New("summary and transaction sender mismatch in IL")
ErrGasLimitMismatch = errors.New("summary and transaction gaslimit mismatch in IL")
ErrIncorrectNonce = errors.New("incorrect nonce in IL")
ErrInsufficientGasFeeCap = errors.New("insufficient gas fee cap in IL")
)

// IL constants taken from specs here: https://github.com/potuz/consensus-specs/blob/a6c55576de059a1b2cae69848dee827f6e26e72d/specs/_features/epbs/beacon-chain.md#execution
const (
MaxTransactionsPerInclusionList = 16
MaxGasPerInclusionList = 2_097_152 // 2^21
)

// verifyInclusionList verifies the properties of the inclusion list and the
// transactions in it based on a `parent` block.
func verifyInclusionList(list types.InclusionList, parent *types.Header, config *params.ChainConfig, getStateNonce func(addr common.Address) uint64) (bool, error) {
if len(list.Summary) != len(list.Transactions) {
log.Debug("IL verification failed: summary and transactions length mismatch", "summary", len(list.Summary), "txs", len(list.Transactions))
return false, ErrSizeMismatch
}

if len(list.Summary) > MaxTransactionsPerInclusionList {
log.Debug("IL verification failed: exceeds maximum number of transactions", "len", len(list.Summary), "max", MaxTransactionsPerInclusionList)
return false, ErrSizeExceeded
}

// As IL will be included in the next block, calculate the current block's base fee.
// As the current block's payload isn't revealed yet (due to ePBS), calculate
// it from parent block.
currentBaseFee := eip1559.CalcBaseFee(config, parent)

// 1.125 * currentBaseFee
gasFeeThreshold := new(big.Float).Mul(new(big.Float).SetFloat64(1.125), new(big.Float).SetInt(currentBaseFee))

// Prepare the signer object
signer := types.LatestSigner(config)

// Create a nonce cache
nonceCache := make(map[common.Address]uint64)

// Track total gas limit
gasLimit := uint64(0)

// Verify if the summary and transactions match. Also check if the txs
// have at least 12.5% higher `maxFeePerGas` than parent block's base fee.
for i, summary := range list.Summary {
tx := list.Transactions[i]

// Don't allow BlobTxs
if tx.Type() == types.BlobTxType {
log.Debug("IL verification failed: received blob tx in IL")
return false, ErrUnsupportedTxType
}

// Verify gas limit
gasLimit += tx.Gas()

if gasLimit > MaxGasPerInclusionList {
log.Debug("IL verification failed: gas limit exceeds maximum allowed", "gaslimit", gasLimit, "max", MaxGasPerInclusionList)
return false, ErrGasLimitExceeded
}

// Verify sender
from, err := types.Sender(signer, tx)
if err != nil {
log.Debug("IL verification failed: unable to get sender from transaction", "err", err)
return false, ErrInvalidTx
}

if summary.Address != from {
log.Debug("IL verification failed: summary and transaction address mismatch", "summary", summary.Address, "tx", from)
return false, ErrSenderMismatch
}

if summary.GasLimit != uint32(tx.Gas()) {
log.Debug("IL verification failed: summary and transaction gaslimit mismatch", "summary", summary.GasLimit, "tx", tx.Gas())
return false, ErrGasLimitMismatch
}

// Verify nonce from state
nonce := getStateNonce(from)
if cacheNonce, ok := nonceCache[from]; ok {
nonce = cacheNonce
}

if tx.Nonce() == nonce {
nonceCache[from] = nonce + 1
} else {
log.Debug("IL verification failed: incorrect nonce", "state nonce", nonce, "tx nonce", tx.Nonce())
return false, ErrIncorrectNonce
}

// Verify gas fee: tx.GasFeeCap >= gasFeeThreshold
if new(big.Float).SetInt(tx.GasFeeCap()).Cmp(gasFeeThreshold) == -1 {
log.Debug("IL verification failed: insufficient gas fee cap", "gasFeeCap", tx.GasFeeCap(), "threshold", gasFeeThreshold)
return false, ErrInsufficientGasFeeCap
}
}

log.Debug("IL verified successfully", "len", len(list.Summary), "gas", gasLimit)

return true, nil
}

// verifyInclusionListInBlock verifies if a block satisfies the inclusion list summary
// or not. Note that this function doesn't validate the state transition. It can be
// considered as a filter before sending the block to state transition. This function
// assumes that basic validations are already done. It only checks the following things:
//
// 1. If the indices in the exclusion list pointing to the parent block transactions
// are present in the summary or not.
// 2. If the remaining summary entries are satisfied by the first `k` transactions
// of the current block.
func verifyInclusionListInBlock(summaryEntries types.InclusionListSummaries, exclusionList []uint64, parentTxs, currentTxs types.Transactions, config *params.ChainConfig) (bool, error) {
// We assume that summary isn't ordered
// Prepare a map of summary entries: address -> []{gas limit}.
summaries := make(map[common.Address][]uint32)
for _, summary := range summaryEntries {
if _, ok := summaries[summary.Address]; !ok {
summaries[summary.Address] = make([]uint32, 0)
}
summaries[summary.Address] = append(summaries[summary.Address], summary.GasLimit)
}

// Prepare the signer object
signer := types.LatestSigner(config)

exclusions := 0
for _, index := range exclusionList {
tx := parentTxs[index]

// Verify sender
from, err := types.Sender(signer, tx)
if err != nil {
return false, errors.New("invalid tx in parent block")
}

if entries, ok := summaries[from]; !ok || len(entries) == 0 {
return false, errors.New("missing summary entry")
}

summaries[from] = summaries[from][1:]
exclusions++
}

index := 0
for {
if exclusions < len(summaryEntries) {
break
}

tx := currentTxs[index]

// Verify sender
from, err := types.Sender(signer, tx)
if err != nil {
return false, errors.New("invalid tx in current block")
}

if entries, ok := summaries[from]; !ok || len(entries) == 0 {
return false, errors.New("missing IL in current block")
}

if summaries[from][0] > uint32(tx.Gas()) {
return false, errors.New("invalid gas limit")
}
summaries[from] = summaries[from][1:]
exclusions++
index++
}

return true, nil
}
120 changes: 120 additions & 0 deletions core/inclusion_list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package core

import (
"crypto/ecdsa"
"math/big"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/assert"
)

func transaction(nonce uint64, gaslimit uint64, gasPrice *big.Int, key *ecdsa.PrivateKey) *types.Transaction {
return pricedTransaction(nonce, gaslimit, gasPrice, key)
}

func pricedTransaction(nonce uint64, gaslimit uint64, gasprice *big.Int, key *ecdsa.PrivateKey) *types.Transaction {
tx, _ := types.SignTx(types.NewTransaction(nonce, common.Address{}, big.NewInt(100), gaslimit, gasprice, nil), types.HomesteadSigner{}, key)
return tx
}

func getTxsAndSummary(n int, startNonce uint64, getGasLimit func(n int) uint64, getGasPrice func(n int) *big.Int, key *ecdsa.PrivateKey) ([]*types.InclusionListEntry, []*types.Transaction) {
summary := make([]*types.InclusionListEntry, 0, n)
txs := make([]*types.Transaction, 0, n)

for i := 0; i < n; i++ {
txs = append(txs, transaction(startNonce, getGasLimit(i), getGasPrice(i), key))
summary = append(summary, &types.InclusionListEntry{Address: crypto.PubkeyToAddress(key.PublicKey), GasLimit: uint32(getGasLimit(i))})
startNonce++
}

return summary, txs
}

func getGasLimitForTest(n int) uint64 {
if n == 15 {
return 1_000_000
}
return 100_000
}

func getGasPriceForTest(n int) *big.Int {
// threshold = 1.125 * 1 Gwei
if n == 0 {
return big.NewInt(1_126_000_000)
}
if n == 17 {
return big.NewInt(1_124_000_000)
}
return big.NewInt(1_125_000_000)
}

func getStateNonceForTest(n int) func(addr common.Address) uint64 {
if n == 1 {
return func(addr common.Address) uint64 {
return 1
}
} else if n == 2 {
return func(addr common.Address) uint64 {
return 17
}
}

return func(addr common.Address) uint64 {
return 0
}
}

func TestVerifyInclusionList(t *testing.T) {
key, _ := crypto.GenerateKey()

// Generate dummy summary and set of txs
summary, txs := getTxsAndSummary(32, 0, getGasLimitForTest, getGasPriceForTest, key)

// Modify a summary entry explicity for validating invalid
// sender address check
summary[16].Address = common.Address{}

// Build a parent block such that the base fee stays the same
// for the next block.
parent := &types.Header{
Number: big.NewInt(0),
GasLimit: 30_00_000,
GasUsed: 15_00_000,
BaseFee: big.NewInt(1_000_000_000), // 1 GWei
}

testCases := []struct {
name string
list types.InclusionList
parent *types.Header
config *params.ChainConfig
getStateNonce func(addr common.Address) uint64
want bool
err error
}{
{"empty inclusion list", types.InclusionList{Summary: summary[:0], Transactions: txs[:0]}, parent, params.TestChainConfig, getStateNonceForTest(0), true, nil},
{"unqeual size of summary and transactions - 1", types.InclusionList{Summary: summary[:1], Transactions: txs[:0]}, parent, params.TestChainConfig, getStateNonceForTest(0), false, ErrSizeMismatch},
{"unqeual size of summary and transactions - 2", types.InclusionList{Summary: summary[:0], Transactions: txs[:1]}, parent, params.TestChainConfig, getStateNonceForTest(0), false, ErrSizeMismatch},
{"size exceeded", types.InclusionList{Summary: summary, Transactions: txs}, parent, params.TestChainConfig, getStateNonceForTest(0), false, ErrSizeExceeded},
{"gas limit exceeded", types.InclusionList{Summary: summary[:16], Transactions: txs[:16]}, parent, params.TestChainConfig, getStateNonceForTest(0), false, ErrGasLimitExceeded},
{"invalid sender address", types.InclusionList{Summary: summary[16:], Transactions: txs[16:]}, parent, params.TestChainConfig, getStateNonceForTest(0), false, ErrSenderMismatch},
{"invalid nonce - 1", types.InclusionList{Summary: summary[1:16], Transactions: txs[1:16]}, parent, params.TestChainConfig, getStateNonceForTest(0), false, ErrIncorrectNonce},
{"invalid nonce - 2", types.InclusionList{Summary: summary[:16], Transactions: txs[:16]}, parent, params.TestChainConfig, getStateNonceForTest(1), false, ErrIncorrectNonce},
{"less base fee", types.InclusionList{Summary: summary[17:], Transactions: txs[17:]}, parent, params.TestChainConfig, getStateNonceForTest(2), false, ErrInsufficientGasFeeCap},
{"happy case", types.InclusionList{Summary: summary[:15], Transactions: txs[:15]}, parent, params.TestChainConfig, getStateNonceForTest(0), true, nil},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
res, err := verifyInclusionList(tc.list, parent, params.TestChainConfig, tc.getStateNonce)
assert.Equal(t, res, tc.want, "result mismatch")
assert.Equal(t, err, tc.err, "error mismatch")
})
}

}
17 changes: 17 additions & 0 deletions core/txpool/blobpool/blobpool.go
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,16 @@ func (p *BlobPool) Nonce(addr common.Address) uint64 {
return p.state.GetNonce(addr)
}

// StateNonce returns the next nonce of an account from the underlying state, without
// applying any transactions from the pool on top. This is only used for verification
// of inclusion list which only supports *legacy* transactions as of now.
func (p *BlobPool) StateNonce(addr common.Address) uint64 {
p.lock.Lock()
defer p.lock.Unlock()

return 0
}

// Stats retrieves the current pool stats, namely the number of pending and the
// number of queued (non-executable) transactions.
func (p *BlobPool) Stats() (int, int) {
Expand Down Expand Up @@ -1526,3 +1536,10 @@ func (p *BlobPool) Status(hash common.Hash) txpool.TxStatus {
}
return txpool.TxStatusUnknown
}

// GetInclusionList returns an inclusion list from the pool containing pairs
// of transaction summary and data which are executable. Currently blob txs
// aren't supported in the inclusion list.
func (pool *BlobPool) GetInclusionList() (*types.InclusionList, error) {
return nil, txpool.ErrUnsupportedMethod
}
Loading