feat(all): changes based on Taiko protocol

This commit is contained in:
David 2024-05-04 15:20:40 +08:00
parent c5ba367eb6
commit 7e1b8b65a3
59 changed files with 4758 additions and 73 deletions

33
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: "Unit tests"
on:
pull_request:
branches: [taiko]
push:
branches: [taiko]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- uses: actions/checkout@v3
with:
repository: taikoxyz/taiko-geth
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.21'
- name: Lint
run: make lint
- name: Test
env:
TAIKO_TEST: true
run: make test

View file

@ -0,0 +1,22 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
days-before-issue-stale: 30
days-before-issue-close: 7
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for a week since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}

47
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: "Push docker image to GAR"
on:
push:
branches: [taiko]
tags:
- "v*"
jobs:
push-docker-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Login to GAR
uses: docker/login-action@v2
with:
registry: us-docker.pkg.dev
username: _json_key
password: ${{ secrets.GAR_JSON_KEY }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
us-docker.pkg.dev/evmchain/images/taiko-geth
tags: |
type=ref,event=branch
type=ref,event=pr
type=ref,event=tag
type=sha
- name: Build and push
uses: docker/build-push-action@v2
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

14
.github/workflows/lint-pr.yml vendored Normal file
View file

@ -0,0 +1,14 @@
name: "Lint PR"
on:
pull_request:
types: [opened, edited, synchronize, reopened]
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

36
.github/workflows/openai-review.yml vendored Normal file
View file

@ -0,0 +1,36 @@
on: [pull_request]
jobs:
add_pr_comment:
permissions: write-all
runs-on: ubuntu-latest
name: OpenAI PR Comment
if: "contains(github.event.pull_request.labels.*.name, 'option.review-by-ai')"
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
- id: patch
run: |
patch_output=$(curl --silent --request GET \
--url https://api.github.com/repos/$PATCH_REPO/pulls/$PATCH_PR \
--header "Accept: application/vnd.github.v3.patch" \
--header "Authorization: Bearer $PATCH_GITHUB_TOKEN")
echo $patch_output
echo "GIT_PATCH_OUTPUT=$(echo $patch_output)" >> $GITHUB_ENV
env:
PATCH_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PATCH_PR: ${{ github.event.pull_request.number }}
PATCH_REPO: ${{ github.repository }}
- id: review
uses: taikoxyz/openai-review-action@main
with:
GIT_COMMIT_HASH: ${{ github.event.pull_request.head.sha }}
GIT_PATCH_OUTPUT: ${{ env.GIT_PATCH_OUTPUT }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORG_KEY: ${{ secrets.OPENAI_ORG_KEY }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
REPOSITORY_NAME: ${{ github.repository }}

View file

@ -1,3 +1,13 @@
# taiko-geth
[![CI](https://github.com/taikoxyz/taiko-geth/actions/workflows/ci.yml/badge.svg)](https://github.com/taikoxyz/taiko-geth/actions/workflows/ci.yml)
The codebase is based on [go-ethereum v1.13.15](https://github.com/ethereum/go-ethereum/releases/tag/v1.13.15).
## Tracing changes
All source code changes by [taiko](https://taiko.xyz) team are commented with `"CHANGE(taiko): ...."`, and the filenames of all additional files have the prefix `"taiko_"` (`taiko_*.go`).
## Go Ethereum
Golang execution layer implementation of the Ethereum protocol.
@ -83,7 +93,7 @@ This command will:
causing it to download more data in exchange for avoiding processing the entire history
of the Ethereum network, which is very CPU intensive.
* Start the built-in interactive [JavaScript console](https://geth.ethereum.org/docs/interacting-with-geth/javascript-console),
(via the trailing `console` subcommand) through which you can interact using [`web3` methods](https://github.com/ChainSafe/web3.js/blob/0.20.7/DOCUMENTATION.md)
(via the trailing `console` subcommand) through which you can interact using [`web3` methods](https://github.com/ChainSafe/web3.js/blob/0.20.7/DOCUMENTATION.md)
(note: the `web3` version bundled within `geth` is very old, and not up to date with official docs),
as well as `geth`'s own [management APIs](https://geth.ethereum.org/docs/interacting-with-geth/rpc).
This tool is optional and if you leave it out you can always attach it to an already running
@ -344,7 +354,7 @@ testing procedures.
### Contributing to geth.ethereum.org
For contributions to the [go-ethereum website](https://geth.ethereum.org), please checkout and raise pull requests against the `website` branch.
For more detailed instructions please see the `website` branch [README](https://github.com/ethereum/go-ethereum/tree/website#readme) or the
For more detailed instructions please see the `website` branch [README](https://github.com/ethereum/go-ethereum/tree/website#readme) or the
[contributing](https://geth.ethereum.org/docs/developers/geth-developer/contributing) page of the website.
## License

View file

@ -0,0 +1,82 @@
// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
package engine
import (
"encoding/json"
"errors"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
var _ = (*blockMetadataMarshaling)(nil)
// MarshalJSON marshals as JSON.
func (b BlockMetadata) MarshalJSON() ([]byte, error) {
type BlockMetadata struct {
Beneficiary common.Address `json:"beneficiary" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"`
MixHash common.Hash `json:"mixHash" gencodec:"required"`
TxList hexutil.Bytes `json:"txList" gencodec:"required"`
HighestBlockID *big.Int `json:"highestBlockID" gencodec:"required"`
ExtraData []byte `json:"extraData" gencodec:"required"`
}
var enc BlockMetadata
enc.Beneficiary = b.Beneficiary
enc.GasLimit = b.GasLimit
enc.Timestamp = hexutil.Uint64(b.Timestamp)
enc.MixHash = b.MixHash
enc.TxList = b.TxList
enc.HighestBlockID = b.HighestBlockID
enc.ExtraData = b.ExtraData
return json.Marshal(&enc)
}
// UnmarshalJSON unmarshals from JSON.
func (b *BlockMetadata) UnmarshalJSON(input []byte) error {
type BlockMetadata struct {
Beneficiary *common.Address `json:"beneficiary" gencodec:"required"`
GasLimit *uint64 `json:"gasLimit" gencodec:"required"`
Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"`
MixHash *common.Hash `json:"mixHash" gencodec:"required"`
TxList *hexutil.Bytes `json:"txList" gencodec:"required"`
HighestBlockID *big.Int `json:"highestBlockID" gencodec:"required"`
ExtraData []byte `json:"extraData" gencodec:"required"`
}
var dec BlockMetadata
if err := json.Unmarshal(input, &dec); err != nil {
return err
}
if dec.Beneficiary == nil {
return errors.New("missing required field 'beneficiary' for BlockMetadata")
}
b.Beneficiary = *dec.Beneficiary
if dec.GasLimit == nil {
return errors.New("missing required field 'gasLimit' for BlockMetadata")
}
b.GasLimit = *dec.GasLimit
if dec.Timestamp == nil {
return errors.New("missing required field 'timestamp' for BlockMetadata")
}
b.Timestamp = uint64(*dec.Timestamp)
if dec.MixHash == nil {
return errors.New("missing required field 'mixHash' for BlockMetadata")
}
b.MixHash = *dec.MixHash
if dec.TxList == nil {
return errors.New("missing required field 'txList' for BlockMetadata")
}
b.TxList = *dec.TxList
if dec.HighestBlockID == nil {
return errors.New("missing required field 'highestBlockID' for BlockMetadata")
}
b.HighestBlockID = dec.HighestBlockID
if dec.ExtraData == nil {
return errors.New("missing required field 'extraData' for BlockMetadata")
}
b.ExtraData = dec.ExtraData
return nil
}

View file

@ -5,9 +5,11 @@ package engine
import (
"encoding/json"
"errors"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
)
@ -21,6 +23,9 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) {
SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"`
Withdrawals []*types.Withdrawal `json:"withdrawals"`
BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"`
BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"`
BlockMetadata *BlockMetadata `json:"blockMetadata" gencodec:"required"`
L1Origin *rawdb.L1Origin `json:"l1Origin" gencodec:"required"`
}
var enc PayloadAttributes
enc.Timestamp = hexutil.Uint64(p.Timestamp)
@ -28,6 +33,9 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) {
enc.SuggestedFeeRecipient = p.SuggestedFeeRecipient
enc.Withdrawals = p.Withdrawals
enc.BeaconRoot = p.BeaconRoot
enc.BaseFeePerGas = p.BaseFeePerGas
enc.BlockMetadata = p.BlockMetadata
enc.L1Origin = p.L1Origin
return json.Marshal(&enc)
}
@ -39,6 +47,9 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error {
SuggestedFeeRecipient *common.Address `json:"suggestedFeeRecipient" gencodec:"required"`
Withdrawals []*types.Withdrawal `json:"withdrawals"`
BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"`
BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"`
BlockMetadata *BlockMetadata `json:"blockMetadata" gencodec:"required"`
L1Origin *rawdb.L1Origin `json:"l1Origin" gencodec:"required"`
}
var dec PayloadAttributes
if err := json.Unmarshal(input, &dec); err != nil {
@ -62,5 +73,17 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error {
if dec.BeaconRoot != nil {
p.BeaconRoot = dec.BeaconRoot
}
if dec.BaseFeePerGas == nil {
return errors.New("missing required field 'baseFeePerGas' for PayloadAttributes")
}
p.BaseFeePerGas = dec.BaseFeePerGas
if dec.BlockMetadata == nil {
return errors.New("missing required field 'blockMetadata' for PayloadAttributes")
}
p.BlockMetadata = dec.BlockMetadata
if dec.L1Origin == nil {
return errors.New("missing required field 'l1Origin' for PayloadAttributes")
}
p.L1Origin = dec.L1Origin
return nil
}

View file

@ -17,23 +17,26 @@ var _ = (*executableDataMarshaling)(nil)
// MarshalJSON marshals as JSON.
func (e ExecutableData) MarshalJSON() ([]byte, error) {
type ExecutableData struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"`
StateRoot common.Hash `json:"stateRoot" gencodec:"required"`
ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"`
LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"`
Random common.Hash `json:"prevRandao" gencodec:"required"`
Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"`
GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"`
ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"`
BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"`
BlockHash common.Hash `json:"blockHash" gencodec:"required"`
Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"`
Withdrawals []*types.Withdrawal `json:"withdrawals"`
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"`
StateRoot common.Hash `json:"stateRoot" gencodec:"required"`
ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"`
LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"`
Random common.Hash `json:"prevRandao" gencodec:"required"`
Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"`
GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"`
ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"`
BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"`
BlockHash common.Hash `json:"blockHash" gencodec:"required"`
Transactions []hexutil.Bytes `json:"transactions"`
Withdrawals []*types.Withdrawal `json:"withdrawals"`
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
TxHash common.Hash `json:"txHash"`
WithdrawalsHash common.Hash `json:"withdrawalsHash"`
TaikoBlock bool
}
var enc ExecutableData
enc.ParentHash = e.ParentHash
@ -58,29 +61,35 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) {
enc.Withdrawals = e.Withdrawals
enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed)
enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas)
enc.TxHash = e.TxHash
enc.WithdrawalsHash = e.WithdrawalsHash
enc.TaikoBlock = e.TaikoBlock
return json.Marshal(&enc)
}
// UnmarshalJSON unmarshals from JSON.
func (e *ExecutableData) UnmarshalJSON(input []byte) error {
type ExecutableData struct {
ParentHash *common.Hash `json:"parentHash" gencodec:"required"`
FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"`
StateRoot *common.Hash `json:"stateRoot" gencodec:"required"`
ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"`
LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"`
Random *common.Hash `json:"prevRandao" gencodec:"required"`
Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"`
GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"`
ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"`
BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"`
BlockHash *common.Hash `json:"blockHash" gencodec:"required"`
Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"`
Withdrawals []*types.Withdrawal `json:"withdrawals"`
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
ParentHash *common.Hash `json:"parentHash" gencodec:"required"`
FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"`
StateRoot *common.Hash `json:"stateRoot" gencodec:"required"`
ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"`
LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"`
Random *common.Hash `json:"prevRandao" gencodec:"required"`
Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"`
GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"`
GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"`
ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"`
BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"`
BlockHash *common.Hash `json:"blockHash" gencodec:"required"`
Transactions []hexutil.Bytes `json:"transactions"`
Withdrawals []*types.Withdrawal `json:"withdrawals"`
BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"`
ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"`
TxHash *common.Hash `json:"txHash"`
WithdrawalsHash *common.Hash `json:"withdrawalsHash"`
TaikoBlock *bool
}
var dec ExecutableData
if err := json.Unmarshal(input, &dec); err != nil {
@ -138,12 +147,11 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error {
return errors.New("missing required field 'blockHash' for ExecutableData")
}
e.BlockHash = *dec.BlockHash
if dec.Transactions == nil {
return errors.New("missing required field 'transactions' for ExecutableData")
}
e.Transactions = make([][]byte, len(dec.Transactions))
for k, v := range dec.Transactions {
e.Transactions[k] = v
if dec.Transactions != nil {
e.Transactions = make([][]byte, len(dec.Transactions))
for k, v := range dec.Transactions {
e.Transactions[k] = v
}
}
if dec.Withdrawals != nil {
e.Withdrawals = dec.Withdrawals
@ -154,5 +162,14 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error {
if dec.ExcessBlobGas != nil {
e.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas)
}
if dec.TxHash != nil {
e.TxHash = *dec.TxHash
}
if dec.WithdrawalsHash != nil {
e.WithdrawalsHash = *dec.WithdrawalsHash
}
if dec.TaikoBlock != nil {
e.TaikoBlock = *dec.TaikoBlock
}
return nil
}

View file

@ -22,6 +22,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/trie"
)
@ -46,6 +47,11 @@ type PayloadAttributes struct {
SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"`
Withdrawals []*types.Withdrawal `json:"withdrawals"`
BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"`
// CHANGE(taiko): extra fields.
BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"`
BlockMetadata *BlockMetadata `json:"blockMetadata" gencodec:"required"`
L1Origin *rawdb.L1Origin `json:"l1Origin" gencodec:"required"`
}
// JSON type overrides for PayloadAttributes.
@ -53,6 +59,29 @@ type payloadAttributesMarshaling struct {
Timestamp hexutil.Uint64
}
//go:generate go run github.com/fjl/gencodec -type BlockMetadata -field-override blockMetadataMarshaling -out gen_blockmetadata.go
// CHANGE(taiko): BlockMetadata represents a `BlockMetadata` struct defined in
// protocol.
type BlockMetadata struct {
// Fields defined in `LibData.blockMetadata`.
Beneficiary common.Address `json:"beneficiary" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
Timestamp uint64 `json:"timestamp" gencodec:"required"`
MixHash common.Hash `json:"mixHash" gencodec:"required"`
// Extra fields required in taiko-geth.
TxList []byte `json:"txList" gencodec:"required"`
HighestBlockID *big.Int `json:"highestBlockID" gencodec:"required"`
ExtraData []byte `json:"extraData" gencodec:"required"`
}
// CHANGE(taiko): JSON type overrides for BlockMetadata.
type blockMetadataMarshaling struct {
Timestamp hexutil.Uint64
TxList hexutil.Bytes
}
//go:generate go run github.com/fjl/gencodec -type ExecutableData -field-override executableDataMarshaling -out gen_ed.go
// ExecutableData is the data necessary to execute an EL payload.
@ -70,10 +99,14 @@ type ExecutableData struct {
ExtraData []byte `json:"extraData" gencodec:"required"`
BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"`
BlockHash common.Hash `json:"blockHash" gencodec:"required"`
Transactions [][]byte `json:"transactions" gencodec:"required"`
Transactions [][]byte `json:"transactions"`
Withdrawals []*types.Withdrawal `json:"withdrawals"`
BlobGasUsed *uint64 `json:"blobGasUsed"`
ExcessBlobGas *uint64 `json:"excessBlobGas"`
TxHash common.Hash `json:"txHash"` // CHANGE(taiko): allow passing txHash directly instead of transactions list
WithdrawalsHash common.Hash `json:"withdrawalsHash"` // CHANGE(taiko): allow passing WithdrawalsHash directly instead of withdrawals
TaikoBlock bool // CHANGE(taiko): whether this is a Taiko L2 block, only used by ExecutableDataToBlock
}
// JSON type overrides for executableData.

View file

@ -179,6 +179,9 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
}
backend, eth := utils.RegisterEthService(stack, &cfg.Eth)
// CHANGE(TAIKO): register Taiko RPC APIs.
utils.RegisterTaikoAPIs(stack, &cfg.Eth, eth)
// Create gauge with geth system and build information
if eth != nil { // The 'eth' backend may be nil in light mode
var protos []string

View file

@ -248,6 +248,9 @@ func init() {
debug.Flags,
metricsFlags,
)
// CHANGE(taiko): append Taiko flags into the original GETH flags
app.Flags = append(app.Flags, &utils.TaikoFlag)
flags.AutoEnvVars(app.Flags, "GETH")
app.Before = func(ctx *cli.Context) error {

View file

@ -1730,6 +1730,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
}
// Override any default configs for hard coded networks.
switch {
// CHANGE(taiko): when --taiko flag is set, use the Taiko genesis.
case ctx.IsSet(TaikoFlag.Name):
cfg.Genesis = core.TaikoGenesisBlock(cfg.NetworkId)
case ctx.Bool(MainnetFlag.Name):
if !ctx.IsSet(NetworkIdFlag.Name) {
cfg.NetworkId = 1

41
cmd/utils/taiko_flags.go Normal file
View file

@ -0,0 +1,41 @@
package utils
import (
"os"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
)
var (
TaikoFlag = cli.BoolFlag{
Name: "taiko",
Usage: "Taiko network",
}
)
// RegisterTaikoAPIs initializes and registers the Taiko RPC APIs.
func RegisterTaikoAPIs(stack *node.Node, cfg *ethconfig.Config, backend *eth.Ethereum) {
if os.Getenv("TAIKO_TEST") != "" {
return
}
// Add methods under "taiko_" RPC namespace to the available APIs list
stack.RegisterAPIs([]rpc.API{
{
Namespace: "taiko",
Version: params.VersionWithMeta,
Service: eth.NewTaikoAPIBackend(backend),
Public: true,
},
{
Namespace: "taikoAuth",
Version: params.VersionWithMeta,
Service: eth.NewTaikoAuthAPIBackend(backend),
Authenticated: true,
},
})
}

View file

@ -0,0 +1,358 @@
package taiko
import (
"bytes"
"errors"
"fmt"
"math/big"
"runtime"
"strings"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/trie"
"github.com/holiman/uint256"
)
var (
ErrOlderBlockTime = errors.New("timestamp older than parent")
ErrUnclesNotEmpty = errors.New("uncles not empty")
ErrEmptyBasefee = errors.New("empty base fee")
ErrEmptyWithdrawalsHash = errors.New("withdrawals hash missing")
ErrAnchorTxNotFound = errors.New("anchor transaction not found")
GoldenTouchAccount = common.HexToAddress("0x0000777735367b36bC9B61C50022d9D0700dB4Ec")
TaikoL2AddressSuffix = "10001"
AnchorSelector = crypto.Keccak256([]byte("anchor(bytes32,bytes32,uint64,uint32)"))[:4]
AnchorGasLimit = uint64(250_000)
)
// Taiko is a consensus engine used by L2 rollup.
type Taiko struct {
chainConfig *params.ChainConfig
taikoL2Address common.Address
}
var _ = new(Taiko)
func New(chainConfig *params.ChainConfig) *Taiko {
taikoL2AddressPrefix := strings.TrimPrefix(chainConfig.ChainID.String(), "0")
return &Taiko{
chainConfig: chainConfig,
taikoL2Address: common.HexToAddress(
"0x" +
taikoL2AddressPrefix +
strings.Repeat("0", common.AddressLength*2-len(taikoL2AddressPrefix)-len(TaikoL2AddressSuffix)) +
TaikoL2AddressSuffix,
),
}
}
// check all method stubs for interface `Engine` without affect performance.
var _ consensus.Engine = (*Taiko)(nil)
// Author retrieves the Ethereum address of the account that minted the given
// block, who proposes the block (not the prover).
func (t *Taiko) Author(header *types.Header) (common.Address, error) {
return header.Coinbase, nil
}
// VerifyHeader checks whether a header conforms to the consensus rules of a
// given engine. Verifying the seal may be done optionally here, or explicitly
// via the VerifySeal method.
func (t *Taiko) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header) error {
// Short circuit if the header is known, or its parent not
number := header.Number.Uint64()
if chain.GetHeader(header.Hash(), number) != nil {
return nil
}
parent := chain.GetHeader(header.ParentHash, number-1)
if parent == nil {
return consensus.ErrUnknownAncestor
}
// Sanity checks passed, do a proper verification
return t.verifyHeader(chain, header, parent, time.Now().Unix())
}
// VerifyHeaders is similar to VerifyHeader, but verifies a batch of headers
// concurrently. The method returns a quit channel to abort the operations and
// a results channel to retrieve the async verifications (the order is that of
// the input slice).
func (t *Taiko) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) (chan<- struct{}, <-chan error) {
// Spawn as many workers as allowed threads
workers := runtime.GOMAXPROCS(0)
if len(headers) < workers {
workers = len(headers)
}
// Create a task channel and spawn the verifiers
var (
inputs = make(chan int)
done = make(chan int, workers)
errors = make([]error, len(headers))
abort = make(chan struct{})
unixNow = time.Now().Unix()
)
for i := 0; i < workers; i++ {
go func() {
for index := range inputs {
errors[index] = t.verifyHeaderWorker(chain, headers, index, unixNow)
done <- index
}
}()
}
errorsOut := make(chan error, len(headers))
go func() {
defer close(inputs)
var (
in, out = 0, 0
checked = make([]bool, len(headers))
inputs = inputs
)
for {
select {
case inputs <- in:
if in++; in == len(headers) {
// Reached end of headers. Stop sending to workers.
inputs = nil
}
case index := <-done:
for checked[index] = true; checked[out]; out++ {
errorsOut <- errors[out]
if out == len(headers)-1 {
return
}
}
case <-abort:
return
}
}
}()
return abort, errorsOut
}
func (t *Taiko) verifyHeader(chain consensus.ChainHeaderReader, header, parent *types.Header, unixNow int64) error {
if header.Time > uint64(unixNow) {
return consensus.ErrFutureBlock
}
// Ensure that the header's extra-data section is of a reasonable size (<= 32 bytes)
if uint64(len(header.Extra)) > params.MaximumExtraDataSize {
return fmt.Errorf("extra-data too long: %d > %d", len(header.Extra), params.MaximumExtraDataSize)
}
// Timestamp should later than or equal to parent (when many L2 blocks included in one L1 block)
if header.Time < parent.Time {
return ErrOlderBlockTime
}
// Verify that the block number is parent's +1
if diff := new(big.Int).Sub(header.Number, parent.Number); diff.Cmp(big.NewInt(1)) != 0 {
return consensus.ErrInvalidNumber
}
// Difficulty should always be zero
if header.Difficulty != nil && header.Difficulty.Cmp(common.Big0) != 0 {
return fmt.Errorf("invalid difficulty: have %v, want %v", header.Difficulty, common.Big0)
}
// Verify that the gas limit is <= 2^63-1
if header.GasLimit > params.MaxGasLimit {
return fmt.Errorf("invalid gasLimit: have %v, max %v", header.GasLimit, params.MaxGasLimit)
}
// Verify that the gasUsed is <= gasLimit
if header.GasUsed > header.GasLimit {
return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit)
}
// Uncles should be empty
if header.UncleHash != types.CalcUncleHash(nil) {
return ErrUnclesNotEmpty
}
// BaseFee should not be empty
if header.BaseFee == nil {
return ErrEmptyBasefee
}
// WithdrawalsHash should not be empty
if header.WithdrawalsHash == nil {
return ErrEmptyWithdrawalsHash
}
return nil
}
func (t *Taiko) verifyHeaderWorker(chain consensus.ChainHeaderReader, headers []*types.Header, index int, unixNow int64) error {
var parent *types.Header
if index == 0 {
parent = chain.GetHeader(headers[0].ParentHash, headers[0].Number.Uint64()-1)
} else if headers[index-1].Hash() == headers[index].ParentHash {
parent = headers[index-1]
}
if parent == nil {
return consensus.ErrUnknownAncestor
}
return t.verifyHeader(chain, headers[index], parent, unixNow)
}
// VerifyUncles verifies that the given block's uncles conform to the consensus
// rules of a given engine.
//
// always returning an error for any uncles as this consensus mechanism doesn't permit uncles.
func (t *Taiko) VerifyUncles(chain consensus.ChainReader, block *types.Block) error {
if len(block.Uncles()) > 0 {
return ErrUnclesNotEmpty
}
return nil
}
// Prepare initializes the consensus fields of a block header according to the
// rules of a particular engine. The changes are executed inline.
func (t *Taiko) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error {
parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1)
if parent == nil {
return consensus.ErrUnknownAncestor
}
header.Difficulty = common.Big0
return nil
}
// Finalize runs any post-transaction state modifications (e.g. block rewards)
// but does not assemble the block.
//
// Note: The block header and state database might be updated to reflect any
// consensus rules that happen at finalization (e.g. block rewards).
func (t *Taiko) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, withdrawals []*types.Withdrawal) {
// no block rewards in l2
header.UncleHash = types.CalcUncleHash(nil)
header.Difficulty = common.Big0
// Withdrawals processing.
for _, w := range withdrawals {
state.AddBalance(w.Address, uint256.MustFromBig(new(big.Int).SetUint64(w.Amount)))
}
header.Root = state.IntermediateRoot(true)
}
// FinalizeAndAssemble runs any post-transaction state modifications (e.g. block
// rewards) and assembles the final block.
//
// Note: The block header and state database might be updated to reflect any
// consensus rules that happen at finalization (e.g. block rewards).
func (t *Taiko) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error) {
if withdrawals == nil {
withdrawals = make([]*types.Withdrawal, 0)
}
// Verify anchor transaction
if len(txs) != 0 { // Transactions list might be empty when building empty payload.
isAnchor, err := t.ValidateAnchorTx(txs[0], header)
if err != nil {
return nil, err
}
if !isAnchor {
return nil, ErrAnchorTxNotFound
}
}
// Finalize block
t.Finalize(chain, header, state, txs, uncles, withdrawals)
return types.NewBlockWithWithdrawals(
header, txs, nil /* ignore uncles */, receipts, withdrawals, trie.NewStackTrie(nil),
), nil
}
// Seal generates a new sealing request for the given input block and pushes
// the result into the given channel.
//
// Note, the method returns immediately and will send the result async. More
// than one result may also be returned depending on the consensus algorithm.
func (t *Taiko) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error {
header := block.Header()
// Sealing the genesis block is not supported
number := header.Number.Uint64()
if number == 0 {
return consensus.ErrInvalidNumber
}
select {
case results <- block.WithSeal(header):
case <-stop:
return nil
default:
log.Warn("Sealing result is not read by miner", "sealHash", t.SealHash(header))
}
return nil
}
// SealHash returns the hash of a block prior to it being sealed.
func (t *Taiko) SealHash(header *types.Header) common.Hash {
// Keccak(rlp(header))
return header.Hash()
}
// CalcDifficulty is the difficulty adjustment algorithm. It returns the difficulty
// that a new block should have.
func (t *Taiko) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int {
return common.Big0
}
// ValidateAnchorTx checks if the given transaction is a valid TaikoL2.anchor transaction.
func (t *Taiko) ValidateAnchorTx(tx *types.Transaction, header *types.Header) (bool, error) {
if tx.Type() != types.DynamicFeeTxType {
return false, nil
}
if tx.To() == nil || *tx.To() != t.taikoL2Address {
return false, nil
}
if !bytes.HasPrefix(tx.Data(), AnchorSelector) {
return false, nil
}
if tx.Value().Cmp(common.Big0) != 0 {
return false, nil
}
if tx.Gas() != AnchorGasLimit {
return false, nil
}
if tx.GasFeeCap().Cmp(header.BaseFee) != 0 {
return false, nil
}
s := types.MakeSigner(t.chainConfig, header.Number, header.Time)
addr, err := s.Sender(tx)
if err != nil {
return false, err
}
return strings.EqualFold(addr.String(), GoldenTouchAccount.String()), nil
}
// APIs returns the RPC APIs this consensus engine provides.
func (t *Taiko) APIs(chain consensus.ChainHeaderReader) []rpc.API {
return nil
}
// Close terminates any background threads maintained by the consensus engine.
func (t *Taiko) Close() error {
return nil
}

View file

@ -0,0 +1,237 @@
package taiko_test
import (
"bytes"
"math/big"
"strings"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/taiko"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/triedb"
"github.com/stretchr/testify/assert"
)
var (
testL2RollupAddress = common.HexToAddress("0x79fcdef22feed20eddacbb2587640e45491b757f")
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
goldenTouchKey, _ = crypto.HexToECDSA("92954368afd3caa1f3ce3ead0069c1af414054aefe1ef9aeacc1bf426222ce38")
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
genesis *core.Genesis
txs []*types.Transaction
testEngine *taiko.Taiko
)
func init() {
config := params.TestChainConfig
config.GrayGlacierBlock = nil
config.ArrowGlacierBlock = nil
config.Ethash = nil
config.Taiko = true
testEngine = taiko.New(config)
taikoL2AddressPrefix := strings.TrimPrefix(config.ChainID.String(), "0")
taikoL2Address := common.HexToAddress(
"0x" +
taikoL2AddressPrefix +
strings.Repeat("0", common.AddressLength*2-len(taikoL2AddressPrefix)-len(taiko.TaikoL2AddressSuffix)) +
taiko.TaikoL2AddressSuffix,
)
genesis = &core.Genesis{
Config: config,
Alloc: types.GenesisAlloc{testAddr: {Balance: big.NewInt(2e15)}},
ExtraData: []byte("test genesis"),
Timestamp: 9000,
Difficulty: common.Big0,
BaseFee: big.NewInt(params.InitialBaseFee),
}
txs = []*types.Transaction{
types.MustSignNewTx(goldenTouchKey, types.LatestSigner(genesis.Config), &types.DynamicFeeTx{
Nonce: 0,
GasTipCap: common.Big0,
GasFeeCap: new(big.Int).SetUint64(875_000_000),
Data: taiko.AnchorSelector,
Gas: taiko.AnchorGasLimit,
To: &taikoL2Address,
}),
types.MustSignNewTx(testKey, types.LatestSigner(genesis.Config), &types.LegacyTx{
Nonce: 0,
Value: big.NewInt(12),
GasPrice: big.NewInt(params.InitialBaseFee),
Gas: params.TxGas,
To: &common.Address{2},
}),
types.MustSignNewTx(testKey, types.LatestSigner(genesis.Config), &types.LegacyTx{
Nonce: 1,
Value: big.NewInt(8),
GasPrice: big.NewInt(params.InitialBaseFee),
Gas: params.TxGas,
To: &common.Address{2},
}),
// prepareBlockTx
types.MustSignNewTx(testKey, types.LatestSigner(genesis.Config), &types.LegacyTx{
Nonce: 2,
Value: big.NewInt(8),
GasPrice: big.NewInt(params.InitialBaseFee),
Gas: params.TxGas,
To: &testL2RollupAddress,
}),
}
}
func newTestBackend(t *testing.T) (*eth.Ethereum, []*types.Block) {
// Generate test chain.
blocks := generateTestChain()
// Create node
n, err := node.New(&node.Config{})
if err != nil {
t.Fatalf("can't create new node: %v", err)
}
// Create Ethereum Service
config := &ethconfig.Config{
Genesis: genesis,
}
ethservice, err := eth.New(n, config)
if err != nil {
t.Fatalf("can't create new ethereum service: %v", err)
}
// Import the test chain.
if err := n.Start(); err != nil {
t.Fatalf("can't start test node: %v", err)
}
if _, err := ethservice.BlockChain().InsertChain(blocks[1:]); err != nil {
t.Fatalf("can't import test blocks: %v", err)
}
if _, ok := ethservice.Engine().(*taiko.Taiko); !ok {
t.Fatalf("not use taiko engine")
}
return ethservice, blocks
}
func generateTestChain() []*types.Block {
db := rawdb.NewMemoryDatabase()
generate := func(i int, g *core.BlockGen) {
g.OffsetTime(5)
g.SetExtra([]byte("test_taiko"))
g.SetDifficulty(common.Big0)
for i, tx := range txs {
if i == 0 {
if err := tx.MarkAsAnchor(); err != nil {
panic(err)
}
}
g.AddTx(tx)
}
}
gblock := genesis.MustCommit(db, triedb.NewDatabase(db, triedb.HashDefaults))
blocks, _ := core.GenerateChain(genesis.Config, gblock, testEngine, db, 1, generate)
blocks = append([]*types.Block{gblock}, blocks...)
return blocks
}
func TestVerifyHeader(t *testing.T) {
ethService, blocks := newTestBackend(t)
for _, b := range blocks {
err := testEngine.VerifyHeader(ethService.BlockChain(), b.Header())
assert.NoErrorf(t, err, "VerifyHeader error: %s", err)
}
err := testEngine.VerifyHeader(ethService.BlockChain(), &types.Header{
Number: common.Big1,
Time: uint64(time.Now().Unix()),
BaseFee: big.NewInt(params.InitialBaseFee),
WithdrawalsHash: &types.EmptyWithdrawalsHash,
UncleHash: types.EmptyUncleHash,
})
assert.ErrorIs(t, err, consensus.ErrUnknownAncestor, "VerifyHeader should throw ErrUnknownAncestor when parentHash is unknown")
err = testEngine.VerifyHeader(ethService.BlockChain(), &types.Header{
ParentHash: blocks[len(blocks)-1].Hash(),
Number: common.Big0,
Time: uint64(time.Now().Unix()),
BaseFee: big.NewInt(params.InitialBaseFee),
WithdrawalsHash: &types.EmptyWithdrawalsHash,
UncleHash: types.EmptyUncleHash,
})
assert.ErrorIs(t, err, consensus.ErrInvalidNumber, "VerifyHeader should throw ErrInvalidNumber when the block number is wrong")
err = testEngine.VerifyHeader(ethService.BlockChain(), &types.Header{
ParentHash: blocks[len(blocks)-1].Hash(),
Number: new(big.Int).SetInt64(int64(len(blocks))),
Time: uint64(time.Now().Unix()),
Extra: bytes.Repeat([]byte{1}, int(params.MaximumExtraDataSize+1)),
BaseFee: big.NewInt(params.InitialBaseFee),
WithdrawalsHash: &types.EmptyWithdrawalsHash,
UncleHash: types.EmptyUncleHash,
})
assert.ErrorContains(t, err, "extra-data too long", "VerifyHeader should throw ErrExtraDataTooLong when the block has too much extra data")
err = testEngine.VerifyHeader(ethService.BlockChain(), &types.Header{
ParentHash: blocks[len(blocks)-1].Hash(),
Number: new(big.Int).SetInt64(int64(len(blocks))),
Time: uint64(time.Now().Unix()),
Difficulty: common.Big1,
BaseFee: big.NewInt(params.InitialBaseFee),
WithdrawalsHash: &types.EmptyWithdrawalsHash,
UncleHash: types.EmptyUncleHash,
})
assert.ErrorContains(t, err, "invalid difficulty", "VerifyHeader should throw ErrInvalidDifficulty when difficulty is not 0")
err = testEngine.VerifyHeader(ethService.BlockChain(), &types.Header{
ParentHash: blocks[len(blocks)-1].Hash(),
Number: new(big.Int).SetInt64(int64(len(blocks))),
Time: uint64(time.Now().Unix()),
GasLimit: params.MaxGasLimit + 1,
BaseFee: big.NewInt(params.InitialBaseFee),
WithdrawalsHash: &types.EmptyWithdrawalsHash,
UncleHash: types.EmptyUncleHash,
})
assert.ErrorContains(t, err, "invalid gasLimit", "VerifyHeader should throw ErrInvalidGasLimit when gasLimit is higher than the limit")
err = testEngine.VerifyHeader(ethService.BlockChain(), &types.Header{
ParentHash: blocks[len(blocks)-1].Hash(),
Number: new(big.Int).SetInt64(int64(len(blocks))),
Time: uint64(time.Now().Unix()),
GasLimit: params.MaxGasLimit,
BaseFee: big.NewInt(params.InitialBaseFee),
UncleHash: types.EmptyUncleHash,
})
assert.ErrorContains(t, err, "withdrawals hash missing", "VerifyHeader should throw ErrWithdrawalsHashMissing withdrawalshash is nil")
err = testEngine.VerifyHeader(ethService.BlockChain(), &types.Header{
ParentHash: blocks[len(blocks)-1].Hash(),
Number: new(big.Int).SetInt64(int64(len(blocks))),
Time: uint64(time.Now().Unix()),
GasLimit: params.MaxGasLimit,
BaseFee: big.NewInt(params.InitialBaseFee),
WithdrawalsHash: &types.EmptyWithdrawalsHash,
})
assert.ErrorContains(t, err, "uncles not empty", "VerifyHeader should throw ErrUnclesNotEmpty if uncles is not the empty hash")
}

View file

@ -0,0 +1,60 @@
// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
package rawdb
import (
"encoding/json"
"errors"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
)
var _ = (*l1OriginMarshaling)(nil)
// MarshalJSON marshals as JSON.
func (l L1Origin) MarshalJSON() ([]byte, error) {
type L1Origin struct {
BlockID *math.HexOrDecimal256 `json:"blockID" gencodec:"required"`
L2BlockHash common.Hash `json:"l2BlockHash"`
L1BlockHeight *math.HexOrDecimal256 `json:"l1BlockHeight" gencodec:"required"`
L1BlockHash common.Hash `json:"l1BlockHash" gencodec:"required"`
}
var enc L1Origin
enc.BlockID = (*math.HexOrDecimal256)(l.BlockID)
enc.L2BlockHash = l.L2BlockHash
enc.L1BlockHeight = (*math.HexOrDecimal256)(l.L1BlockHeight)
enc.L1BlockHash = l.L1BlockHash
return json.Marshal(&enc)
}
// UnmarshalJSON unmarshals from JSON.
func (l *L1Origin) UnmarshalJSON(input []byte) error {
type L1Origin struct {
BlockID *math.HexOrDecimal256 `json:"blockID" gencodec:"required"`
L2BlockHash *common.Hash `json:"l2BlockHash"`
L1BlockHeight *math.HexOrDecimal256 `json:"l1BlockHeight" gencodec:"required"`
L1BlockHash *common.Hash `json:"l1BlockHash" gencodec:"required"`
}
var dec L1Origin
if err := json.Unmarshal(input, &dec); err != nil {
return err
}
if dec.BlockID == nil {
return errors.New("missing required field 'blockID' for L1Origin")
}
l.BlockID = (*big.Int)(dec.BlockID)
if dec.L2BlockHash != nil {
l.L2BlockHash = *dec.L2BlockHash
}
if dec.L1BlockHeight == nil {
return errors.New("missing required field 'l1BlockHeight' for L1Origin")
}
l.L1BlockHeight = (*big.Int)(dec.L1BlockHeight)
if dec.L1BlockHash == nil {
return errors.New("missing required field 'l1BlockHash' for L1Origin")
}
l.L1BlockHash = *dec.L1BlockHash
return nil
}

View file

@ -0,0 +1,92 @@
package rawdb
import (
"bytes"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
)
var (
// Database key prefix for L2 block's L1Origin.
l1OriginPrefix = []byte("TKO:L1O")
headL1OriginKey = []byte("TKO:LastL1O")
)
// l1OriginKey calculates the L1Origin key.
// l1OriginPrefix + l2HeaderHash -> l1OriginKey
func l1OriginKey(blockID *big.Int) []byte {
data, _ := (*math.HexOrDecimal256)(blockID).MarshalText()
return append(l1OriginPrefix, data...)
}
//go:generate go run github.com/fjl/gencodec -type L1Origin -field-override l1OriginMarshaling -out gen_taiko_l1_origin.go
// L1Origin represents a L1Origin of a L2 block.
type L1Origin struct {
BlockID *big.Int `json:"blockID" gencodec:"required"`
L2BlockHash common.Hash `json:"l2BlockHash"`
L1BlockHeight *big.Int `json:"l1BlockHeight" gencodec:"required"`
L1BlockHash common.Hash `json:"l1BlockHash" gencodec:"required"`
}
type l1OriginMarshaling struct {
BlockID *math.HexOrDecimal256
L1BlockHeight *math.HexOrDecimal256
}
// WriteL1Origin stores a L1Origin into the database.
func WriteL1Origin(db ethdb.KeyValueWriter, blockID *big.Int, l1Origin *L1Origin) {
data, err := rlp.EncodeToBytes(l1Origin)
if err != nil {
log.Crit("Failed to encode L1Origin", "err", err)
}
if err := db.Put(l1OriginKey(blockID), data); err != nil {
log.Crit("Failed to store L1Origin", "err", err)
}
}
// ReadL1Origin retrieves the given L2 block's L1Origin from database.
func ReadL1Origin(db ethdb.KeyValueReader, blockID *big.Int) (*L1Origin, error) {
data, _ := db.Get(l1OriginKey(blockID))
if len(data) == 0 {
return nil, nil
}
l1Origin := new(L1Origin)
if err := rlp.Decode(bytes.NewReader(data), l1Origin); err != nil {
return nil, fmt.Errorf("invalid L1Origin RLP bytes: %w", err)
}
return l1Origin, nil
}
// WriteHeadL1Origin stores the given L1Origin as the last L1Origin.
func WriteHeadL1Origin(db ethdb.KeyValueWriter, blockID *big.Int) {
data, _ := (*math.HexOrDecimal256)(blockID).MarshalText()
if err := db.Put(headL1OriginKey, data); err != nil {
log.Crit("Failed to store head L1Origin", "error", err)
}
}
// ReadHeadL1Origin retrieves the last L1Origin from database.
func ReadHeadL1Origin(db ethdb.KeyValueReader) (*big.Int, error) {
data, _ := db.Get(headL1OriginKey)
if len(data) == 0 {
return nil, nil
}
blockID := new(math.HexOrDecimal256)
if err := blockID.UnmarshalText(data); err != nil {
log.Error("Unmarshal L1Origin unmarshal error", "error", err)
return nil, fmt.Errorf("invalid L1Origin unmarshal: %w", err)
}
return (*big.Int)(blockID), nil
}

View file

@ -0,0 +1,59 @@
package rawdb
import (
"crypto/rand"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// randomBigInt generates a random big integer.
func randomBigInt() *big.Int {
randomBigInt, err := rand.Int(rand.Reader, common.Big256)
if err != nil {
log.Crit(err.Error())
}
return randomBigInt
}
// randomHash generates a random blob of data and returns it as a hash.
func randomHash() common.Hash {
var hash common.Hash
if n, err := rand.Read(hash[:]); n != common.HashLength || err != nil {
panic(err)
}
return hash
}
func TestL1Origin(t *testing.T) {
db := NewMemoryDatabase()
testL1Origin := &L1Origin{
BlockID: randomBigInt(),
L2BlockHash: randomHash(),
L1BlockHeight: randomBigInt(),
L1BlockHash: randomHash(),
}
WriteL1Origin(db, testL1Origin.BlockID, testL1Origin)
l1Origin, err := ReadL1Origin(db, testL1Origin.BlockID)
require.Nil(t, err)
require.NotNil(t, l1Origin)
assert.Equal(t, testL1Origin.BlockID, l1Origin.BlockID)
assert.Equal(t, testL1Origin.L2BlockHash, l1Origin.L2BlockHash)
assert.Equal(t, testL1Origin.L1BlockHeight, l1Origin.L1BlockHeight)
assert.Equal(t, testL1Origin.L1BlockHash, l1Origin.L1BlockHash)
}
func TestHeadL1Origin(t *testing.T) {
db := NewMemoryDatabase()
testBlockID := randomBigInt()
WriteHeadL1Origin(db, testBlockID)
blockID, err := ReadHeadL1Origin(db)
require.Nil(t, err)
require.NotNil(t, blockID)
assert.Equal(t, testBlockID, blockID)
}

View file

@ -81,6 +81,11 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
}
// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
if i == 0 && p.config.Taiko {
if err := tx.MarkAsAnchor(); err != nil {
return nil, nil, 0, err
}
}
msg, err := TransactionToMessage(tx, signer, header.BaseFee)
if err != nil {
return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)

View file

@ -20,6 +20,7 @@ import (
"fmt"
"math"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/common"
cmath "github.com/ethereum/go-ethereum/common/math"
@ -145,6 +146,9 @@ type Message struct {
// account nonce in state. It also disables checking that the sender is an EOA.
// This field will be set to true for operations like RPC eth_call.
SkipAccountChecks bool
// CHANGE(taiko): whether the current transaction is the first TaikoL2.anchor transaction in a block.
IsAnchor bool
}
// TransactionToMessage converts a transaction into a Message.
@ -162,6 +166,7 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In
SkipAccountChecks: false,
BlobHashes: tx.BlobHashes(),
BlobGasFeeCap: tx.BlobGasFeeCap(),
IsAnchor: tx.IsAnchor(),
}
// If baseFee provided, set gasPrice to effectiveGasPrice.
if baseFee != nil {
@ -257,6 +262,10 @@ func (st *StateTransition) buyGas() error {
if overflow {
return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex())
}
if st.msg.IsAnchor {
balanceCheckU256 = common.U2560
mgval = common.Big0
}
if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; have.Cmp(want) < 0 {
return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want)
}
@ -297,7 +306,8 @@ func (st *StateTransition) preCheck() error {
// Make sure that transaction gasFeeCap is greater than the baseFee (post london)
if st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) {
// Skip the checks if gas fields are zero and baseFee was explicitly disabled (eth_call)
skipCheck := st.evm.Config.NoBaseFee && msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0
skipCheck := (st.evm.Config.NoBaseFee && msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0) ||
st.msg.IsAnchor
if !skipCheck {
if l := msg.GasFeeCap.BitLen(); l > 256 {
return fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh,
@ -457,6 +467,13 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
fee := new(uint256.Int).SetUint64(st.gasUsed())
fee.Mul(fee, effectiveTipU256)
st.state.AddBalance(st.evm.Context.Coinbase, fee)
// CHANGE(taiko): basefee is not burnt, but sent to a treasury instead.
if st.evm.ChainConfig().Taiko && st.evm.Context.BaseFee != nil && !st.msg.IsAnchor {
st.state.AddBalance(
st.getTreasuryAddress(),
uint256.MustFromBig(new(big.Int).Mul(st.evm.Context.BaseFee, new(big.Int).SetUint64(st.gasUsed()))),
)
}
}
return &ExecutionResult{
@ -475,10 +492,13 @@ func (st *StateTransition) refundGas(refundQuotient uint64) uint64 {
}
st.gasRemaining += refund
// Return ETH for remaining gas, exchanged at the original rate.
remaining := uint256.NewInt(st.gasRemaining)
remaining = remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice))
st.state.AddBalance(st.msg.From, remaining)
// Do not change the balance in anchor transactions.
if !st.msg.IsAnchor {
// Return ETH for remaining gas, exchanged at the original rate.
remaining := uint256.NewInt(st.gasRemaining)
remaining = remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice))
st.state.AddBalance(st.msg.From, remaining)
}
// Also return remaining gas to the block gas counter so it is
// available for the next transaction.
@ -496,3 +516,17 @@ func (st *StateTransition) gasUsed() uint64 {
func (st *StateTransition) blobGasUsed() uint64 {
return uint64(len(st.msg.BlobHashes) * params.BlobTxBlobGasPerBlob)
}
// CHANGE(taiko): returns the treasury address based on chain ID.
func (st *StateTransition) getTreasuryAddress() common.Address {
var (
prefix = st.evm.ChainConfig().ChainID.String()
suffix = "10001"
)
return common.HexToAddress(
"0x" +
prefix +
strings.Repeat("0", common.AddressLength*2-len(prefix)-len(suffix)) +
suffix,
)
}

67
core/taiko_genesis.go Normal file
View file

@ -0,0 +1,67 @@
package core
import (
"math/big"
"github.com/ethereum/go-ethereum/common"
taikoGenesis "github.com/ethereum/go-ethereum/core/taiko_genesis"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)
// TaikoGenesisBlock returns the Taiko network genesis block configs.
func TaikoGenesisBlock(networkID uint64) *Genesis {
chainConfig := params.TaikoChainConfig
var allocJSON []byte
switch networkID {
case params.TaikoMainnetNetworkID.Uint64():
chainConfig.ChainID = params.TaikoMainnetNetworkID
allocJSON = taikoGenesis.MainnetGenesisAllocJSON
case params.TaikoInternalL2ANetworkID.Uint64():
chainConfig.ChainID = params.TaikoInternalL2ANetworkID
allocJSON = taikoGenesis.InternalL2AGenesisAllocJSON
case params.TaikoInternalL2BNetworkID.Uint64():
chainConfig.ChainID = params.TaikoInternalL2BNetworkID
allocJSON = taikoGenesis.InternalL2BGenesisAllocJSON
case params.SnaefellsjokullNetworkID.Uint64():
chainConfig.ChainID = params.SnaefellsjokullNetworkID
allocJSON = taikoGenesis.SnaefellsjokullGenesisAllocJSON
case params.AskjaNetworkID.Uint64():
chainConfig.ChainID = params.AskjaNetworkID
allocJSON = taikoGenesis.AskjaGenesisAllocJSON
case params.GrimsvotnNetworkID.Uint64():
chainConfig.ChainID = params.GrimsvotnNetworkID
allocJSON = taikoGenesis.GrimsvotnGenesisAllocJSON
case params.EldfellNetworkID.Uint64():
chainConfig.ChainID = params.EldfellNetworkID
allocJSON = taikoGenesis.EldfellGenesisAllocJSON
case params.JolnirNetworkID.Uint64():
chainConfig.ChainID = params.JolnirNetworkID
allocJSON = taikoGenesis.JolnirGenesisAllocJSON
case params.KatlaNetworkID.Uint64():
chainConfig.ChainID = params.KatlaNetworkID
allocJSON = taikoGenesis.KatlaGenesisAllocJSON
case params.HeklaNetworkID.Uint64():
chainConfig.ChainID = params.HeklaNetworkID
allocJSON = taikoGenesis.HeklaGenesisAllocJSON
default:
chainConfig.ChainID = params.TaikoInternalL2ANetworkID
allocJSON = taikoGenesis.InternalL2AGenesisAllocJSON
}
var alloc GenesisAlloc
if err := alloc.UnmarshalJSON(allocJSON); err != nil {
log.Crit("unmarshal alloc json error", "error", err)
}
return &Genesis{
Config: chainConfig,
ExtraData: []byte{},
GasLimit: uint64(15_000_000),
Difficulty: common.Big0,
Alloc: alloc,
GasUsed: 0,
BaseFee: new(big.Int).SetUint64(10_000_000),
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,35 @@
package taiko_genesis
import (
_ "embed"
)
//go:embed internal_l2a.json
var InternalL2AGenesisAllocJSON []byte
//go:embed internal_l2b.json
var InternalL2BGenesisAllocJSON []byte
//go:embed snaefellsjokull.json
var SnaefellsjokullGenesisAllocJSON []byte
//go:embed askja.json
var AskjaGenesisAllocJSON []byte
//go:embed grimsvotn.json
var GrimsvotnGenesisAllocJSON []byte
//go:embed eldfell.json
var EldfellGenesisAllocJSON []byte
//go:embed jolnir.json
var JolnirGenesisAllocJSON []byte
//go:embed katla.json
var KatlaGenesisAllocJSON []byte
//go:embed hekla.json
var HeklaGenesisAllocJSON []byte
//go:embed mainnet.json
var MainnetGenesisAllocJSON []byte

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -18,8 +18,10 @@ package txpool
import (
"crypto/sha256"
"errors"
"fmt"
"math/big"
"os"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
@ -96,6 +98,10 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
if tx.GasFeeCapIntCmp(tx.GasTipCap()) < 0 {
return core.ErrTipAboveFeeCap
}
// CHANGE(taiko): ensure gasFeeCap fee cap larger than 0.1 GWei
if os.Getenv("TAIKO_TEST") == "" && tx.GasFeeCap().Cmp(new(big.Int).SetUint64(params.GWei/10)) < 0 {
return errors.New("max fee per gas is less than 0.1 gwei")
}
// Make sure the transaction is signed properly
if _, err := types.Sender(signer, tx); err != nil {
return ErrInvalidSender

View file

@ -0,0 +1,42 @@
package types
func (tx *Transaction) MarkAsAnchor() error {
return tx.inner.markAsAnchor()
}
func (tx *Transaction) IsAnchor() bool {
return tx.inner.isAnchor()
}
func (tx *DynamicFeeTx) isAnchor() bool {
return tx.isAnhcor
}
func (tx *LegacyTx) isAnchor() bool {
return false
}
func (tx *AccessListTx) isAnchor() bool {
return false
}
func (tx *BlobTx) isAnchor() bool {
return false
}
func (tx *DynamicFeeTx) markAsAnchor() error {
tx.isAnhcor = true
return nil
}
func (tx *LegacyTx) markAsAnchor() error {
return ErrInvalidTxType
}
func (tx *AccessListTx) markAsAnchor() error {
return ErrInvalidTxType
}
func (tx *BlobTx) markAsAnchor() error {
return ErrInvalidTxType
}

View file

@ -87,6 +87,10 @@ type TxData interface {
nonce() uint64
to() *common.Address
// CHANGE(taiko): anchor transaction related.
isAnchor() bool
markAsAnchor() error
rawSignatureValues() (v, r, s *big.Int)
setSignatureValues(chainID, v, r, s *big.Int)
@ -589,3 +593,15 @@ func copyAddressPtr(a *common.Address) *common.Address {
cpy := *a
return &cpy
}
// CHANGE(taiko): SetBlobTxSidecar sets the sidecar of a transaction.
// The sidecar should match the blob-tx versioned hashes, or the transaction will be invalid.
// This allows tools to easily re-attach blob sidecars to signed transactions that omit the sidecar.
func (tx *Transaction) SetBlobTxSidecar(sidecar *BlobTxSidecar) error {
blobtx, ok := tx.inner.(*BlobTx)
if !ok {
return fmt.Errorf("not a blob tx, type = %d", tx.Type())
}
blobtx.Sidecar = sidecar
return nil
}

View file

@ -40,6 +40,9 @@ type DynamicFeeTx struct {
V *big.Int `json:"v" gencodec:"required"`
R *big.Int `json:"r" gencodec:"required"`
S *big.Int `json:"s" gencodec:"required"`
// CHANGE(taiko): if this transaction is the first TaikoL2.anchor transaction in a block
isAnhcor bool
}
// copy creates a deep copy of the transaction data and initializes all fields.
@ -58,6 +61,7 @@ func (tx *DynamicFeeTx) copy() TxData {
V: new(big.Int),
R: new(big.Int),
S: new(big.Int),
isAnhcor: tx.isAnhcor,
}
copy(cpy.AccessList, tx.AccessList)
if tx.Value != nil {

View file

@ -20,6 +20,7 @@ package catalyst
import (
"errors"
"fmt"
"math/big"
"sync"
"time"
@ -318,6 +319,10 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl
PayloadID: id,
}
}
// CHANGE(taiko): check whether --taiko flag is set.
isTaiko := api.eth.BlockChain().Config().Taiko
if rawdb.ReadCanonicalHash(api.eth.ChainDb(), block.NumberU64()) != update.HeadBlockHash {
// Block is not canonical, set head.
if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil {
@ -327,6 +332,10 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl
// If the specified head matches with our local head, do nothing and keep
// generating the payload. It's a special corner case that a few slots are
// missing and we are requested to generate the payload in slot.
} else if isTaiko { // CHANGE(taiko): reorg is allowed in L2.
if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil {
return engine.ForkChoiceResponse{PayloadStatus: engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &latestValid}}, err
}
} else {
// If the head block is already in our canonical chain, the beacon client is
// probably resyncing. Ignore the update.
@ -371,6 +380,56 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl
// sealed by the beacon client. The payload will be requested later, and we
// will replace it arbitrarily many times in between.
if payloadAttributes != nil {
// CHANGE(taiko): create a L2 block by Taiko protocol.
if isTaiko {
// No need to check payloadAttribute here, because all its fields are
// marked as required.
block, err := api.eth.Miner().SealBlockWith(
update.HeadBlockHash,
payloadAttributes.Timestamp,
payloadAttributes.BlockMetadata,
payloadAttributes.BaseFeePerGas,
payloadAttributes.Withdrawals,
)
if err != nil {
log.Error("Failed to create sealing block", "err", err)
return valid(nil), engine.InvalidPayloadAttributes.With(err)
}
// Cache the mined block for later use.
args := &miner.BuildPayloadArgs{
Parent: block.ParentHash(),
Timestamp: block.Time(),
FeeRecipient: block.Coinbase(),
Random: block.MixDigest(),
Withdrawals: block.Withdrawals(),
Version: payloadVersion,
}
id := args.Id()
payload, err := api.eth.Miner().BuildPayload(args)
if err != nil {
log.Error("Failed to build payload", "err", err)
return valid(nil), engine.InvalidPayloadAttributes.With(err)
}
payload.SetFullBlock(block, common.Big0)
api.localBlocks.put(id, payload)
// L1Origin **MUST NOT** be nil, it's a required field in PayloadAttributesV1.
l1Origin := payloadAttributes.L1Origin
// Set the block hash before inserting the L1Origin into database.
l1Origin.L2BlockHash = block.Hash()
// Write L1Origin.
rawdb.WriteL1Origin(api.eth.ChainDb(), l1Origin.BlockID, l1Origin)
// Write the head L1Origin.
rawdb.WriteHeadL1Origin(api.eth.ChainDb(), l1Origin.BlockID)
return valid(&id), nil
}
args := &miner.BuildPayloadArgs{
Parent: update.HeadBlockHash,
Timestamp: payloadAttributes.Timestamp,
@ -491,7 +550,8 @@ func (api *ConsensusAPI) NewPayloadV2(params engine.ExecutableData) (engine.Payl
return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("can't use newPayloadV2 post-cancun"))
}
if api.eth.BlockChain().Config().LatestFork(params.Timestamp) == forks.Shanghai {
if params.Withdrawals == nil {
if params.Withdrawals == nil &&
(api.eth.BlockChain().Config().Taiko && params.WithdrawalsHash == (common.Hash{})) {
return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil withdrawals post-shanghai"))
}
} else {
@ -551,10 +611,37 @@ func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashe
defer api.newPayloadLock.Unlock()
log.Trace("Engine API request received", "method", "NewPayload", "number", params.Number, "hash", params.BlockHash)
block, err := engine.ExecutableDataToBlock(params, versionedHashes, beaconRoot)
if err != nil {
log.Warn("Invalid NewPayload params", "params", params, "error", err)
return api.invalid(err, nil), nil
// CHANGE(taiko): allow passing the executable data with txHash instead of all transactions.
var (
block *types.Block
err error
)
params.TaikoBlock = api.eth.BlockChain().Config().Taiko
if api.eth.BlockChain().Config().Taiko && params.Transactions == nil && params.Withdrawals == nil {
block = types.NewBlockWithHeader(&types.Header{
ParentHash: params.ParentHash,
UncleHash: types.EmptyUncleHash,
Coinbase: params.FeeRecipient,
Root: params.StateRoot,
TxHash: params.TxHash,
ReceiptHash: params.ReceiptsRoot,
Bloom: types.BytesToBloom(params.LogsBloom),
Difficulty: common.Big0,
Number: new(big.Int).SetUint64(params.Number),
GasLimit: params.GasLimit,
GasUsed: params.GasUsed,
Time: params.Timestamp,
BaseFee: params.BaseFeePerGas,
Extra: params.ExtraData,
MixDigest: params.Random,
WithdrawalsHash: &params.WithdrawalsHash,
})
} else {
block, err = engine.ExecutableDataToBlock(params, versionedHashes, beaconRoot)
if err != nil {
log.Debug("Invalid NewPayload params", "params", params, "error", err)
return engine.PayloadStatusV1{Status: engine.INVALID}, nil
}
}
// Stash away the last update to warn the user if the beacon client goes offline
api.lastNewPayloadLock.Lock()
@ -597,9 +684,18 @@ func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashe
log.Error("Ignoring pre-merge parent block", "number", params.Number, "hash", params.BlockHash, "td", ptd, "ttd", ttd)
return engine.INVALID_TERMINAL_BLOCK, nil
}
if block.Time() <= parent.Time() {
log.Warn("Invalid timestamp", "parent", block.Time(), "block", block.Time())
return api.invalid(errors.New("invalid timestamp"), parent.Header()), nil
// CHANGE(taiko): a block that has the same timestamp as its parents is
// allowed in Taiko protocol.
if api.eth.BlockChain().Config().Taiko {
if block.Time() < parent.Time() {
log.Warn("Invalid timestamp", "parent", block.Time(), "block", block.Time())
return api.invalid(errors.New("invalid timestamp"), parent.Header()), nil
}
} else {
if block.Time() <= parent.Time() {
log.Warn("Invalid timestamp", "parent", block.Time(), "block", block.Time())
return api.invalid(errors.New("invalid timestamp"), parent.Header()), nil
}
}
// Another corner case: if the node is in snap sync mode, but the CL client
// tries to make it import a block. That should be denied as pushing something

View file

@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/beacon"
"github.com/ethereum/go-ethereum/consensus/clique"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/consensus/taiko"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool/blobpool"
"github.com/ethereum/go-ethereum/core/txpool/legacypool"
@ -165,6 +166,10 @@ type Config struct {
// Clique is allowed for now to live standalone, but ethash is forbidden and can
// only exist on already merged networks.
func CreateConsensusEngine(config *params.ChainConfig, db ethdb.Database) (consensus.Engine, error) {
// CHANGE(taiko): use Taiko consensus engine when the --taiko flag is set.
if config.Taiko {
return taiko.New(config), nil
}
// If proof-of-authority is requested, set it up
if config.Clique != nil {
return beacon.New(clique.New(config.Clique, db)), nil

View file

@ -239,6 +239,11 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block,
// Recompute transactions up to the target index.
signer := types.MakeSigner(eth.blockchain.Config(), block.Number(), block.Time())
for idx, tx := range block.Transactions() {
if idx == 0 && eth.config.Genesis.Config.Taiko {
if err := tx.MarkAsAnchor(); err != nil {
return nil, vm.BlockContext{}, nil, nil, err
}
}
// Assemble the transaction call message and return if the requested offset
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
txContext := core.NewEVMTxContext(msg)

104
eth/taiko_api_backend.go Normal file
View file

@ -0,0 +1,104 @@
package eth
import (
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/miner"
)
// TaikoAPIBackend handles L2 node related RPC calls.
type TaikoAPIBackend struct {
eth *Ethereum
}
// NewTaikoAPIBackend creates a new TaikoAPIBackend instance.
func NewTaikoAPIBackend(eth *Ethereum) *TaikoAPIBackend {
return &TaikoAPIBackend{
eth: eth,
}
}
// HeadL1Origin returns the latest L2 block's corresponding L1 origin.
func (s *TaikoAPIBackend) HeadL1Origin() (*rawdb.L1Origin, error) {
blockID, err := rawdb.ReadHeadL1Origin(s.eth.ChainDb())
if err != nil {
return nil, err
}
if blockID == nil {
return nil, ethereum.NotFound
}
l1Origin, err := rawdb.ReadL1Origin(s.eth.ChainDb(), blockID)
if err != nil {
return nil, err
}
if l1Origin == nil {
return nil, ethereum.NotFound
}
return l1Origin, nil
}
// L1OriginByID returns the L2 block's corresponding L1 origin.
func (s *TaikoAPIBackend) L1OriginByID(blockID *math.HexOrDecimal256) (*rawdb.L1Origin, error) {
l1Origin, err := rawdb.ReadL1Origin(s.eth.ChainDb(), (*big.Int)(blockID))
if err != nil {
return nil, err
}
if l1Origin == nil {
return nil, ethereum.NotFound
}
return l1Origin, nil
}
// GetSyncMode returns the node sync mode.
func (s *TaikoAPIBackend) GetSyncMode() (string, error) {
return s.eth.config.SyncMode.String(), nil
}
// TaikoAuthAPIBackend handles L2 node related authorized RPC calls.
type TaikoAuthAPIBackend struct {
eth *Ethereum
}
// NewTaikoAuthAPIBackend creates a new TaikoAuthAPIBackend instance.
func NewTaikoAuthAPIBackend(eth *Ethereum) *TaikoAuthAPIBackend {
return &TaikoAuthAPIBackend{eth}
}
// TxPoolContent retrieves the transaction pool content with the given upper limits.
func (a *TaikoAuthAPIBackend) TxPoolContent(
beneficiary common.Address,
baseFee *big.Int,
blockMaxGasLimit uint64,
maxBytesPerTxList uint64,
locals []string,
maxTransactionsLists uint64,
) ([]*miner.PreBuiltTxList, error) {
log.Debug(
"Fetching L2 pending transactions finished",
"baseFee", baseFee,
"blockMaxGasLimit", blockMaxGasLimit,
"maxBytesPerTxList", maxBytesPerTxList,
"maxTransactions", maxTransactionsLists,
"locals", locals,
)
return a.eth.Miner().BuildTransactionsLists(
beneficiary,
baseFee,
blockMaxGasLimit,
maxBytesPerTxList,
locals,
maxTransactionsLists,
)
}

View file

@ -270,6 +270,13 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed
)
// Trace all the transactions contained within
for i, tx := range task.block.Transactions() {
if i == 0 && api.backend.ChainConfig().Taiko {
if err := tx.MarkAsAnchor(); err != nil {
log.Warn("Mark anchor transaction error", "error", err)
task.results[i] = &txTraceResult{TxHash: tx.Hash(), Error: err.Error()}
break
}
}
msg, _ := core.TransactionToMessage(tx, signer, task.block.BaseFee())
txctx := &Context{
BlockHash: task.block.Hash(),
@ -527,6 +534,11 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config
deleteEmptyObjects = chainConfig.IsEIP158(block.Number())
)
for i, tx := range block.Transactions() {
if i == 0 && chainConfig.Taiko {
if err := tx.MarkAsAnchor(); err != nil {
return nil, err
}
}
if err := ctx.Err(); err != nil {
return nil, err
}
@ -604,6 +616,11 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
results = make([]*txTraceResult, len(txs))
)
for i, tx := range txs {
if i == 0 && api.backend.ChainConfig().Taiko {
if err := tx.MarkAsAnchor(); err != nil {
return nil, err
}
}
// Generate the next state snapshot fast without tracing
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
txctx := &Context{
@ -757,6 +774,11 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
chainConfig, canon = overrideConfig(chainConfig, config.Overrides)
}
for i, tx := range block.Transactions() {
if i == 0 && chainConfig.Taiko {
if err := tx.MarkAsAnchor(); err != nil {
return nil, err
}
}
// Prepare the transaction for un-traced execution
var (
msg, _ = core.TransactionToMessage(tx, signer, block.BaseFee())

42
ethclient/taiko_api.go Normal file
View file

@ -0,0 +1,42 @@
package ethclient
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/rawdb"
)
// HeadL1Origin returns the latest L2 block's corresponding L1 origin.
func (ec *Client) HeadL1Origin(ctx context.Context) (*rawdb.L1Origin, error) {
var res *rawdb.L1Origin
if err := ec.c.CallContext(ctx, &res, "taiko_headL1Origin"); err != nil {
return nil, err
}
return res, nil
}
// L1OriginByID returns the L2 block's corresponding L1 origin.
func (ec *Client) L1OriginByID(ctx context.Context, blockID *big.Int) (*rawdb.L1Origin, error) {
var res *rawdb.L1Origin
if err := ec.c.CallContext(ctx, &res, "taiko_l1OriginByID", hexutil.EncodeBig(blockID)); err != nil {
return nil, err
}
return res, nil
}
// GetSyncMode returns the current sync mode of the L2 node.
func (ec *Client) GetSyncMode(ctx context.Context) (string, error) {
var res string
if err := ec.c.CallContext(ctx, &res, "taiko_getSyncMode"); err != nil {
return "", err
}
return res, nil
}

124
ethclient/taiko_api_test.go Normal file
View file

@ -0,0 +1,124 @@
package ethclient
import (
"context"
"crypto/rand"
"math/big"
"testing"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/require"
)
func newTaikoAPITestClient(t *testing.T) (*Client, []*types.Block, ethdb.Database) {
// Generate test chain.
blocks := generateTestChain()
// Create node
n, err := node.New(&node.Config{})
require.Nil(t, err)
// Create Ethereum Service
config := &ethconfig.Config{Genesis: genesis}
ethservice, err := eth.New(n, config)
require.Nil(t, err)
n.RegisterAPIs([]rpc.API{
{
Namespace: "taiko",
Version: params.VersionWithMeta,
Service: eth.NewTaikoAPIBackend(ethservice),
Public: true,
},
})
// Start node
require.Nil(t, n.Start())
// Insert test blocks
_, err = ethservice.BlockChain().InsertChain(blocks[1:])
require.Nil(t, err)
return NewClient(n.Attach()), blocks, ethservice.ChainDb()
}
func TestHeadL1Origin(t *testing.T) {
ec, blocks, db := newTaikoAPITestClient(t)
headerHash := blocks[len(blocks)-1].Hash()
l1OriginFound, err := ec.HeadL1Origin(context.Background())
require.Equal(t, ethereum.NotFound.Error(), err.Error())
require.Nil(t, l1OriginFound)
testL1Origin := &rawdb.L1Origin{
BlockID: randomBigInt(),
L2BlockHash: headerHash,
L1BlockHeight: randomBigInt(),
L1BlockHash: randomHash(),
}
rawdb.WriteL1Origin(db, testL1Origin.BlockID, testL1Origin)
rawdb.WriteHeadL1Origin(db, testL1Origin.BlockID)
l1OriginFound, err = ec.HeadL1Origin(context.Background())
require.Nil(t, err)
require.Equal(t, testL1Origin, l1OriginFound)
}
func TestL1OriginByID(t *testing.T) {
ec, blocks, db := newTaikoAPITestClient(t)
headerHash := blocks[len(blocks)-1].Hash()
testL1Origin := &rawdb.L1Origin{
BlockID: randomBigInt(),
L2BlockHash: headerHash,
L1BlockHeight: randomBigInt(),
L1BlockHash: randomHash(),
}
l1OriginFound, err := ec.L1OriginByID(context.Background(), testL1Origin.BlockID)
require.Equal(t, ethereum.NotFound.Error(), err.Error())
require.Nil(t, l1OriginFound)
rawdb.WriteL1Origin(db, testL1Origin.BlockID, testL1Origin)
rawdb.WriteHeadL1Origin(db, testL1Origin.BlockID)
l1OriginFound, err = ec.L1OriginByID(context.Background(), testL1Origin.BlockID)
require.Nil(t, err)
require.Equal(t, testL1Origin, l1OriginFound)
}
// randomHash generates a random blob of data and returns it as a hash.
func randomHash() common.Hash {
var hash common.Hash
if n, err := rand.Read(hash[:]); n != common.HashLength || err != nil {
panic(err)
}
return hash
}
// randomBigInt generates a random big integer.
func randomBigInt() *big.Int {
randomBigInt, err := rand.Int(rand.Reader, common.Big256)
if err != nil {
log.Crit(err.Error())
}
return randomBigInt
}

2
go.mod
View file

@ -72,6 +72,7 @@ require (
golang.org/x/tools v0.15.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/mathutil v1.6.0
)
require (
@ -132,6 +133,7 @@ require (
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect

4
go.sum
View file

@ -531,6 +531,8 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7 h1:cZC+usqsYgHtlBaGulVnZ1hfKAi8iWtujBnRLQE698c=
github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7/go.mod h1:IToEjHuttnUzwZI5KBSM/LOOW3qLbbrHOEfp3SbECGY=
github.com/prysmaticlabs/gohashtree v0.0.1-alpha.0.20220714111606-acbb2962fb48 h1:cSo6/vk8YpvkLbk9v3FO97cakNmUoxwi2KMP8hd5WIw=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@ -973,6 +975,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View file

@ -76,6 +76,7 @@ type Payload struct {
stop chan struct{}
lock sync.Mutex
cond *sync.Cond
done chan struct{} // CHANGE(taiko): done channel to communicate we shouldnt write to `stop` channel.
}
// newPayload initializes the payload object.
@ -84,6 +85,7 @@ func newPayload(empty *types.Block, id engine.PayloadID) *Payload {
id: id,
empty: empty,
stop: make(chan struct{}),
done: make(chan struct{}, 1), // CHANGE(taiko): buffered channel to communicate done to taiko payload builder
}
log.Info("Starting work on payload", "id", payload.id)
payload.cond = sync.NewCond(&payload.lock)
@ -133,6 +135,7 @@ func (payload *Payload) Resolve() *engine.ExecutionPayloadEnvelope {
select {
case <-payload.stop:
default:
payload.done <- struct{}{} // CHANGE(taiko): signal to taiko payload builder to not write to `payload.stop` channel
close(payload.stop)
}
if payload.full != nil {
@ -171,6 +174,7 @@ func (payload *Payload) ResolveFull() *engine.ExecutionPayloadEnvelope {
select {
case <-payload.stop:
default:
payload.done <- struct{}{} // CHANGE(taiko): signal to taiko payload builder to not write to `payload.stop` channel
close(payload.stop)
}
return engine.BlockToExecutableData(payload.full, payload.fullFees, payload.sidecars)
@ -226,6 +230,10 @@ func (w *worker) buildPayload(args *BuildPayloadArgs) (*Payload, error) {
for {
select {
case <-timer.C:
// CHANGE(taiko): do not update payload.
if w.chainConfig.Taiko {
continue
}
start := time.Now()
r := w.getSealingBlock(fullParams)
if r.err == nil {

47
miner/taiko_miner.go Normal file
View file

@ -0,0 +1,47 @@
package miner
import (
"math/big"
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
// PreBuiltTxList is a pre-built transaction list based on the latest chain state,
// with estimated gas used / bytes.
type PreBuiltTxList struct {
TxList types.Transactions
EstimatedGasUsed uint64
BytesLength uint64
}
// SealBlockWith mines and seals a block without changing the canonical chain.
func (miner *Miner) SealBlockWith(
parent common.Hash,
timestamp uint64,
blkMeta *engine.BlockMetadata,
baseFeePerGas *big.Int,
withdrawals types.Withdrawals,
) (*types.Block, error) {
return miner.worker.sealBlockWith(parent, timestamp, blkMeta, baseFeePerGas, withdrawals)
}
// BuildTransactionsLists builds multiple transactions lists which satisfy all the given limits.
func (miner *Miner) BuildTransactionsLists(
beneficiary common.Address,
baseFee *big.Int,
blockMaxGasLimit uint64,
maxBytesPerTxList uint64,
locals []string,
maxTransactionsLists uint64,
) ([]*PreBuiltTxList, error) {
return miner.worker.BuildTransactionsLists(
beneficiary,
baseFee,
blockMaxGasLimit,
maxBytesPerTxList,
locals,
maxTransactionsLists,
)
}

View file

@ -0,0 +1,53 @@
package miner
import (
"math/big"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)
// SetFullBlock updates the full-block to the given block.
func (payload *Payload) SetFullBlock(block *types.Block, fees *big.Int) {
payload.lock.Lock()
defer payload.lock.Unlock()
go payload.afterSetFullBlock()
payload.full = block
payload.fullFees = fees
feesInEther := new(big.Float).Quo(new(big.Float).SetInt(fees), big.NewFloat(params.Ether))
log.Info("Updated payload", "id", payload.id, "number", block.NumberU64(), "hash", block.Hash(),
"txs", len(block.Transactions()), "gas", block.GasUsed(), "fees", feesInEther,
"root", block.Root())
payload.cond.Broadcast() // fire signal for notifying full block
}
func (payload *Payload) afterSetFullBlock() {
payload.lock.Lock()
defer payload.lock.Unlock()
select {
case <-payload.done:
log.Info("SetFullBlock payload.done received", "id", payload.id)
return
default:
}
select {
case _, ok := <-payload.stop:
if ok {
log.Info("SetFullBlock payload.stop received", "id", payload.id)
return
}
default:
}
select {
case payload.stop <- struct{}{}:
default:
}
}

View file

@ -0,0 +1,140 @@
package miner
import (
"math/big"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie"
"github.com/stretchr/testify/assert"
)
func newTestBlock() *types.Block {
tx1 := types.NewTransaction(1, common.BytesToAddress([]byte{0x11}), big.NewInt(111), 1111, big.NewInt(11111), []byte{0x11, 0x11, 0x11})
txs := []*types.Transaction{tx1}
block := types.NewBlock(&types.Header{Number: big.NewInt(314)}, txs, nil, nil, trie.NewStackTrie(nil))
return block
}
func TestSetFullBlock_AvoidPanic(t *testing.T) {
var (
db = rawdb.NewMemoryDatabase()
recipient = common.HexToAddress("0xdeadbeef")
)
w, b := newTestWorker(t, params.TestChainConfig, ethash.NewFaker(), db, 0)
defer w.close()
timestamp := uint64(time.Now().Unix())
args := &BuildPayloadArgs{
Parent: b.chain.CurrentBlock().Hash(),
Timestamp: timestamp,
Random: common.Hash{},
FeeRecipient: recipient,
}
payload, err := w.buildPayload(args)
if err != nil {
t.Fatalf("Failed to build payload %v", err)
}
fees := big.NewInt(1)
payload.done <- struct{}{}
close(payload.stop)
block := newTestBlock()
// expect not to panic sending to payload.stop
// now that done is closed
payload.SetFullBlock(block, fees)
}
func TestAfterSetFullBlock_Panic_DoneChannelNotSent(t *testing.T) {
var (
db = rawdb.NewMemoryDatabase()
recipient = common.HexToAddress("0xdeadbeef")
)
w, b := newTestWorker(t, params.TestChainConfig, ethash.NewFaker(), db, 0)
defer w.close()
timestamp := uint64(time.Now().Unix())
args := &BuildPayloadArgs{
Parent: b.chain.CurrentBlock().Hash(),
Timestamp: timestamp,
Random: common.Hash{},
FeeRecipient: recipient,
}
payload, err := w.buildPayload(args)
if err != nil {
t.Fatalf("Failed to build payload %v", err)
}
// dont send on done channel, but close stop channel.
// should panic when sent on.
close(payload.stop)
assert.Panics(t, func() {
payload.afterSetFullBlock()
})
}
func TestAfterSetFullBlock_AvoidPanic_DoneChannelSent(t *testing.T) {
var (
db = rawdb.NewMemoryDatabase()
recipient = common.HexToAddress("0xdeadbeef")
)
w, b := newTestWorker(t, params.TestChainConfig, ethash.NewFaker(), db, 0)
defer w.close()
timestamp := uint64(time.Now().Unix())
args := &BuildPayloadArgs{
Parent: b.chain.CurrentBlock().Hash(),
Timestamp: timestamp,
Random: common.Hash{},
FeeRecipient: recipient,
}
payload, err := w.buildPayload(args)
if err != nil {
t.Fatalf("Failed to build payload %v", err)
}
payload.done <- struct{}{}
close(payload.stop)
assert.NotPanics(t, func() {
payload.afterSetFullBlock()
})
}
func TestSetFullBlock(t *testing.T) {
var (
db = rawdb.NewMemoryDatabase()
recipient = common.HexToAddress("0xdeadbeef")
)
w, b := newTestWorker(t, params.TestChainConfig, ethash.NewFaker(), db, 0)
defer w.close()
timestamp := uint64(time.Now().Unix())
args := &BuildPayloadArgs{
Parent: b.chain.CurrentBlock().Hash(),
Timestamp: timestamp,
Random: common.Hash{},
FeeRecipient: recipient,
}
payload, err := w.buildPayload(args)
if err != nil {
t.Fatalf("Failed to build payload %v", err)
}
fees := big.NewInt(1)
block := newTestBlock()
payload.SetFullBlock(block, fees)
assert.Equal(t, block, payload.full)
assert.Equal(t, fees, payload.fullFees)
}

335
miner/taiko_worker.go Normal file
View file

@ -0,0 +1,335 @@
package miner
import (
"bytes"
"compress/zlib"
"errors"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/holiman/uint256"
)
// BuildTransactionsLists builds multiple transactions lists which satisfy all the given conditions
// 1. All transactions should all be able to pay the given base fee.
// 2. The total gas used should not exceed the given blockMaxGasLimit
// 3. The total bytes used should not exceed the given maxBytesPerTxList
// 4. The total number of transactions lists should not exceed the given maxTransactionsLists
func (w *worker) BuildTransactionsLists(
beneficiary common.Address,
baseFee *big.Int,
blockMaxGasLimit uint64,
maxBytesPerTxList uint64,
localAccounts []string,
maxTransactionsLists uint64,
) ([]*PreBuiltTxList, error) {
var (
txsLists []*PreBuiltTxList
currentHead = w.chain.CurrentBlock()
)
if currentHead == nil {
return nil, fmt.Errorf("failed to find current head")
}
// Check if tx pool is empty at first.
if len(w.eth.TxPool().Pending(txpool.PendingFilter{BaseFee: uint256.MustFromBig(baseFee), OnlyPlainTxs: true})) == 0 {
return txsLists, nil
}
params := &generateParams{
timestamp: uint64(time.Now().Unix()),
forceTime: true,
parentHash: currentHead.Hash(),
coinbase: beneficiary,
random: currentHead.MixDigest,
noTxs: false,
baseFeePerGas: baseFee,
}
env, err := w.prepareWork(params)
if err != nil {
return nil, err
}
defer env.discard()
var (
signer = types.MakeSigner(w.chainConfig, new(big.Int).Add(currentHead.Number, common.Big1), currentHead.Time)
// Split the pending transactions into locals and remotes, then
// fill the block with all available pending transactions.
localTxs, remoteTxs = w.getPendingTxs(localAccounts, baseFee)
)
commitTxs := func() (*PreBuiltTxList, error) {
env.tcount = 0
env.txs = []*types.Transaction{}
env.gasPool = new(core.GasPool).AddGas(blockMaxGasLimit)
env.header.GasLimit = blockMaxGasLimit
var (
locals = make(map[common.Address][]*txpool.LazyTransaction)
remotes = make(map[common.Address][]*txpool.LazyTransaction)
)
for address, txs := range localTxs {
locals[address] = txs
}
for address, txs := range remoteTxs {
remotes[address] = txs
}
w.commitL2Transactions(
env,
newTransactionsByPriceAndNonce(signer, locals, baseFee),
newTransactionsByPriceAndNonce(signer, remotes, baseFee),
maxBytesPerTxList,
)
b, err := encodeAndComporeessTxList(env.txs)
if err != nil {
return nil, err
}
return &PreBuiltTxList{
TxList: env.txs,
EstimatedGasUsed: env.header.GasLimit - env.gasPool.Gas(),
BytesLength: uint64(len(b)),
}, nil
}
for i := 0; i < int(maxTransactionsLists); i++ {
res, err := commitTxs()
if err != nil {
return nil, err
}
if len(res.TxList) == 0 {
break
}
txsLists = append(txsLists, res)
}
return txsLists, nil
}
// sealBlockWith mines and seals a block with the given block metadata.
func (w *worker) sealBlockWith(
parent common.Hash,
timestamp uint64,
blkMeta *engine.BlockMetadata,
baseFeePerGas *big.Int,
withdrawals types.Withdrawals,
) (*types.Block, error) {
// Decode transactions bytes.
var txs types.Transactions
if err := rlp.DecodeBytes(blkMeta.TxList, &txs); err != nil {
return nil, fmt.Errorf("failed to decode txList: %w", err)
}
if len(txs) == 0 {
// A L2 block needs to have have at least one `V1TaikoL2.anchor` or
// `V1TaikoL2.invalidateBlock` transaction.
return nil, fmt.Errorf("too less transactions in the block")
}
params := &generateParams{
timestamp: timestamp,
forceTime: true,
parentHash: parent,
coinbase: blkMeta.Beneficiary,
random: blkMeta.MixHash,
withdrawals: withdrawals,
noTxs: false,
baseFeePerGas: baseFeePerGas,
}
// Set extraData
w.extra = blkMeta.ExtraData
env, err := w.prepareWork(params)
if err != nil {
return nil, err
}
defer env.discard()
env.header.GasLimit = blkMeta.GasLimit
// Commit transactions.
gasLimit := env.header.GasLimit
rules := w.chain.Config().Rules(env.header.Number, true, timestamp)
env.gasPool = new(core.GasPool).AddGas(gasLimit)
for i, tx := range txs {
if i == 0 {
if err := tx.MarkAsAnchor(); err != nil {
return nil, err
}
}
sender, err := types.LatestSignerForChainID(tx.ChainId()).Sender(tx)
if err != nil {
log.Info("Skip an invalid proposed transaction", "hash", tx.Hash(), "reason", err)
continue
}
env.state.Prepare(rules, sender, blkMeta.Beneficiary, tx.To(), vm.ActivePrecompiles(rules), tx.AccessList())
env.state.SetTxContext(tx.Hash(), env.tcount)
if _, err := w.commitTransaction(env, tx); err != nil {
log.Info("Skip an invalid proposed transaction", "hash", tx.Hash(), "reason", err)
continue
}
env.tcount++
}
block, err := w.engine.FinalizeAndAssemble(w.chain, env.header, env.state, env.txs, nil, env.receipts, withdrawals)
if err != nil {
return nil, err
}
results := make(chan *types.Block, 1)
if err := w.engine.Seal(w.chain, block, results, nil); err != nil {
return nil, err
}
block = <-results
return block, nil
}
// getPendingTxs fetches the pending transactions from tx pool.
func (w *worker) getPendingTxs(localAccounts []string, baseFee *big.Int) (
map[common.Address][]*txpool.LazyTransaction,
map[common.Address][]*txpool.LazyTransaction,
) {
pending := w.eth.TxPool().Pending(txpool.PendingFilter{OnlyPlainTxs: true, BaseFee: uint256.MustFromBig(baseFee)})
localTxs, remoteTxs := make(map[common.Address][]*txpool.LazyTransaction), pending
for _, local := range localAccounts {
account := common.HexToAddress(local)
if txs := remoteTxs[account]; len(txs) > 0 {
delete(remoteTxs, account)
localTxs[account] = txs
}
}
return localTxs, remoteTxs
}
// commitL2Transactions tries to commit the transactions into the given state.
func (w *worker) commitL2Transactions(
env *environment,
txsLocal *transactionsByPriceAndNonce,
txsRemote *transactionsByPriceAndNonce,
maxBytesPerTxList uint64,
) {
var (
txs = txsLocal
isLocal = true
)
for {
// If we don't have enough gas for any further transactions then we're done.
if env.gasPool.Gas() < params.TxGas {
log.Trace("Not enough gas for further transactions", "have", env.gasPool, "want", params.TxGas)
break
}
// Retrieve the next transaction and abort if all done.
ltx, _ := txs.Peek()
if ltx == nil {
if isLocal {
txs = txsRemote
isLocal = false
continue
}
break
}
tx := ltx.Resolve()
if tx == nil {
log.Trace("Ignoring evicted transaction")
txs.Pop()
continue
}
// Error may be ignored here. The error has already been checked
// during transaction acceptance is the transaction pool.
from, _ := types.Sender(env.signer, tx)
b, err := encodeAndComporeessTxList(append(env.txs, tx))
if err != nil {
log.Trace("Failed to rlp encode and compress the pending transaction %s: %w", tx.Hash(), err)
txs.Pop()
continue
}
if len(b) >= int(maxBytesPerTxList) {
break
}
// Check whether the tx is replay protected. If we're not in the EIP155 hf
// phase, start ignoring the sender until we do.
if tx.Protected() && !w.chainConfig.IsEIP155(env.header.Number) {
log.Trace("Ignoring reply protected transaction", "hash", tx.Hash(), "eip155", w.chainConfig.EIP155Block)
txs.Pop()
continue
}
// Start executing the transaction
env.state.SetTxContext(tx.Hash(), env.tcount)
_, err = w.commitTransaction(env, tx)
switch {
case errors.Is(err, core.ErrNonceTooLow):
// New head notification data race between the transaction pool and miner, shift
log.Trace("Skipping transaction with low nonce", "hash", ltx.Hash, "sender", from, "nonce", tx.Nonce())
txs.Shift()
case errors.Is(err, nil):
// Everything ok, collect the logs and shift in the next transaction from the same account
env.tcount++
txs.Shift()
default:
// Transaction is regarded as invalid, drop all consecutive transactions from
// the same sender because of `nonce-too-high` clause.
log.Trace("Transaction failed, account skipped", "hash", ltx.Hash, "err", err)
txs.Pop()
}
}
}
// encodeAndComporeessTxList encodes and compresses the given transactions list.
func encodeAndComporeessTxList(txs types.Transactions) ([]byte, error) {
b, err := rlp.EncodeToBytes(txs)
if err != nil {
return nil, err
}
return compress(b)
}
// compress compresses the given txList bytes using zlib.
func compress(txListBytes []byte) ([]byte, error) {
var b bytes.Buffer
w := zlib.NewWriter(&b)
defer w.Close()
if _, err := w.Write(txListBytes); err != nil {
return nil, err
}
if err := w.Flush(); err != nil {
return nil, err
}
return b.Bytes(), nil
}

View file

@ -925,14 +925,15 @@ func (w *worker) commitTransactions(env *environment, plainTxs, blobTxs *transac
// generateParams wraps various of settings for generating sealing task.
type generateParams struct {
timestamp uint64 // The timestamp for sealing task
forceTime bool // Flag whether the given timestamp is immutable or not
parentHash common.Hash // Parent block hash, empty means the latest chain head
coinbase common.Address // The fee recipient address for including transaction
random common.Hash // The randomness generated by beacon chain, empty before the merge
withdrawals types.Withdrawals // List of withdrawals to include in block.
beaconRoot *common.Hash // The beacon root (cancun field).
noTxs bool // Flag whether an empty block without any transaction is expected
timestamp uint64 // The timestamp for sealing task
forceTime bool // Flag whether the given timestamp is immutable or not
parentHash common.Hash // Parent block hash, empty means the latest chain head
coinbase common.Address // The fee recipient address for including transaction
random common.Hash // The randomness generated by beacon chain, empty before the merge
withdrawals types.Withdrawals // List of withdrawals to include in block.
beaconRoot *common.Hash // The beacon root (cancun field).
noTxs bool // Flag whether an empty block without any transaction is expected
baseFeePerGas *big.Int // CHANGE(taiko): The base fee per gas for the next block
}
// prepareWork constructs the sealing task according to the given parameters,
@ -955,10 +956,17 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) {
// to parent+1 if the mutation is allowed.
timestamp := genParams.timestamp
if parent.Time >= timestamp {
if genParams.forceTime {
return nil, fmt.Errorf("invalid timestamp, parent %d given %d", parent.Time, timestamp)
// CHANGE(taiko): block.timestamp == parent.timestamp is allowed in Taiko protocol.
if !w.chainConfig.Taiko {
if genParams.forceTime {
return nil, fmt.Errorf("invalid timestamp, parent %d given %d", parent.Time, timestamp)
}
timestamp = parent.Time + 1
} else {
if parent.Time > timestamp {
return nil, fmt.Errorf("invalid timestamp, parent %d given %d", parent.Time, timestamp)
}
}
timestamp = parent.Time + 1
}
// Construct the sealing block header.
header := &types.Header{
@ -978,10 +986,14 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) {
}
// Set baseFee and GasLimit if we are on an EIP-1559 chain
if w.chainConfig.IsLondon(header.Number) {
header.BaseFee = eip1559.CalcBaseFee(w.chainConfig, parent)
if !w.chainConfig.IsLondon(parent.Number) {
parentGasLimit := parent.GasLimit * w.chainConfig.ElasticityMultiplier()
header.GasLimit = core.CalcGasLimit(parentGasLimit, w.config.GasCeil)
if w.chainConfig.Taiko && genParams.baseFeePerGas != nil {
header.BaseFee = genParams.baseFeePerGas
} else {
header.BaseFee = eip1559.CalcBaseFee(w.chainConfig, parent)
if !w.chainConfig.IsLondon(parent.Number) {
parentGasLimit := parent.GasLimit * w.chainConfig.ElasticityMultiplier()
header.GasLimit = core.CalcGasLimit(parentGasLimit, w.config.GasCeil)
}
}
}
// Apply EIP-4844, EIP-4788.

View file

@ -49,7 +49,8 @@ var (
DefaultAuthVhosts = []string{"localhost"} // Default virtual hosts for the authenticated apis
DefaultAuthOrigins = []string{"localhost"} // Default origins for the authenticated apis
DefaultAuthPrefix = "" // Default prefix for the authenticated apis
DefaultAuthModules = []string{"eth", "engine"}
// CHANGE(taiko): add `taikoAuth` namespace for authenticated APIs.
DefaultAuthModules = []string{"eth", "engine", rpc.TaikoAuth}
)
// DefaultConfig contains reasonable default settings.

View file

@ -62,7 +62,8 @@ func checkModuleAvailability(modules []string, apis []rpc.API) (bad, available [
}
for _, name := range modules {
if _, ok := availableSet[name]; !ok {
if name != rpc.MetadataApi && name != rpc.EngineApi {
// CHANGE(taiko): add `taikoAuth` namespace for authenticated APIs.
if name != rpc.MetadataApi && name != rpc.EngineApi && name != rpc.TaikoAuth {
bad = append(bad, name)
}
}

View file

@ -315,6 +315,17 @@ var NetworkNames = map[string]string{
GoerliChainConfig.ChainID.String(): "goerli",
SepoliaChainConfig.ChainID.String(): "sepolia",
HoleskyChainConfig.ChainID.String(): "holesky",
// CHANGE(taiko): add Taiko network name.
TaikoMainnetNetworkID.String(): "Taiko",
TaikoInternalL2ANetworkID.String(): "Taiko Internal L2A Devnet",
TaikoInternalL2BNetworkID.String(): "Taiko Internal L2B Devnet",
SnaefellsjokullNetworkID.String(): "Taiko Alpha-1 (Snæfellsjökull)",
AskjaNetworkID.String(): "Taiko Alpha-2 (Askja)",
GrimsvotnNetworkID.String(): "Taiko Alpha-3 L2 (Grimsvotn)",
EldfellNetworkID.String(): "Taiko Alpha-4 L3 (Eldfell)",
JolnirNetworkID.String(): "Taiko Alpha-5 L2 (Jolnir)",
KatlaNetworkID.String(): "Taiko Alpha-6 L2 (Katla)",
HeklaNetworkID.String(): "Taiko Alpha-7 L2 (Hekla)",
}
// ChainConfig is the core config which determines the blockchain settings.
@ -365,6 +376,9 @@ type ChainConfig struct {
// Various consensus engines
Ethash *EthashConfig `json:"ethash,omitempty"`
Clique *CliqueConfig `json:"clique,omitempty"`
// CHANGE(taiko): Taiko network flag.
Taiko bool `json:"taiko"`
}
// EthashConfig is the consensus engine configs for proof-of-work based sealing.
@ -397,6 +411,9 @@ func (c *ChainConfig) Description() string {
}
banner += fmt.Sprintf("Chain ID: %v (%s)\n", c.ChainID, network)
switch {
// CHANGE(taiko): print Taiko consensus engine in banner.
case c.Taiko:
banner += "Consensus: Taiko\n"
case c.Ethash != nil:
if c.TerminalTotalDifficulty == nil {
banner += "Consensus: Ethash (proof-of-work)\n"

View file

@ -173,6 +173,8 @@ const (
BlobTxTargetBlobGasPerBlock = 3 * BlobTxBlobGasPerBlob // Target consumable blob gas for data blobs per block (for 1559-like pricing)
MaxBlobGasPerBlock = 6 * BlobTxBlobGasPerBlob // Maximum consumable blob gas for data blobs per block
BlobTxHashVersion = 0x01 // CHANGE(taiko): Version byte of the commitment hash
)
// Gas discount table for BLS12-381 G1 and G2 multi exponentiation operations

68
params/taiko_config.go Normal file
View file

@ -0,0 +1,68 @@
package params
import (
"math/big"
"github.com/ethereum/go-ethereum/common"
)
func u64(val uint64) *uint64 { return &val }
// Network IDs
var (
TaikoMainnetNetworkID = big.NewInt(167000)
TaikoInternalL2ANetworkID = big.NewInt(167001)
TaikoInternalL2BNetworkID = big.NewInt(167002)
SnaefellsjokullNetworkID = big.NewInt(167003)
AskjaNetworkID = big.NewInt(167004)
GrimsvotnNetworkID = big.NewInt(167005)
EldfellNetworkID = big.NewInt(167006)
JolnirNetworkID = big.NewInt(167007)
KatlaNetworkID = big.NewInt(167008)
HeklaNetworkID = big.NewInt(167009)
)
var networkIDToChainConfig = map[*big.Int]*ChainConfig{
TaikoMainnetNetworkID: TaikoChainConfig,
TaikoInternalL2ANetworkID: TaikoChainConfig,
TaikoInternalL2BNetworkID: TaikoChainConfig,
SnaefellsjokullNetworkID: TaikoChainConfig,
AskjaNetworkID: TaikoChainConfig,
GrimsvotnNetworkID: TaikoChainConfig,
EldfellNetworkID: TaikoChainConfig,
JolnirNetworkID: TaikoChainConfig,
KatlaNetworkID: TaikoChainConfig,
HeklaNetworkID: TaikoChainConfig,
MainnetChainConfig.ChainID: MainnetChainConfig,
SepoliaChainConfig.ChainID: SepoliaChainConfig,
GoerliChainConfig.ChainID: GoerliChainConfig,
TestChainConfig.ChainID: TestChainConfig,
NonActivatedConfig.ChainID: NonActivatedConfig,
}
func NetworkIDToChainConfigOrDefault(networkID *big.Int) *ChainConfig {
if config, ok := networkIDToChainConfig[networkID]; ok {
return config
}
return AllEthashProtocolChanges
}
var TaikoChainConfig = &ChainConfig{
ChainID: TaikoInternalL2ANetworkID, // Use Internal Devnet network ID by default.
HomesteadBlock: common.Big0,
EIP150Block: common.Big0,
EIP155Block: common.Big0,
EIP158Block: common.Big0,
ByzantiumBlock: common.Big0,
ConstantinopleBlock: common.Big0,
PetersburgBlock: common.Big0,
IstanbulBlock: common.Big0,
BerlinBlock: common.Big0,
LondonBlock: common.Big0,
ShanghaiTime: u64(0),
MergeNetsplitBlock: nil,
TerminalTotalDifficulty: common.Big0,
TerminalTotalDifficultyPassed: true,
Taiko: true,
}

View file

@ -0,0 +1,93 @@
package params
import (
"math/big"
"testing"
)
func TestNetworkIDToChainConfigOrDefault(t *testing.T) {
tests := []struct {
name string
networkID *big.Int
wantChainConfig *ChainConfig
}{
{
"taikoMainnetNetworkID",
TaikoMainnetNetworkID,
TaikoChainConfig,
},
{
"taikoInternalL2ANetworkId",
TaikoInternalL2ANetworkID,
TaikoChainConfig,
},
{
"taikoInternalL2BNetworkId",
TaikoInternalL2BNetworkID,
TaikoChainConfig,
},
{
"snaefoll",
SnaefellsjokullNetworkID,
TaikoChainConfig,
},
{
"askja",
AskjaNetworkID,
TaikoChainConfig,
},
{
"grimsvotn",
GrimsvotnNetworkID,
TaikoChainConfig,
},
{
"eldfellNetworkID",
EldfellNetworkID,
TaikoChainConfig,
},
{
"jolnirNetworkID",
JolnirNetworkID,
TaikoChainConfig,
},
{
"katlaNetworkID",
KatlaNetworkID,
TaikoChainConfig,
},
{
"heklaNetworkID",
HeklaNetworkID,
TaikoChainConfig,
},
{
"mainnet",
MainnetChainConfig.ChainID,
MainnetChainConfig,
},
{
"sepolia",
SepoliaChainConfig.ChainID,
SepoliaChainConfig,
},
{
"goerli",
GoerliChainConfig.ChainID,
GoerliChainConfig,
},
{
"doesntExist",
big.NewInt(89390218390),
AllEthashProtocolChanges,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if config := NetworkIDToChainConfigOrDefault(tt.networkID); config != tt.wantChainConfig {
t.Fatalf("expected %v, got %v", config, tt.wantChainConfig)
}
})
}
}

View file

@ -28,6 +28,9 @@ import (
const MetadataApi = "rpc"
const EngineApi = "engine"
// CHANGE(taiko): add `taikoAuth` namespace for authenticated APIs
const TaikoAuth = "taikoAuth"
// CodecOption specifies which type of messages a codec supports.
//
// Deprecated: this option is no longer honored by Server.