feat(beacon): introduce soft blocks (#342)

* feat(beacon): introduce soft blocks

* feat: update api.go

* chore(ci): update CI

* feat: update L1Origin

* feat: update `verifyHeader`

* test: update tests

* feat: update consensus

* feat: update consensus

* feat: update genesis

* feat: remove timestamp check in prepareWork

* feat: merge changes in #281

* Update eth/catalyst/api.go

Co-authored-by: maskpp <maskpp266@gmail.com>

* Update internal/ethapi/taiko_preconf.go

Co-authored-by: maskpp <maskpp266@gmail.com>

* fix consensus test

* revert commit f1df58

* fix consensus test (#349)

* Update eth/catalyst/api.go

Co-authored-by: maskpp <maskpp266@gmail.com>

* feat: add back timestamp check in worker

* add genesis

* temp fix for old l1origin

* nil value

* feat: rename to `L1OriginLegacy`

* feat: change `common.Big0` as the default value for legacy l1Origin, to make `IsSoftblock` return `false`

* feat(beacon): change the reorg log level (#350)

* use debug log level to avoid logging too many logs when frequently soft block reorg.

* use debug log level to avoid logging too many logs when frequently soft block reorg.

* feat: check --taiko flag

---------

Co-authored-by: David <david@taiko.xyz>

* add rlp optional flag (#353)

* fix lint

* fix test case

* feat(l1Origin): remove the reverted l1Origins (#355)

* remove the reverted l1Origins

* feat: add more comments

---------

Co-authored-by: David <david@taiko.xyz>

* only forward txs

* chore: update ci

---------

Co-authored-by: maskpp <maskpp266@gmail.com>
Co-authored-by: Jeffery Walsh <cyberhorsey@gmail.com>
This commit is contained in:
David 2025-01-07 16:25:44 +08:00 committed by GitHub
parent 5ef74219ab
commit a2cbf904ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 566 additions and 83 deletions

View file

@ -2,7 +2,7 @@ name: "Push multi-arch docker image to GAR"
on: on:
push: push:
branches: [ taiko ] branches: [taiko]
tags: tags:
- "v*" - "v*"

View file

@ -192,6 +192,11 @@ func makeFullNode(ctx *cli.Context) *node.Node {
cfg.Eth.OverrideVerkle = &v cfg.Eth.OverrideVerkle = &v
} }
// CHANGE(taiko): set preconfirmation forwarding URL.
if ctx.IsSet(utils.PreconfirmationForwardingURLFlag.Name) {
cfg.Eth.PreconfirmationForwardingURL = ctx.String(utils.PreconfirmationForwardingURLFlag.Name)
}
backend, eth := utils.RegisterEthService(stack, &cfg.Eth) backend, eth := utils.RegisterEthService(stack, &cfg.Eth)
// CHANGE(TAIKO): register Taiko RPC APIs. // CHANGE(TAIKO): register Taiko RPC APIs.

View file

@ -261,7 +261,7 @@ func init() {
metricsFlags, metricsFlags,
) )
// CHANGE(taiko): append Taiko flags into the original GETH flags // CHANGE(taiko): append Taiko flags into the original GETH flags
app.Flags = append(app.Flags, &utils.TaikoFlag) app.Flags = append(app.Flags, utils.TaikoFlag, utils.PreconfirmationForwardingURLFlag)
flags.AutoEnvVars(app.Flags, "GETH") flags.AutoEnvVars(app.Flags, "GETH")

View file

@ -5,6 +5,7 @@ import (
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
@ -12,9 +13,15 @@ import (
) )
var ( var (
TaikoFlag = cli.BoolFlag{ TaikoFlag = &cli.BoolFlag{
Name: "taiko", Name: "taiko",
Usage: "Taiko network", Usage: "Taiko network",
Category: flags.TaikoCategory,
}
PreconfirmationForwardingURLFlag = &cli.StringFlag{
Name: "taiko.preconfirmationForwardingUrl",
Usage: "URL to forward RPC requests before confirmation",
Category: flags.TaikoCategory,
} }
) )

View file

@ -10,10 +10,12 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
@ -40,12 +42,13 @@ var (
// Taiko is a consensus engine used by L2 rollup. // Taiko is a consensus engine used by L2 rollup.
type Taiko struct { type Taiko struct {
chainConfig *params.ChainConfig chainConfig *params.ChainConfig
chainDB ethdb.Database
taikoL2Address common.Address taikoL2Address common.Address
} }
var _ = new(Taiko) var _ = new(Taiko)
func New(chainConfig *params.ChainConfig) *Taiko { func New(chainConfig *params.ChainConfig, chainDB ethdb.Database) *Taiko {
taikoL2AddressPrefix := strings.TrimPrefix(chainConfig.ChainID.String(), "0") taikoL2AddressPrefix := strings.TrimPrefix(chainConfig.ChainID.String(), "0")
return &Taiko{ return &Taiko{
@ -56,6 +59,7 @@ func New(chainConfig *params.ChainConfig) *Taiko {
strings.Repeat("0", common.AddressLength*2-len(taikoL2AddressPrefix)-len(TaikoL2AddressSuffix)) + strings.Repeat("0", common.AddressLength*2-len(taikoL2AddressPrefix)-len(TaikoL2AddressSuffix)) +
TaikoL2AddressSuffix, TaikoL2AddressSuffix,
), ),
chainDB: chainDB,
} }
} }
@ -82,7 +86,7 @@ func (t *Taiko) VerifyHeader(chain consensus.ChainHeaderReader, header *types.He
return consensus.ErrUnknownAncestor return consensus.ErrUnknownAncestor
} }
// Sanity checks passed, do a proper verification // Sanity checks passed, do a proper verification
return t.verifyHeader(chain, header, parent, time.Now().Unix()) return t.verifyHeader(header, parent, time.Now().Unix())
} }
// VerifyHeaders is similar to VerifyHeader, but verifies a batch of headers // VerifyHeaders is similar to VerifyHeader, but verifies a batch of headers
@ -109,7 +113,7 @@ func (t *Taiko) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*type
if parent == nil { if parent == nil {
err = consensus.ErrUnknownAncestor err = consensus.ErrUnknownAncestor
} else { } else {
err = t.verifyHeader(chain, header, parent, unixNow) err = t.verifyHeader(header, parent, unixNow)
} }
select { select {
case <-abort: case <-abort:
@ -121,11 +125,7 @@ func (t *Taiko) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*type
return abort, results return abort, results
} }
func (t *Taiko) verifyHeader(chain consensus.ChainHeaderReader, header, parent *types.Header, unixNow int64) error { func (t *Taiko) verifyHeader(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) // Ensure that the header's extra-data section is of a reasonable size (<= 32 bytes)
if uint64(len(header.Extra)) > params.MaximumExtraDataSize { if uint64(len(header.Extra)) > params.MaximumExtraDataSize {
return fmt.Errorf("extra-data too long: %d > %d", len(header.Extra), params.MaximumExtraDataSize) return fmt.Errorf("extra-data too long: %d > %d", len(header.Extra), params.MaximumExtraDataSize)
@ -171,6 +171,16 @@ func (t *Taiko) verifyHeader(chain consensus.ChainHeaderReader, header, parent *
return ErrEmptyWithdrawalsHash return ErrEmptyWithdrawalsHash
} }
l1Origin, err := rawdb.ReadL1Origin(t.chainDB, header.Number)
if err != nil {
return err
}
// If the current block is not a soft block, then check the timestamp.
if l1Origin != nil && !l1Origin.IsSoftBlock() && header.Time > uint64(unixNow) {
return consensus.ErrFutureBlock
}
return nil return nil
} }

