From bc935f496d56d399256f3a5fe6a29b101f5e5eec Mon Sep 17 00:00:00 2001 From: Piers Powlesland Date: Fri, 22 Mar 2024 14:13:36 +0000 Subject: [PATCH 1/7] Add code to handle block receipts This pr adds code to handle the extra block receipt added by celo to the list of receipts in a block and also adds the GetBlockReceipt rpc api which was an additional api added by celo to allow retrieveal of the block receipt. --- core/types/receipt.go | 21 +++++++++- eth/filters/filter.go | 8 +++- internal/ethapi/api.go | 10 +++-- internal/ethapi/celo_block_receipt.go | 60 +++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 internal/ethapi/celo_block_receipt.go diff --git a/core/types/receipt.go b/core/types/receipt.go index 5ce8302dd3..58984c056f 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -600,13 +600,16 @@ func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, nu signer := MakeSigner(config, new(big.Int).SetUint64(number), time) logIndex := uint(0) - if len(txs) != len(rs) { + + // If rs are one longer than txs it indicates the presence of a celo block receipt. + if len(txs) != len(rs) && len(txs)+1 != len(rs) { return errors.New("transaction and receipt count mismatch") } - for i := 0; i < len(rs); i++ { + for i := 0; i < len(txs); i++ { // The transaction type and hash can be retrieved from the transaction itself rs[i].Type = txs[i].Type() rs[i].TxHash = txs[i].Hash() + // The CeloDynamicFeeTxs set the baseFee in the receipt if txs[i].Type() != CeloDynamicFeeTxV2Type { rs[i].EffectiveGasPrice = txs[i].inner.effectiveGasPrice(new(big.Int), baseFee) @@ -655,6 +658,20 @@ func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, nu logIndex++ } } + + // This is a celo block receipt, which uses the block hash in place of the tx hash. + if len(txs)+1 == len(rs) { + j := len(txs) + for k := 0; k < len(rs[j].Logs); k++ { + rs[j].Logs[k].BlockNumber = number + rs[j].Logs[k].BlockHash = hash + rs[j].Logs[k].TxHash = hash + rs[j].Logs[k].TxIndex = uint(j) + rs[j].Logs[k].Index = logIndex + logIndex++ + } + } + if config.Optimism != nil && len(txs) >= 2 && config.IsBedrock(new(big.Int).SetUint64(number)) { // need at least an info tx and a non-info tx gasParams, err := extractL1GasParams(config, time, txs[0].Data()) if err != nil { diff --git a/eth/filters/filter.go b/eth/filters/filter.go index 83e3284a2b..664df4cf03 100644 --- a/eth/filters/filter.go +++ b/eth/filters/filter.go @@ -325,7 +325,13 @@ func (f *Filter) checkMatches(ctx context.Context, header *types.Header) ([]*typ for i, log := range logs { // Copy log not to modify cache elements logcopy := *log - logcopy.TxHash = body.Transactions[logcopy.TxIndex].Hash() + // Block receipts reference a non existent transaction ocurring after the last transaction. + // We use the block hash in place of the transaction hash for the block receipt. + if logcopy.TxIndex == uint(len(body.Transactions)) { + logcopy.TxHash = logcopy.BlockHash + } else { + logcopy.TxHash = body.Transactions[logcopy.TxIndex].Hash() + } logs[i] = &logcopy } return logs, nil diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 3594bccc10..725e097dd9 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1015,7 +1015,8 @@ func (s *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc. return nil, err } txs := block.Transactions() - if len(txs) != len(receipts) { + // Legacy Celo blocks sometimes include an extra block receipt. See https://docs.celo.org/developer/migrate/from-ethereum#core-contract-calls + if len(txs) != len(receipts) && len(txs)+1 != len(receipts) { return nil, fmt.Errorf("receipts length mismatch: %d vs %d", len(txs), len(receipts)) } @@ -1024,9 +1025,12 @@ func (s *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc. result := make([]map[string]interface{}, len(receipts)) for i, receipt := range receipts { - result[i] = marshalReceipt(receipt, block.Hash(), block.NumberU64(), signer, txs[i], i, s.b.ChainConfig()) + if i == len(txs) { + result[i] = marshalBlockReceipt(receipt, block.Hash(), block.NumberU64(), i) + } else { + result[i] = marshalReceipt(receipt, block.Hash(), block.NumberU64(), signer, txs[i], i, s.b.ChainConfig()) + } } - return result, nil } diff --git a/internal/ethapi/celo_block_receipt.go b/internal/ethapi/celo_block_receipt.go new file mode 100644 index 0000000000..e89b3a684a --- /dev/null +++ b/internal/ethapi/celo_block_receipt.go @@ -0,0 +1,60 @@ +package ethapi + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" +) + +// GetBlockReceipt returns "system calls" receipt for the block with the given block hash. +func (s *BlockChainAPI) GetBlockReceipt(ctx context.Context, hash common.Hash) (map[string]interface{}, error) { + block, err := s.b.BlockByHash(ctx, hash) + if block == nil || err != nil { + // If no header with that hash is found, err gives "header for hash not found". + // But we return nil with no error, to match the behavior of eth_getBlockByHash and eth_getTransactionReceipt in these cases. + return nil, nil + } + index := block.Transactions().Len() + blockNumber := block.NumberU64() + receipts, err := s.b.GetReceipts(ctx, block.Hash()) + // GetReceipts() doesn't return an error if things go wrong, so we also check len(receipts) + if err != nil || len(receipts) < index { + return nil, err + } + + var receipt *types.Receipt + if len(receipts) == index { + // The block didn't have any logs from system calls and no receipt was created. + // So we create an empty receipt to return, similarly to how system receipts are created. + receipt = types.NewReceipt(nil, false, 0) + receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) + } else { + receipt = receipts[index] + } + return marshalBlockReceipt(receipt, hash, blockNumber, index), nil +} + +// marshalBlockReceipt marshals a Celo block receipt into a JSON object. See https://docs.celo.org/developer/migrate/from-ethereum#core-contract-calls +func marshalBlockReceipt(receipt *types.Receipt, blockHash common.Hash, blockNumber uint64, index int) map[string]interface{} { + fields := map[string]interface{}{ + "blockHash": blockHash, + "blockNumber": hexutil.Uint64(blockNumber), + "transactionHash": blockHash, + "transactionIndex": hexutil.Uint64(index), + "from": common.Address{}, + "to": nil, + "gasUsed": hexutil.Uint64(0), + "cumulativeGasUsed": hexutil.Uint64(0), + "contractAddress": nil, + "logs": receipt.Logs, + "logsBloom": receipt.Bloom, + "type": hexutil.Uint(0), + "status": hexutil.Uint(types.ReceiptStatusSuccessful), + } + if receipt.Logs == nil { + fields["logs"] = []*types.Log{} + } + return fields +} From baf6830b2a0d1e0849eebc56e992d118fdd22968 Mon Sep 17 00:00:00 2001 From: Piers Powlesland Date: Tue, 18 Jun 2024 15:11:52 +0100 Subject: [PATCH 2/7] Ensure non nil difficulty in pre-gingerbread blocks Nil difficulty in headers causes problems because it is assumed by the go-ethereum codebase that difficulty is non nil. The specific problem requiring this fix was a NPE in Downloader.processHeaders where the total difficulty (td) is calculated by adding individual header difficulty values. Rather than try to fix this specific case it seems safer to ensure blocks have a non nil Difficulty which should help future proof since it upholds the assumptions of the go-ethereum codebase. --- core/genesis.go | 3 +++ core/types/celo_block.go | 1 + 2 files changed, 4 insertions(+) diff --git a/core/genesis.go b/core/genesis.go index d69d9ed261..55a6e08aea 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -485,6 +485,9 @@ func (g *Genesis) ToBlock() *types.Block { if g.Difficulty == nil && g.Mixhash == (common.Hash{}) { head.Difficulty = params.GenesisDifficulty } + } else if g.Difficulty == nil { + // In the case of migrated chains we ensure a zero rather than nil difficulty. + head.Difficulty = new(big.Int) } if g.Config != nil && g.Config.IsLondon(common.Big0) { if g.BaseFee != nil { diff --git a/core/types/celo_block.go b/core/types/celo_block.go index 07aebf7139..c657fa4153 100644 --- a/core/types/celo_block.go +++ b/core/types/celo_block.go @@ -53,6 +53,7 @@ func (h *Header) DecodeRLP(s *rlp.Stream) error { h.GasUsed = decodedHeader.GasUsed h.Time = decodedHeader.Time h.Extra = decodedHeader.Extra + h.Difficulty = new(big.Int) } else { // After gingerbread decodedHeader := afterGingerbreadHeader{} From cf391dedfb0d9711982ae322d87d1427521c899c Mon Sep 17 00:00:00 2001 From: Piers Powlesland Date: Tue, 18 Jun 2024 15:32:54 +0100 Subject: [PATCH 3/7] Add method to header to check for pre-gingerbread --- core/types/celo_block.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/core/types/celo_block.go b/core/types/celo_block.go index c657fa4153..075c856087 100644 --- a/core/types/celo_block.go +++ b/core/types/celo_block.go @@ -86,13 +86,7 @@ func (h *Header) DecodeRLP(s *rlp.Stream) error { // EncodeRLP implements encodes the Header to an RLP data stream. func (h *Header) EncodeRLP(w io.Writer) error { - // We check for a pre gingerbread header by looking for (GasLimit == 0) - // here. We don't use Difficulty because CopyHeader can end up setting a - // nil Difficulty to a zero difficulty, so testing for nil difficulty is - // not reliable, and post gingerbread difficulty is hardcoded to zero. Also - // testing for base fee is not reliable because some older eth blocks had - // no base fee and they are used in some tests. - if h.GasLimit == 0 { + if h.IsPreGingerbread() { // Encode the header encodedHeader := beforeGingerbreadHeader{ ParentHash: h.ParentHash, @@ -153,3 +147,15 @@ func isPreGingerbreadHeader(buf []byte) (bool, error) { return contentSize == common.AddressLength, nil } + +// Returns if the header is a gingerbread header by looking at the gas limit. +func (h *Header) IsPreGingerbread() bool { + // We check for a pre gingerbread header by looking for (GasLimit == 0) + // here. We don't use Difficulty because we ensure that headers have a zero + // difficulty, even if it's not set in the rlp encoded form (we do this + // because the go ethereum codebase assumed non nil difficulties) and post + // gingerbread difficulty is hardcoded to zero. Also testing for base fee + // is not reliable because some older eth blocks had no base fee and they + // are used in some tests. + return h.GasLimit == 0 +} From bc6fe0f52102392a0e4106f0106da0267a6f4f2c Mon Sep 17 00:00:00 2001 From: Piers Powlesland Date: Tue, 18 Jun 2024 15:36:31 +0100 Subject: [PATCH 4/7] Skip uncle hash check for pre gingerbread headers Pre gingerbread headers do not have a valid uncle hash because they have no notion of uncles. --- eth/downloader/queue.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eth/downloader/queue.go b/eth/downloader/queue.go index 6ff858d755..06005d96e6 100644 --- a/eth/downloader/queue.go +++ b/eth/downloader/queue.go @@ -782,7 +782,8 @@ func (q *queue) DeliverBodies(id string, txLists [][]*types.Transaction, txListH if txListHashes[index] != header.TxHash { return errInvalidBody } - if uncleListHashes[index] != header.UncleHash { + // Pre gingerbread headers do not have a valid uncle hash. + if !header.IsPreGingerbread() && uncleListHashes[index] != header.UncleHash { return errInvalidBody } if header.WithdrawalsHash == nil { From 9795acac4fa262334dd1114b7997f84b533bdcc5 Mon Sep 17 00:00:00 2001 From: Piers Powlesland Date: Tue, 18 Jun 2024 17:19:42 +0100 Subject: [PATCH 5/7] Handle base fee calculation for transition block --- consensus/misc/eip1559/eip1559.go | 6 ++++++ params/celo_config.go | 19 +++++++++++++++++++ params/config.go | 2 ++ 3 files changed, 27 insertions(+) create mode 100644 params/celo_config.go diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index a66298af69..dc237c227b 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -58,6 +58,12 @@ func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Heade // CalcBaseFee calculates the basefee of the header. // The time belongs to the new block to check if Canyon is activted or not func CalcBaseFee(config *params.ChainConfig, parent *types.Header, time uint64) *big.Int { + // If this is the cel2 transition block then we use the initial base fee + // from the Cel2Config of the chain config. + if config.Cel2Time != nil && *config.Cel2Time == time { + return new(big.Int).SetUint64(config.Cel2Config.TransitionBlockBaseFee) + } + // If the current block is the first EIP-1559 block, return the InitialBaseFee. if !config.IsLondon(parent.Number) { return new(big.Int).SetUint64(params.InitialBaseFee) diff --git a/params/celo_config.go b/params/celo_config.go new file mode 100644 index 0000000000..821d523876 --- /dev/null +++ b/params/celo_config.go @@ -0,0 +1,19 @@ +package params + +// Cel2Config holds config required when running a cel2 chain. +type Cel2Config struct { + // TransitionBlockBaseFee is the base fee for the transition block in a + // migrated chain. it must be set for migrated chains during the migration + // to the value in the transition block. For non migrated chains it does + // not need to be set.This is required because we need + // eip1559.CalcBaseFee(config *params.ChainConfig, parent *types.Header, + // time uint64) to be able to return the correct base fee for the + // transition block and CalcBaseFee does not have access to the current + // header so cannot know what the base fee should be. We can't just use the + // base fee of the parent either because if we are transitioning at a + // pre-gingerbread block then it won't have a base fee, so this seems like + // the least invasive approach. Alternatively we could change the signature + // of CalcBaseFee to include the current header but that would require + // changing code in a number of places. + TransitionBlockBaseFee uint64 +} diff --git a/params/config.go b/params/config.go index 823ace9fb2..7951c97eed 100644 --- a/params/config.go +++ b/params/config.go @@ -434,6 +434,8 @@ type ChainConfig struct { Cel2Time *uint64 `json:"cel2Time,omitempty"` // Cel2 switch time (nil = no fork, 0 = already on optimism cel2) + Cel2Config *Cel2Config `json:"cel2Config,omitempty"` // Cel2 config + // TerminalTotalDifficulty is the amount of total difficulty reached by // the network that triggers the consensus upgrade. TerminalTotalDifficulty *big.Int `json:"terminalTotalDifficulty,omitempty"` From 97be5212f49ad30c93a8f5b1659ab2ac787b48c3 Mon Sep 17 00:00:00 2001 From: Piers Powlesland Date: Tue, 25 Jun 2024 09:41:46 +0100 Subject: [PATCH 6/7] Revert "Handle base fee calculation for transition block" This reverts commit 9795acac4fa262334dd1114b7997f84b533bdcc5. --- consensus/misc/eip1559/eip1559.go | 6 ------ params/celo_config.go | 19 ------------------- params/config.go | 2 -- 3 files changed, 27 deletions(-) delete mode 100644 params/celo_config.go diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index dc237c227b..a66298af69 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -58,12 +58,6 @@ func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Heade // CalcBaseFee calculates the basefee of the header. // The time belongs to the new block to check if Canyon is activted or not func CalcBaseFee(config *params.ChainConfig, parent *types.Header, time uint64) *big.Int { - // If this is the cel2 transition block then we use the initial base fee - // from the Cel2Config of the chain config. - if config.Cel2Time != nil && *config.Cel2Time == time { - return new(big.Int).SetUint64(config.Cel2Config.TransitionBlockBaseFee) - } - // If the current block is the first EIP-1559 block, return the InitialBaseFee. if !config.IsLondon(parent.Number) { return new(big.Int).SetUint64(params.InitialBaseFee) diff --git a/params/celo_config.go b/params/celo_config.go deleted file mode 100644 index 821d523876..0000000000 --- a/params/celo_config.go +++ /dev/null @@ -1,19 +0,0 @@ -package params - -// Cel2Config holds config required when running a cel2 chain. -type Cel2Config struct { - // TransitionBlockBaseFee is the base fee for the transition block in a - // migrated chain. it must be set for migrated chains during the migration - // to the value in the transition block. For non migrated chains it does - // not need to be set.This is required because we need - // eip1559.CalcBaseFee(config *params.ChainConfig, parent *types.Header, - // time uint64) to be able to return the correct base fee for the - // transition block and CalcBaseFee does not have access to the current - // header so cannot know what the base fee should be. We can't just use the - // base fee of the parent either because if we are transitioning at a - // pre-gingerbread block then it won't have a base fee, so this seems like - // the least invasive approach. Alternatively we could change the signature - // of CalcBaseFee to include the current header but that would require - // changing code in a number of places. - TransitionBlockBaseFee uint64 -} diff --git a/params/config.go b/params/config.go index 7951c97eed..823ace9fb2 100644 --- a/params/config.go +++ b/params/config.go @@ -434,8 +434,6 @@ type ChainConfig struct { Cel2Time *uint64 `json:"cel2Time,omitempty"` // Cel2 switch time (nil = no fork, 0 = already on optimism cel2) - Cel2Config *Cel2Config `json:"cel2Config,omitempty"` // Cel2 config - // TerminalTotalDifficulty is the amount of total difficulty reached by // the network that triggers the consensus upgrade. TerminalTotalDifficulty *big.Int `json:"terminalTotalDifficulty,omitempty"` From 87af81b053ab1b43473204d96adb0db7a418e7bd Mon Sep 17 00:00:00 2001 From: Piers Powlesland Date: Tue, 25 Jun 2024 09:50:33 +0100 Subject: [PATCH 7/7] Change approach to choosing transition block base fee If the parent block has a base fee then we use that, otherwise we use the initial base fee. --- consensus/misc/eip1559/eip1559.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index a66298af69..d21e41654d 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -58,6 +58,12 @@ func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Heade // CalcBaseFee calculates the basefee of the header. // The time belongs to the new block to check if Canyon is activted or not func CalcBaseFee(config *params.ChainConfig, parent *types.Header, time uint64) *big.Int { + // If this is the cel2 transition block and the parent block has a base fee + // then use that. + if config.Cel2Time != nil && *config.Cel2Time == time && parent.BaseFee != nil { + return parent.BaseFee + } + // If the current block is the first EIP-1559 block, return the InitialBaseFee. if !config.IsLondon(parent.Number) { return new(big.Int).SetUint64(params.InitialBaseFee)