View file

@ -39,7 +39,7 @@ func init() {
config.ArrowGlacierBlock = nil config.ArrowGlacierBlock = nil
config.Ethash = nil config.Ethash = nil
config.Taiko = true config.Taiko = true
testEngine = taiko.New(config) testEngine = taiko.New(config, rawdb.NewMemoryDatabase())
taikoL2AddressPrefix := strings.TrimPrefix(config.ChainID.String(), "0") taikoL2AddressPrefix := strings.TrimPrefix(config.ChainID.String(), "0")
@ -93,44 +93,7 @@ func init() {
} }
} }
func newTestBackend(t *testing.T) (*eth.Ethereum, []*types.Block) { func generateTestChain(t *testing.T) ([]*types.Block, *eth.Ethereum) {
// 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) { generate := func(i int, g *core.BlockGen) {
g.OffsetTime(5) g.OffsetTime(5)
@ -147,16 +110,56 @@ func generateTestChain() []*types.Block {
} }
} }
// Create node
n, err := node.New(&node.Config{
DataDir: t.TempDir(),
})
if err != nil {
t.Fatalf("can't create new node: %v", err)
}
// Create Ethereum Service
ethService, err := eth.New(n, &ethconfig.Config{
Genesis: genesis,
})
if err != nil {
t.Fatalf("can't create new ethereum service: %v", err)
}
db := ethService.ChainDb()
gblock := genesis.MustCommit(db, triedb.NewDatabase(db, triedb.HashDefaults)) gblock := genesis.MustCommit(db, triedb.NewDatabase(db, triedb.HashDefaults))
blocks, _ := core.GenerateChain(genesis.Config, gblock, testEngine, db, 1, generate) blocks, _ := core.GenerateChain(genesis.Config, gblock, testEngine, db, 1, generate)
blocks = append([]*types.Block{gblock}, blocks...) blocks = append([]*types.Block{gblock}, blocks...)
return blocks
// Insert L1Origins.
for _, block := range blocks {
rawdb.WriteL1Origin(db, block.Number(), &rawdb.L1Origin{
BlockID: block.Number(),
L1BlockHeight: block.Number(),
L1BlockHash: block.Hash(),
})
}
// 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 blocks, ethService
} }
func TestVerifyHeader(t *testing.T) { func TestVerifyHeader(t *testing.T) {
ethService, blocks := newTestBackend(t) // Generate test chain.
blocks, ethService := generateTestChain(t)
for _, b := range blocks { for _, b := range blocks {
err := testEngine.VerifyHeader(ethService.BlockChain(), b.Header()) err := testEngine.VerifyHeader(ethService.BlockChain(), b.Header())

View file

@ -2269,8 +2269,13 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Block) error {
} else { } else {
// len(newChain) == 0 && len(oldChain) > 0 // len(newChain) == 0 && len(oldChain) > 0
// rewind the canonical chain to a lower point. // rewind the canonical chain to a lower point.
// CHANGE(taiko): use debug log level to avoid logging too many logs when frequently soft block rollback.
if bc.chainConfig.Taiko {
log.Debug("Impossible reorg, please file an issue", "oldnum", oldBlock.Number(), "oldhash", oldBlock.Hash(), "oldblocks", len(oldChain), "newnum", newBlock.Number(), "newhash", newBlock.Hash(), "newblocks", len(newChain))
} else {
log.Error("Impossible reorg, please file an issue", "oldnum", oldBlock.Number(), "oldhash", oldBlock.Hash(), "oldblocks", len(oldChain), "newnum", newBlock.Number(), "newhash", newBlock.Hash(), "newblocks", len(newChain)) log.Error("Impossible reorg, please file an issue", "oldnum", oldBlock.Number(), "oldhash", oldBlock.Hash(), "oldblocks", len(oldChain), "newnum", newBlock.Number(), "newhash", newBlock.Hash(), "newblocks", len(newChain))
} }
}
// Acquire the tx-lookup lock before mutation. This step is essential // Acquire the tx-lookup lock before mutation. This step is essential
// as the txlookups should be changed atomically, and all subsequent // as the txlookups should be changed atomically, and all subsequent
// reads should be blocked until the mutation is complete. // reads should be blocked until the mutation is complete.

View file

@ -18,14 +18,22 @@ func (l L1Origin) MarshalJSON() ([]byte, error) {
type L1Origin struct { type L1Origin struct {
BlockID *math.HexOrDecimal256 `json:"blockID" gencodec:"required"` BlockID *math.HexOrDecimal256 `json:"blockID" gencodec:"required"`
L2BlockHash common.Hash `json:"l2BlockHash"` L2BlockHash common.Hash `json:"l2BlockHash"`
L1BlockHeight *math.HexOrDecimal256 `json:"l1BlockHeight" gencodec:"required"` L1BlockHeight *math.HexOrDecimal256 `json:"l1BlockHeight"`
L1BlockHash common.Hash `json:"l1BlockHash" gencodec:"required"` L1BlockHash common.Hash `json:"l1BlockHash"`
BatchID *big.Int `json:"batchID"`
EndOfBlock bool `json:"endOfBlock"`
EndOfPreconf bool `json:"endOfPreconf"`
Preconfer common.Address `json:"preconfer"`
} }
var enc L1Origin var enc L1Origin
enc.BlockID = (*math.HexOrDecimal256)(l.BlockID) enc.BlockID = (*math.HexOrDecimal256)(l.BlockID)
enc.L2BlockHash = l.L2BlockHash enc.L2BlockHash = l.L2BlockHash
enc.L1BlockHeight = (*math.HexOrDecimal256)(l.L1BlockHeight) enc.L1BlockHeight = (*math.HexOrDecimal256)(l.L1BlockHeight)
enc.L1BlockHash = l.L1BlockHash enc.L1BlockHash = l.L1BlockHash
enc.BatchID = l.BatchID
enc.EndOfBlock = l.EndOfBlock
enc.EndOfPreconf = l.EndOfPreconf
enc.Preconfer = l.Preconfer
return json.Marshal(&enc) return json.Marshal(&enc)
} }
@ -34,8 +42,12 @@ func (l *L1Origin) UnmarshalJSON(input []byte) error {
type L1Origin struct { type L1Origin struct {
BlockID *math.HexOrDecimal256 `json:"blockID" gencodec:"required"` BlockID *math.HexOrDecimal256 `json:"blockID" gencodec:"required"`
L2BlockHash *common.Hash `json:"l2BlockHash"` L2BlockHash *common.Hash `json:"l2BlockHash"`
L1BlockHeight *math.HexOrDecimal256 `json:"l1BlockHeight" gencodec:"required"` L1BlockHeight *math.HexOrDecimal256 `json:"l1BlockHeight"`
L1BlockHash *common.Hash `json:"l1BlockHash" gencodec:"required"` L1BlockHash *common.Hash `json:"l1BlockHash"`
BatchID *big.Int `json:"batchID"`
EndOfBlock *bool `json:"endOfBlock"`
EndOfPreconf *bool `json:"endOfPreconf"`
Preconfer *common.Address `json:"preconfer"`
} }
var dec L1Origin var dec L1Origin
if err := json.Unmarshal(input, &dec); err != nil { if err := json.Unmarshal(input, &dec); err != nil {
@ -48,13 +60,23 @@ func (l *L1Origin) UnmarshalJSON(input []byte) error {
if dec.L2BlockHash != nil { if dec.L2BlockHash != nil {
l.L2BlockHash = *dec.L2BlockHash l.L2BlockHash = *dec.L2BlockHash
} }
if dec.L1BlockHeight == nil { if dec.L1BlockHeight != nil {
return errors.New("missing required field 'l1BlockHeight' for L1Origin")
}
l.L1BlockHeight = (*big.Int)(dec.L1BlockHeight) l.L1BlockHeight = (*big.Int)(dec.L1BlockHeight)
if dec.L1BlockHash == nil {
return errors.New("missing required field 'l1BlockHash' for L1Origin")
} }
if dec.L1BlockHash != nil {
l.L1BlockHash = *dec.L1BlockHash l.L1BlockHash = *dec.L1BlockHash
}
if dec.BatchID != nil {
l.BatchID = dec.BatchID
}
if dec.EndOfBlock != nil {
l.EndOfBlock = *dec.EndOfBlock
}
if dec.EndOfPreconf != nil {
l.EndOfPreconf = *dec.EndOfPreconf
}
if dec.Preconfer != nil {
l.Preconfer = *dec.Preconfer
}
return nil return nil
} }

View file

@ -31,8 +31,20 @@ func l1OriginKey(blockID *big.Int) []byte {
type L1Origin struct { type L1Origin struct {
BlockID *big.Int `json:"blockID" gencodec:"required"` BlockID *big.Int `json:"blockID" gencodec:"required"`
L2BlockHash common.Hash `json:"l2BlockHash"` L2BlockHash common.Hash `json:"l2BlockHash"`
L1BlockHeight *big.Int `json:"l1BlockHeight" gencodec:"required"` L1BlockHeight *big.Int `json:"l1BlockHeight"`
L1BlockHash common.Hash `json:"l1BlockHash" gencodec:"required"` L1BlockHash common.Hash `json:"l1BlockHash"`
BatchID *big.Int `json:"batchID" rlp:"optional"`
EndOfBlock bool `json:"endOfBlock" rlp:"optional"`
EndOfPreconf bool `json:"endOfPreconf" rlp:"optional"`
Preconfer common.Address `json:"preconfer" rlp:"optional"`
}
// L1OriginLegacy represents the legacy L1Origin structure of a L2 block.
type L1OriginLegacy struct {
BlockID *big.Int `json:"blockID" gencodec:"required"`
L2BlockHash common.Hash `json:"l2BlockHash"`
L1BlockHeight *big.Int `json:"l1BlockHeight"`
L1BlockHash common.Hash `json:"l1BlockHash"`
} }
type l1OriginMarshaling struct { type l1OriginMarshaling struct {
@ -40,6 +52,11 @@ type l1OriginMarshaling struct {
L1BlockHeight *math.HexOrDecimal256 L1BlockHeight *math.HexOrDecimal256
} }
// IsSoftBlock returns true if the L1Origin is a softblock.
func (l *L1Origin) IsSoftBlock() bool {
return l.BatchID != nil
}
// WriteL1Origin stores a L1Origin into the database. // WriteL1Origin stores a L1Origin into the database.
func WriteL1Origin(db ethdb.KeyValueWriter, blockID *big.Int, l1Origin *L1Origin) { func WriteL1Origin(db ethdb.KeyValueWriter, blockID *big.Int, l1Origin *L1Origin) {
data, err := rlp.EncodeToBytes(l1Origin) data, err := rlp.EncodeToBytes(l1Origin)
@ -52,16 +69,40 @@ func WriteL1Origin(db ethdb.KeyValueWriter, blockID *big.Int, l1Origin *L1Origin
} }
} }
// ReadL1Origin retrieves the given L2 block's L1Origin from database. // DeleteL1Origin removes the L1Origin.
func DeleteL1Origin(db ethdb.KeyValueWriter, blockID *big.Int) {
_ = db.Delete(l1OriginKey(blockID))
}
// ReadL1Origin retrieves the L1Origin of the given blockID from the database.
func ReadL1Origin(db ethdb.KeyValueReader, blockID *big.Int) (*L1Origin, error) { func ReadL1Origin(db ethdb.KeyValueReader, blockID *big.Int) (*L1Origin, error) {
data, _ := db.Get(l1OriginKey(blockID)) data, _ := db.Get(l1OriginKey(blockID))
if len(data) == 0 { if len(data) == 0 {
return nil, nil return nil, nil
} }
// First try to decode the new version (with new fields).
l1Origin := new(L1Origin) l1Origin := new(L1Origin)
if err := rlp.Decode(bytes.NewReader(data), l1Origin); err != nil { if err := rlp.Decode(bytes.NewReader(data), l1Origin); err != nil {
return nil, fmt.Errorf("invalid L1Origin RLP bytes: %w", err) // If decoding the new version fails, try to decode the legacy version (without new fields).
l1OriginLegacy := new(L1OriginLegacy)
if err := rlp.Decode(bytes.NewReader(data), &l1OriginLegacy); err != nil {
return nil, fmt.Errorf("invalid legacy L1Origin RLP bytes: %w", err)
}
// If decoding legacy version succeeds, manually
// construct the new L1Origin with default values for the new fields.
l1Origin = &L1Origin{
BlockID: l1OriginLegacy.BlockID,
L2BlockHash: l1OriginLegacy.L2BlockHash,
L1BlockHeight: l1OriginLegacy.L1BlockHeight,
L1BlockHash: l1OriginLegacy.L1BlockHash,
// default value, as the new fields are not present in the legacy version.
BatchID: common.Big0,
EndOfBlock: false,
EndOfPreconf: false,
Preconfer: common.Address{},
}
} }
return l1Origin, nil return l1Origin, nil

View file

@ -42,6 +42,7 @@ func TestL1Origin(t *testing.T) {
l1Origin, err := ReadL1Origin(db, testL1Origin.BlockID) l1Origin, err := ReadL1Origin(db, testL1Origin.BlockID)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, l1Origin) require.NotNil(t, l1Origin)
assert.Equal(t, testL1Origin.BatchID, l1Origin.BatchID)
assert.Equal(t, testL1Origin.BlockID, l1Origin.BlockID) assert.Equal(t, testL1Origin.BlockID, l1Origin.BlockID)
assert.Equal(t, testL1Origin.L2BlockHash, l1Origin.L2BlockHash) assert.Equal(t, testL1Origin.L2BlockHash, l1Origin.L2BlockHash)
assert.Equal(t, testL1Origin.L1BlockHeight, l1Origin.L1BlockHeight) assert.Equal(t, testL1Origin.L1BlockHeight, l1Origin.L1BlockHeight)

View file

@ -10,7 +10,8 @@ import (
) )
var ( var (
InternalDevnetOntakeBlock = new(big.Int).SetUint64(0) InternalDevnetOntakeBlock = common.Big0
PreconfDevnetOntakeBlock = common.Big0
HeklaOntakeBlock = new(big.Int).SetUint64(840_512) HeklaOntakeBlock = new(big.Int).SetUint64(840_512)
MainnetOntakeBlock = new(big.Int).SetUint64(538_304) MainnetOntakeBlock = new(big.Int).SetUint64(538_304)
) )
@ -54,6 +55,10 @@ func TaikoGenesisBlock(networkID uint64) *Genesis {
chainConfig.ChainID = params.HeklaNetworkID chainConfig.ChainID = params.HeklaNetworkID
chainConfig.OntakeBlock = HeklaOntakeBlock chainConfig.OntakeBlock = HeklaOntakeBlock
allocJSON = taikoGenesis.HeklaGenesisAllocJSON allocJSON = taikoGenesis.HeklaGenesisAllocJSON
case params.PreconfDevnetNetworkID.Uint64():
chainConfig.ChainID = params.PreconfDevnetNetworkID
chainConfig.OntakeBlock = PreconfDevnetOntakeBlock
allocJSON = taikoGenesis.PreconfDevnetGenesisAllocJSON
default: default:
chainConfig.ChainID = params.TaikoInternalL2ANetworkID chainConfig.ChainID = params.TaikoInternalL2ANetworkID
chainConfig.OntakeBlock = InternalDevnetOntakeBlock chainConfig.OntakeBlock = InternalDevnetOntakeBlock

View file

@ -33,3 +33,6 @@ var HeklaGenesisAllocJSON []byte
//go:embed mainnet.json //go:embed mainnet.json
var MainnetGenesisAllocJSON []byte var MainnetGenesisAllocJSON []byte
//go:embed preconf_devnet.json
var PreconfDevnetGenesisAllocJSON []byte

File diff suppressed because one or more lines are too long

View file

@ -48,6 +48,7 @@ type EthAPIBackend struct {
allowUnprotectedTxs bool allowUnprotectedTxs bool
eth *Ethereum eth *Ethereum
gpo *gasprice.Oracle gpo *gasprice.Oracle
preconfirmationForwardingURL string // CHANGE(taiko): add preconfirmation forwarding URL
} }
// ChainConfig returns the active chain configuration. // ChainConfig returns the active chain configuration.
@ -431,3 +432,8 @@ func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, re
func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) {
return b.eth.stateAtTransaction(ctx, block, txIndex, reexec) return b.eth.stateAtTransaction(ctx, block, txIndex, reexec)
} }
// GetPreconfirmationForwardingURL returns the URL to forward RPC requests before confirmation.
func (b *EthAPIBackend) GetPreconfirmationForwardingURL() string {
return b.preconfirmationForwardingURL
}

View file

@ -96,6 +96,8 @@ type Ethereum struct {
lock sync.RWMutex // Protects the variadic fields (e.g. gas price and etherbase) lock sync.RWMutex // Protects the variadic fields (e.g. gas price and etherbase)
shutdownTracker *shutdowncheck.ShutdownTracker // Tracks if and when the node has shutdown ungracefully shutdownTracker *shutdowncheck.ShutdownTracker // Tracks if and when the node has shutdown ungracefully
PreconfirmationForwardingURL string // CHANGE(taiko): add preconfirmation forwarding URL
} }
// New creates a new Ethereum object (including the initialisation of the common Ethereum object), // New creates a new Ethereum object (including the initialisation of the common Ethereum object),
@ -254,7 +256,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
eth.miner = miner.New(eth, config.Miner, eth.engine) eth.miner = miner.New(eth, config.Miner, eth.engine)
eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData)) eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData))
eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, eth, nil} // CHANGE(taiko): set up the pre-confirmation forwarding URL
eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, eth, nil, config.PreconfirmationForwardingURL}
if eth.APIBackend.allowUnprotectedTxs { if eth.APIBackend.allowUnprotectedTxs {
log.Info("Unprotected transactions allowed") log.Info("Unprotected transactions allowed")
} }

View file

@ -400,6 +400,11 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl
// generating the payload. It's a special corner case that a few slots are // 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. // missing and we are requested to generate the payload in slot.
} else if isTaiko { // CHANGE(taiko): reorg is allowed in L2. } else if isTaiko { // CHANGE(taiko): reorg is allowed in L2.
// After inserting a new canonical head, we delete all L1Origin data of the pending soft blocks.
head := api.eth.BlockChain().CurrentBlock()
for number := head.Number.Uint64(); number > block.NumberU64(); number-- {
rawdb.DeleteL1Origin(api.eth.ChainDb(), new(big.Int).SetUint64(number))
}
if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil { if latestValid, err := api.eth.BlockChain().SetCanonical(block); err != nil {
return engine.ForkChoiceResponse{PayloadStatus: engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &latestValid}}, err return engine.ForkChoiceResponse{PayloadStatus: engine.PayloadStatusV1{Status: engine.INVALID, LatestValidHash: &latestValid}}, err
} }
@ -446,6 +451,17 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl
if payloadAttributes != nil { if payloadAttributes != nil {
// CHANGE(taiko): create a L2 block by Taiko protocol. // CHANGE(taiko): create a L2 block by Taiko protocol.
if isTaiko { if isTaiko {
// L1Origin is a required field in PayloadAttributesV1.
if payloadAttributes.L1Origin == nil {
return valid(nil), engine.InvalidPayloadAttributes.With(errors.New("L1Origin is nil"))
}
// If it's not a softblock creation request, then `L1BlockHash` and `L1BlockHeight` are required.
if payloadAttributes.L1Origin.BatchID == nil &&
(payloadAttributes.L1Origin.L1BlockHash == (common.Hash{}) ||
payloadAttributes.L1Origin.L1BlockHeight == nil) {
return valid(nil), engine.InvalidPayloadAttributes.With(errors.New("L1BlockHash or L1BlockHeight is nil"))
}
// No need to check payloadAttribute here, because all its fields are // No need to check payloadAttribute here, because all its fields are
// marked as required. // marked as required.
block, err := api.eth.Miner().SealBlockWith( block, err := api.eth.Miner().SealBlockWith(
@ -488,8 +504,10 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl
// Write L1Origin. // Write L1Origin.
rawdb.WriteL1Origin(api.eth.ChainDb(), l1Origin.BlockID, l1Origin) rawdb.WriteL1Origin(api.eth.ChainDb(), l1Origin.BlockID, l1Origin)
// Write the head L1Origin. // Write the head L1Origin, only when it's not a soft block.
if l1Origin.BatchID == nil {
rawdb.WriteHeadL1Origin(api.eth.ChainDb(), l1Origin.BlockID) rawdb.WriteHeadL1Origin(api.eth.ChainDb(), l1Origin.BlockID)
}
return valid(&id), nil return valid(&id), nil
} }

View file

@ -157,6 +157,9 @@ type Config struct {
// OverrideVerkle (TODO: remove after the fork) // OverrideVerkle (TODO: remove after the fork)
OverrideVerkle *uint64 `toml:",omitempty"` OverrideVerkle *uint64 `toml:",omitempty"`
// CHANGE(taiko): add preconfirmation forwarding URL
PreconfirmationForwardingURL string
} }
// CreateConsensusEngine creates a consensus engine for the given chain config. // CreateConsensusEngine creates a consensus engine for the given chain config.
@ -165,7 +168,7 @@ type Config struct {
func CreateConsensusEngine(config *params.ChainConfig, db ethdb.Database) (consensus.Engine, error) { func CreateConsensusEngine(config *params.ChainConfig, db ethdb.Database) (consensus.Engine, error) {
// CHANGE(taiko): use Taiko consensus engine when the --taiko flag is set. // CHANGE(taiko): use Taiko consensus engine when the --taiko flag is set.
if config.Taiko { if config.Taiko {
return taiko.New(config), nil return taiko.New(config, db), nil
} }
// Geth v1.14.0 dropped support for non-merged networks in any consensus // Geth v1.14.0 dropped support for non-merged networks in any consensus
// mode. If such a network is requested, reject startup. // mode. If such a network is requested, reject startup.

View file

@ -69,6 +69,10 @@ func TestHeadL1Origin(t *testing.T) {
L2BlockHash: headerHash, L2BlockHash: headerHash,
L1BlockHeight: randomBigInt(), L1BlockHeight: randomBigInt(),
L1BlockHash: randomHash(), L1BlockHash: randomHash(),
BatchID: nil,
EndOfBlock: false,
EndOfPreconf: false,
Preconfer: common.Address{},
} }
rawdb.WriteL1Origin(db, testL1Origin.BlockID, testL1Origin) rawdb.WriteL1Origin(db, testL1Origin.BlockID, testL1Origin)
@ -78,6 +82,7 @@ func TestHeadL1Origin(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, testL1Origin, l1OriginFound) require.Equal(t, testL1Origin, l1OriginFound)
require.False(t, l1OriginFound.IsSoftBlock())
} }
func TestL1OriginByID(t *testing.T) { func TestL1OriginByID(t *testing.T) {
@ -89,6 +94,10 @@ func TestL1OriginByID(t *testing.T) {
L2BlockHash: headerHash, L2BlockHash: headerHash,
L1BlockHeight: randomBigInt(), L1BlockHeight: randomBigInt(),
L1BlockHash: randomHash(), L1BlockHash: randomHash(),
BatchID: common.Big1,
EndOfBlock: false,
EndOfPreconf: false,
Preconfer: testAddr,
} }
l1OriginFound, err := ec.L1OriginByID(context.Background(), testL1Origin.BlockID) l1OriginFound, err := ec.L1OriginByID(context.Background(), testL1Origin.BlockID)
@ -102,6 +111,7 @@ func TestL1OriginByID(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, testL1Origin, l1OriginFound) require.Equal(t, testL1Origin, l1OriginFound)
require.True(t, l1OriginFound.IsSoftBlock())
} }
// randomHash generates a random blob of data and returns it as a hash. // randomHash generates a random blob of data and returns it as a hash.

View file

@ -1974,6 +1974,18 @@ func (api *TransactionAPI) FillTransaction(ctx context.Context, args Transaction
// SendRawTransaction will add the signed transaction to the transaction pool. // SendRawTransaction will add the signed transaction to the transaction pool.
// The sender is responsible for signing the transaction and using the correct nonce. // The sender is responsible for signing the transaction and using the correct nonce.
func (api *TransactionAPI) SendRawTransaction(ctx context.Context, input hexutil.Bytes) (common.Hash, error) { func (api *TransactionAPI) SendRawTransaction(ctx context.Context, input hexutil.Bytes) (common.Hash, error) {
// CHANGE(taiko): Forward the request to the preconf node if specified.
if forwardURL := api.b.GetPreconfirmationForwardingURL(); forwardURL != "" {
log.Info("Forwarding SendRawTransaction request", "forwardURL", forwardURL)
// Forward the raw transaction to the specified URL
h, err := forward[string](forwardURL, "eth_sendRawTransaction", []interface{}{input.String()})
if err == nil && h != nil {
return common.HexToHash(*h), nil
} else {
return common.Hash{}, err
}
}
tx := new(types.Transaction) tx := new(types.Transaction)
if err := tx.UnmarshalBinary(input); err != nil { if err := tx.UnmarshalBinary(input); err != nil {
return common.Hash{}, err return common.Hash{}, err

View file

@ -3400,3 +3400,8 @@ func testRPCResponseWithFile(t *testing.T, testid int, result interface{}, rpc s
func addressToHash(a common.Address) common.Hash { func addressToHash(a common.Address) common.Hash {
return common.BytesToHash(a.Bytes()) return common.BytesToHash(a.Bytes())
} }
// CHANGE(taiko): add preconfirmation forwarding URL
func (b testBackend) GetPreconfirmationForwardingURL() string {
return ""
}

View file

@ -96,6 +96,9 @@ type Backend interface {
SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription
BloomStatus() (uint64, uint64) BloomStatus() (uint64, uint64)
ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession)
// CHANGE(taiko): add preconfirmation forwarding URL
GetPreconfirmationForwardingURL() string
} }
func GetAPIs(apiBackend Backend) []rpc.API { func GetAPIs(apiBackend Backend) []rpc.API {

View file

@ -0,0 +1,94 @@
package ethapi
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/ethereum/go-ethereum/log"
)
type rpcRequest struct {
Jsonrpc string `json:"jsonrpc"`
Method string `json:"method"`
Params []interface{} `json:"params"`
ID int `json:"id"`
}
type rpcResponse struct {
Jsonrpc string `json:"jsonrpc"`
ID int `json:"id"`
Result *json.RawMessage `json:"result"`
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data"`
} `json:"error,omitempty"`
}
func forward[T any](forwardURL string, method string, params []interface{}) (*T, error) {
rpcReq := rpcRequest{
Jsonrpc: "2.0",
Method: method,
Params: params,
ID: 1,
}
jsonData, err := json.Marshal(rpcReq)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", forwardURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to forward transaction, status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var rpcResp rpcResponse
// Unmarshal the response into the struct
if err := json.Unmarshal(body, &rpcResp); err != nil {
return nil, err
}
// Check for errors in the response
if rpcResp.Error != nil {
err := fmt.Errorf("RPC error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
log.Error("forwarded request error", "err", err, "method", method, "params", params)
return nil, fmt.Errorf("RPC error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
}
if rpcResp.Result == nil {
log.Warn("forwarded request result is nil", "method", method)
return nil, nil
}
// Unmarshal the Result into the desired type
var result T
if err := json.Unmarshal(*rpcResp.Result, &result); err != nil {
return nil, err
}
return &result, nil
}

View file

@ -405,3 +405,8 @@ func (b *backendMock) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent)
} }
func (b *backendMock) Engine() consensus.Engine { return nil } func (b *backendMock) Engine() consensus.Engine { return nil }
// CHANGE(taiko): add preconfirmation forwarding URL
func (b *backendMock) GetPreconfirmationForwardingURL() string {
return ""
}

View file

@ -37,6 +37,8 @@ const (
MiscCategory = "MISC" MiscCategory = "MISC"
TestingCategory = "TESTING" TestingCategory = "TESTING"
DeprecatedCategory = "ALIASED (deprecated)" DeprecatedCategory = "ALIASED (deprecated)"
// CHANGE(taiko): Add a new flag category for Taiko network
TaikoCategory = "TAIKO"
) )
func init() { func init() {

View file

@ -300,6 +300,7 @@ var NetworkNames = map[string]string{
JolnirNetworkID.String(): "Taiko Alpha-5 L2 (Jolnir)", JolnirNetworkID.String(): "Taiko Alpha-5 L2 (Jolnir)",
KatlaNetworkID.String(): "Taiko Alpha-6 L2 (Katla)", KatlaNetworkID.String(): "Taiko Alpha-6 L2 (Katla)",
HeklaNetworkID.String(): "Taiko Alpha-7 L2 (Hekla)", HeklaNetworkID.String(): "Taiko Alpha-7 L2 (Hekla)",
PreconfDevnetNetworkID.String(): "Taiko Preconfirmation Devnet",
} }
// ChainConfig is the core config which determines the blockchain settings. // ChainConfig is the core config which determines the blockchain settings.

View file

@ -20,6 +20,7 @@ var (
JolnirNetworkID = big.NewInt(167007) JolnirNetworkID = big.NewInt(167007)
KatlaNetworkID = big.NewInt(167008) KatlaNetworkID = big.NewInt(167008)
HeklaNetworkID = big.NewInt(167009) HeklaNetworkID = big.NewInt(167009)
PreconfDevnetNetworkID = big.NewInt(167010)
) )
var networkIDToChainConfig = map[*big.Int]*ChainConfig{ var networkIDToChainConfig = map[*big.Int]*ChainConfig{
@ -33,6 +34,7 @@ var networkIDToChainConfig = map[*big.Int]*ChainConfig{
JolnirNetworkID: TaikoChainConfig, JolnirNetworkID: TaikoChainConfig,
KatlaNetworkID: TaikoChainConfig, KatlaNetworkID: TaikoChainConfig,
HeklaNetworkID: TaikoChainConfig, HeklaNetworkID: TaikoChainConfig,
PreconfDevnetNetworkID: TaikoChainConfig,
MainnetChainConfig.ChainID: MainnetChainConfig, MainnetChainConfig.ChainID: MainnetChainConfig,
SepoliaChainConfig.ChainID: SepoliaChainConfig, SepoliaChainConfig.ChainID: SepoliaChainConfig,
TestChainConfig.ChainID: TestChainConfig, TestChainConfig.ChainID: TestChainConfig,