diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index 185d2d75d..925d05a12 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -561,6 +561,10 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header return nil } +func (c *Clique) StartHook(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB) error { + return nil +} + // Finalize implements consensus.Engine, ensuring no uncles are set, nor block // rewards given. func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header) { diff --git a/consensus/consensus.go b/consensus/consensus.go index 53908490a..f7cd5520d 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -81,6 +81,10 @@ type Engine interface { // rules of a particular engine. The changes are executed inline. Prepare(chain ChainHeaderReader, header *types.Header) error + // StartHook calling before start apply transactions of block + //StartHook(chain consensus.ChainHeaderReader, header *types.Header, preHeader *types.Header, state *state.StateDB) error + StartHook(chain ChainHeaderReader, header *types.Header, state *state.StateDB) error + // Finalize runs any post-transaction state modifications (e.g. block rewards) // but does not assemble the block. // diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index 6549848e6..73db14323 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -590,6 +590,10 @@ func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.H return nil } +func (ethash *Ethash) StartHook(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB) error { + return nil +} + // Finalize implements consensus.Engine, accumulating the block and uncle rewards, // setting the final state on the header func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header) { diff --git a/consensus/l2/consensus.go b/consensus/l2/consensus.go index 1f70ed344..0be10d8a1 100644 --- a/consensus/l2/consensus.go +++ b/consensus/l2/consensus.go @@ -3,21 +3,27 @@ package l2 import ( "errors" "fmt" + "github.com/morph-l2/go-ethereum/contracts/morphtoken" "math/big" "github.com/morph-l2/go-ethereum/common" "github.com/morph-l2/go-ethereum/consensus" "github.com/morph-l2/go-ethereum/consensus/misc" + "github.com/morph-l2/go-ethereum/contracts/l2staking" + "github.com/morph-l2/go-ethereum/core" "github.com/morph-l2/go-ethereum/core/state" "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/core/vm" "github.com/morph-l2/go-ethereum/params" + "github.com/morph-l2/go-ethereum/rollup/rcfg" "github.com/morph-l2/go-ethereum/rpc" "github.com/morph-l2/go-ethereum/trie" ) var ( - l2Difficulty = common.Big0 // The default block difficulty in the l2 consensus - l2Nonce = types.EncodeNonce(0) // The default block nonce in the l2 consensus + l2Difficulty = common.Big0 // The default block difficulty in the l2 consensus + l2Nonce = types.EncodeNonce(0) // The default block nonce in the l2 consensus + rewardEpoch uint64 = 86400 ) // Various error messages to mark blocks invalid. These should be private to @@ -29,9 +35,28 @@ var ( errInvalidNonce = errors.New("invalid nonce") errInvalidUncleHash = errors.New("invalid uncle hash") errInvalidTimestamp = errors.New("invalid timestamp") - errInvalidCoinbase = errors.New("invalid coinbase") ) +// chain context +type chainContext struct { + Chain consensus.ChainHeaderReader + engine consensus.Engine +} + +func (c chainContext) Engine() consensus.Engine { + return c.engine +} + +func (c chainContext) GetHeader(hash common.Hash, number uint64) *types.Header { + return c.Chain.GetHeader(hash, number) +} + +func (c chainContext) Config() *params.ChainConfig { + return c.Chain.Config() +} + +var _ = consensus.Engine(&Consensus{}) + type Consensus struct { ethone consensus.Engine // Original consensus engine used in eth1, e.g. ethash or clique config *params.ChainConfig @@ -142,9 +167,6 @@ func (l2 *Consensus) verifyHeader(chain consensus.ChainHeaderReader, header, par return errInvalidUncleHash } - if l2.config.Morph.FeeVaultEnabled() && header.Coinbase != types.EmptyAddress { - return errInvalidCoinbase - } // Verify the timestamp if header.Time <= parent.Time { return errInvalidTimestamp @@ -187,9 +209,43 @@ func (l2 *Consensus) Prepare(chain consensus.ChainHeaderReader, header *types.He header.Nonce = l2Nonce header.UncleHash = types.EmptyUncleHash header.Extra = []byte{} // disable extra field filling with bytes - // set coinbase to empty address, if feeVault is enabled - if l2.config.Morph.FeeVaultEnabled() { - header.Coinbase = types.EmptyAddress + return nil +} + +// StartHook implements calling before start apply transactions of block +func (l2 *Consensus) StartHook(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB) error { + rewardStarted := state.GetState(rcfg.L2StakingAddress, rcfg.RewardStartedSlot).Big() + if rewardStarted.Cmp(common.Big1) != 0 { + return nil + } + inflationMintedEpochs := state.GetState(rcfg.MorphTokenAddress, rcfg.InflationMintedEpochsSolt).Big().Uint64() + rewardStartTime := state.GetState(rcfg.L2StakingAddress, rcfg.RewardStartTimeSlot).Big().Uint64() + parentHeader := chain.GetHeaderByHash(header.ParentHash) + if parentHeader == nil { + return consensus.ErrUnknownAncestor + } + cx := chainContext{Chain: chain, engine: l2.ethone} + blockContext := core.NewEVMBlockContext(header, cx, l2.config, nil) + // TODO tracer + evm := vm.NewEVM(blockContext, vm.TxContext{}, state, l2.config, vm.Config{Tracer: nil}) + stakingCallData, err := l2staking.PacketData(parentHeader.Coinbase) + if err != nil { + return err + } + systemAddress := vm.AccountRef(rcfg.SystemAddress) + _, _, err = evm.Call(systemAddress, rcfg.L2StakingAddress, stakingCallData, params.MaxGasLimit, common.Big0) + if err != nil { + return err + } + if header.Time > rewardStartTime && (header.Time-rewardStartTime)/rewardEpoch > inflationMintedEpochs { + callData, err := morphtoken.PacketData() + if err != nil { + return err + } + _, _, err = evm.Call(systemAddress, rcfg.MorphTokenAddress, callData, params.MaxGasLimit, common.Big0) + if err != nil { + return err + } } return nil } diff --git a/contracts/l2staking/l2staking.go b/contracts/l2staking/l2staking.go new file mode 100644 index 000000000..6c5473dd6 --- /dev/null +++ b/contracts/l2staking/l2staking.go @@ -0,0 +1,42 @@ +package l2staking + +import ( + "fmt" + "strings" + "sync" + + "github.com/morph-l2/go-ethereum/accounts/abi" + "github.com/morph-l2/go-ethereum/common" +) + +const jsonData = `[{"inputs":[{"internalType":"address","name":"sequencerAddr","type":"address"}],"name":"recordBlocks","outputs":[],"stateMutability":"nonpayable","type":"function"}]` + +var ( + l2StakingABI *abi.ABI + loadOnce sync.Once + loadErr error +) + +func Abi() (*abi.ABI, error) { + loadOnce.Do(func() { + stakingABI, err := abi.JSON(strings.NewReader(jsonData)) + if err != nil { + loadErr = fmt.Errorf("failed to parse ABI: %w", err) + return + } + l2StakingABI = &stakingABI + }) + return l2StakingABI, loadErr +} + +func PacketData(addr common.Address) ([]byte, error) { + a, err := Abi() + if err != nil { + return nil, fmt.Errorf("failed to get ABI: %w", err) + } + data, err := a.Pack("recordBlocks", addr) + if err != nil { + return nil, fmt.Errorf("failed to pack data: %w", err) + } + return data, nil +} diff --git a/contracts/l2staking/l2staking_test.go b/contracts/l2staking/l2staking_test.go new file mode 100644 index 000000000..1e1a07d50 --- /dev/null +++ b/contracts/l2staking/l2staking_test.go @@ -0,0 +1,13 @@ +package l2staking + +import ( + "testing" + + "github.com/morph-l2/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestPackData(t *testing.T) { + _, err := PacketData(common.HexToAddress("0x01")) + require.NoError(t, err) +} diff --git a/contracts/morphtoken/morph_token.go b/contracts/morphtoken/morph_token.go new file mode 100644 index 000000000..52f80d860 --- /dev/null +++ b/contracts/morphtoken/morph_token.go @@ -0,0 +1,41 @@ +package morphtoken + +import ( + "fmt" + "strings" + "sync" + + "github.com/morph-l2/go-ethereum/accounts/abi" +) + +const jsonData = `[{"inputs":[],"name":"mintInflations","outputs":[],"stateMutability":"nonpayable","type":"function"}]` + +var ( + morphTokenABI *abi.ABI + loadOnce sync.Once + loadErr error +) + +func Abi() (*abi.ABI, error) { + loadOnce.Do(func() { + tokenABI, err := abi.JSON(strings.NewReader(jsonData)) + if err != nil { + loadErr = fmt.Errorf("failed to parse ABI: %w", err) + return + } + morphTokenABI = &tokenABI + }) + return morphTokenABI, loadErr +} + +func PacketData() ([]byte, error) { + a, err := Abi() + if err != nil { + return nil, fmt.Errorf("failed to get ABI: %w", err) + } + data, err := a.Pack("mintInflations") + if err != nil { + return nil, fmt.Errorf("failed to pack data: %w", err) + } + return data, nil +} diff --git a/contracts/morphtoken/morph_token_test.go b/contracts/morphtoken/morph_token_test.go new file mode 100644 index 000000000..a5b0a7602 --- /dev/null +++ b/contracts/morphtoken/morph_token_test.go @@ -0,0 +1,12 @@ +package morphtoken + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPackData(t *testing.T) { + _, err := PacketData() + require.NoError(t, err) +} diff --git a/core/state_processor.go b/core/state_processor.go index e4e2e00e8..0ab47a65f 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -92,6 +92,10 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg blockContext := NewEVMBlockContext(header, p.bc, p.config, nil) vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) processorBlockTransactionGauge.Update(int64(block.Transactions().Len())) + err := p.engine.StartHook(p.bc, header, statedb) + if err != nil { + return nil, nil, 0, err + } // Iterate over and process the individual transactions for i, tx := range block.Transactions() { msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee) diff --git a/core/types/l2trace.go b/core/types/l2trace.go index a125a7556..0337515d2 100644 --- a/core/types/l2trace.go +++ b/core/types/l2trace.go @@ -20,6 +20,7 @@ type BlockTrace struct { Bytecodes []*BytecodeTrace `json:"codes"` TxStorageTraces []*StorageTrace `json:"txStorageTraces,omitempty"` ExecutionResults []*ExecutionResult `json:"executionResults"` + StartHookResult *ExecutionResult `json:"startHookResult,omitempty"` WithdrawTrieRoot common.Hash `json:"withdraw_trie_root,omitempty"` SequencerSetVerifyHash common.Hash `json:"sequencer_set_verify_hash,omitempty"` StartL1QueueIndex uint64 `json:"startL1QueueIndex"` diff --git a/eth/catalyst/api_types.go b/eth/catalyst/api_types.go index a7d20b241..c56ca331d 100644 --- a/eth/catalyst/api_types.go +++ b/eth/catalyst/api_types.go @@ -39,8 +39,9 @@ type assembleBlockParamsMarshaling struct { //go:generate go run github.com/fjl/gencodec -type AssembleL2BlockParams -field-override assembleL2BlockParamsMarshaling -out gen_l2blockparams.go type AssembleL2BlockParams struct { - Number uint64 `json:"number" gencodec:"required"` - Transactions [][]byte `json:"transactions"` + Number uint64 `json:"number" gencodec:"required"` + Coinbase common.Address `json:"coinbase"` + Transactions [][]byte `json:"transactions"` } // JSON type overrides for assembleL2BlockParams. @@ -125,12 +126,13 @@ type executableL2DataMarshaling struct { // SafeL2Data is the block data which is approved in L1 and considered to be safe type SafeL2Data struct { - Number uint64 `json:"number" gencodec:"required"` - GasLimit uint64 `json:"gasLimit" gencodec:"required"` - BaseFee *big.Int `json:"baseFeePerGas"` - Timestamp uint64 `json:"timestamp" gencodec:"required"` - Transactions [][]byte `json:"transactions" gencodec:"required"` - BatchHash *common.Hash `json:"batchHash"` + Miner common.Address `json:"miner" gencodec:"required"` + Number uint64 `json:"number" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + BaseFee *big.Int `json:"baseFeePerGas"` + Timestamp uint64 `json:"timestamp" gencodec:"required"` + Transactions [][]byte `json:"transactions" gencodec:"required"` + BatchHash *common.Hash `json:"batchHash"` } // JSON type overrides for SafeL2Data. diff --git a/eth/catalyst/gen_l2_sd.go b/eth/catalyst/gen_l2_sd.go index 7a3b21688..48777b618 100644 --- a/eth/catalyst/gen_l2_sd.go +++ b/eth/catalyst/gen_l2_sd.go @@ -16,6 +16,7 @@ var _ = (*safeL2DataMarshaling)(nil) // MarshalJSON marshals as JSON. func (s SafeL2Data) MarshalJSON() ([]byte, error) { type SafeL2Data struct { + Miner common.Address `json:"miner" gencodec:"required"` Number hexutil.Uint64 `json:"number" gencodec:"required"` GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` BaseFee *hexutil.Big `json:"baseFeePerGas"` @@ -24,6 +25,7 @@ func (s SafeL2Data) MarshalJSON() ([]byte, error) { BatchHash *common.Hash `json:"batchHash"` } var enc SafeL2Data + enc.Miner = s.Miner enc.Number = hexutil.Uint64(s.Number) enc.GasLimit = hexutil.Uint64(s.GasLimit) enc.BaseFee = (*hexutil.Big)(s.BaseFee) @@ -41,6 +43,7 @@ func (s SafeL2Data) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals from JSON. func (s *SafeL2Data) UnmarshalJSON(input []byte) error { type SafeL2Data struct { + Miner *common.Address `json:"miner" gencodec:"required"` Number *hexutil.Uint64 `json:"number" gencodec:"required"` GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` BaseFee *hexutil.Big `json:"baseFeePerGas"` @@ -52,6 +55,10 @@ func (s *SafeL2Data) UnmarshalJSON(input []byte) error { if err := json.Unmarshal(input, &dec); err != nil { return err } + if dec.Miner == nil { + return errors.New("missing required field 'miner' for SafeL2Data") + } + s.Miner = *dec.Miner if dec.Number == nil { return errors.New("missing required field 'number' for SafeL2Data") } diff --git a/eth/catalyst/gen_l2blockparams.go b/eth/catalyst/gen_l2blockparams.go index 332f94f06..70aa9aea6 100644 --- a/eth/catalyst/gen_l2blockparams.go +++ b/eth/catalyst/gen_l2blockparams.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" + "github.com/morph-l2/go-ethereum/common" "github.com/morph-l2/go-ethereum/common/hexutil" ) @@ -15,10 +16,12 @@ var _ = (*assembleL2BlockParamsMarshaling)(nil) func (a AssembleL2BlockParams) MarshalJSON() ([]byte, error) { type AssembleL2BlockParams struct { Number hexutil.Uint64 `json:"number" gencodec:"required"` + Coinbase common.Address `json:"coinbase"` Transactions []hexutil.Bytes `json:"transactions"` } var enc AssembleL2BlockParams enc.Number = hexutil.Uint64(a.Number) + enc.Coinbase = a.Coinbase if a.Transactions != nil { enc.Transactions = make([]hexutil.Bytes, len(a.Transactions)) for k, v := range a.Transactions { @@ -32,6 +35,7 @@ func (a AssembleL2BlockParams) MarshalJSON() ([]byte, error) { func (a *AssembleL2BlockParams) UnmarshalJSON(input []byte) error { type AssembleL2BlockParams struct { Number *hexutil.Uint64 `json:"number" gencodec:"required"` + Coinbase *common.Address `json:"coinbase"` Transactions []hexutil.Bytes `json:"transactions"` } var dec AssembleL2BlockParams @@ -42,6 +46,9 @@ func (a *AssembleL2BlockParams) UnmarshalJSON(input []byte) error { return errors.New("missing required field 'number' for AssembleL2BlockParams") } a.Number = uint64(*dec.Number) + if dec.Coinbase != nil { + a.Coinbase = *dec.Coinbase + } if dec.Transactions != nil { a.Transactions = make([][]byte, len(dec.Transactions)) for k, v := range dec.Transactions { diff --git a/eth/catalyst/l2_api.go b/eth/catalyst/l2_api.go index 45b7a293f..ed49b8890 100644 --- a/eth/catalyst/l2_api.go +++ b/eth/catalyst/l2_api.go @@ -64,7 +64,7 @@ type executionResult struct { } func (api *l2ConsensusAPI) AssembleL2Block(params AssembleL2BlockParams) (*ExecutableL2Data, error) { - log.Info("Assembling block", "block number", params.Number) + log.Info("Assembling block", "block number", params.Number, "coinbase", params.Coinbase) parent := api.eth.BlockChain().CurrentHeader() expectedBlockNumber := parent.Number.Uint64() + 1 if params.Number != expectedBlockNumber { @@ -81,7 +81,7 @@ func (api *l2ConsensusAPI) AssembleL2Block(params AssembleL2BlockParams) (*Execu } start := time.Now() - newBlockResult, err := api.eth.Miner().BuildBlock(parent.Hash(), time.Now(), transactions) + newBlockResult, err := api.eth.Miner().BuildBlock(parent.Hash(), time.Now(), params.Coinbase, transactions) if err != nil { return nil, err } @@ -272,6 +272,7 @@ func (api *l2ConsensusAPI) safeDataToBlock(params SafeL2Data) (*types.Block, err } header := &types.Header{ Number: big.NewInt(int64(params.Number)), + Coinbase: params.Miner, GasLimit: params.GasLimit, Time: params.Timestamp, BaseFee: params.BaseFee, diff --git a/eth/catalyst/l2_api_test.go b/eth/catalyst/l2_api_test.go index fcacb35fd..61369ce4d 100644 --- a/eth/catalyst/l2_api_test.go +++ b/eth/catalyst/l2_api_test.go @@ -111,7 +111,7 @@ func TestValidateL2Block(t *testing.T) { // generic case err = sendTransfer(config, ethService) require.NoError(t, err) - ret, err := ethService.Miner().BuildBlock(ethService.BlockChain().CurrentHeader().Hash(), time.Now(), nil) + ret, err := ethService.Miner().BuildBlock(ethService.BlockChain().CurrentHeader().Hash(), time.Now(), common.Address{}, nil) require.NoError(t, err) block := ret.Block l2Data := ExecutableL2Data{ @@ -170,7 +170,7 @@ func TestNewL2Block(t *testing.T) { err := sendTransfer(config, ethService) require.NoError(t, err) - ret, err := ethService.Miner().BuildBlock(ethService.BlockChain().CurrentHeader().Hash(), time.Now(), nil) + ret, err := ethService.Miner().BuildBlock(ethService.BlockChain().CurrentHeader().Hash(), time.Now(), common.Address{}, nil) block := ret.Block require.NoError(t, err) l2Data := ExecutableL2Data{ @@ -223,7 +223,7 @@ func TestNewSafeL2Block(t *testing.T) { err := sendTransfer(config, ethService) require.NoError(t, err) - ret, err := ethService.Miner().BuildBlock(ethService.BlockChain().CurrentHeader().Hash(), time.Now(), nil) + ret, err := ethService.Miner().BuildBlock(ethService.BlockChain().CurrentHeader().Hash(), time.Now(), common.Address{}, nil) require.NoError(t, err) block := ret.Block l2Data := SafeL2Data{ diff --git a/ethclient/authclient/engine.go b/ethclient/authclient/engine.go index e8d73c202..4df14604b 100644 --- a/ethclient/authclient/engine.go +++ b/ethclient/authclient/engine.go @@ -12,7 +12,7 @@ import ( ) // AssembleL2Block assembles L2 Block used for L2 sequencer to propose a block in L2 consensus progress -func (ec *Client) AssembleL2Block(ctx context.Context, number *big.Int, transactions types.Transactions) (*catalyst.ExecutableL2Data, error) { +func (ec *Client) AssembleL2Block(ctx context.Context, number *big.Int, coinbase common.Address, transactions types.Transactions) (*catalyst.ExecutableL2Data, error) { txs := make([][]byte, 0, len(transactions)) for i, tx := range transactions { bz, err := tx.MarshalBinary() @@ -24,6 +24,7 @@ func (ec *Client) AssembleL2Block(ctx context.Context, number *big.Int, transact var result catalyst.ExecutableL2Data err := ec.c.CallContext(ctx, &result, "engine_assembleL2Block", &catalyst.AssembleL2BlockParams{ Number: number.Uint64(), + Coinbase: coinbase, Transactions: txs, }) return &result, err diff --git a/miner/miner.go b/miner/miner.go index 8328fb991..dc86a7de4 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -167,10 +167,11 @@ func (miner *Miner) getSealingBlockAndState(params *generateParams) (*NewBlockRe } } -func (miner *Miner) BuildBlock(parentHash common.Hash, timestamp time.Time, transactions types.Transactions) (*NewBlockResult, error) { +func (miner *Miner) BuildBlock(parentHash common.Hash, timestamp time.Time, coinbase common.Address, transactions types.Transactions) (*NewBlockResult, error) { return miner.getSealingBlockAndState(&generateParams{ timestamp: uint64(timestamp.Unix()), parentHash: parentHash, + coinbase: coinbase, transactions: transactions, timeout: miner.newBlockTimeout, }) diff --git a/miner/worker.go b/miner/worker.go index 41722acbb..85c17a50c 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -132,6 +132,9 @@ func (miner *Miner) generateWork(genParams *generateParams, interrupt *int32) (* work.gasPool = new(core.GasPool).AddGas(work.header.GasLimit) } + if err := miner.engine.StartHook(miner.chain, work.header, work.state); err != nil { + return nil, err + } fillTxErr := miner.fillTransactions(work, genParams.transactions, interrupt) if fillTxErr != nil && errors.Is(fillTxErr, errBlockInterruptedByTimeout) { log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(miner.newBlockTimeout)) @@ -168,7 +171,8 @@ func (miner *Miner) prepareWork(genParams *generateParams) (*environment, error) timestamp = parent.Time() + 1 } var coinBase common.Address - if genParams.coinbase != (common.Address{}) { + if miner.chainConfig.IsMorph204(timestamp) && // only when Morph204 is activated can we set the coinbase + genParams.coinbase != (common.Address{}) { coinBase = genParams.coinbase } header, err := miner.makeHeader(parent, timestamp, coinBase) diff --git a/params/config.go b/params/config.go index 4a52df94a..8fd6707f5 100644 --- a/params/config.go +++ b/params/config.go @@ -540,6 +540,7 @@ type ChainConfig struct { BernoulliBlock *big.Int `json:"bernoulliBlock,omitempty"` // Bernoulli switch block (nil = no fork, 0 = already on bernoulli) CurieBlock *big.Int `json:"curieBlock,omitempty"` // Curie switch block (nil = no fork, 0 = already on curie) Morph203Time *uint64 `json:"morph203Time,omitempty"` // Morph203Time switch time (nil = no fork, 0 = already on morph203) + Morph204Time *uint64 `json:"morph204Time,omitempty"` // TerminalTotalDifficulty is the amount of total difficulty reached by // the network that triggers the consensus upgrade. @@ -750,6 +751,10 @@ func (c *ChainConfig) IsMorph203(now uint64) bool { return isForkedTime(now, c.Morph203Time) } +func (c *ChainConfig) IsMorph204(now uint64) bool { + return isForkedTime(now, c.Morph204Time) +} + // IsTerminalPoWBlock returns whether the given block is the last block of PoW stage. func (c *ChainConfig) IsTerminalPoWBlock(parentTotalDiff *big.Int, totalDiff *big.Int) bool { if c.TerminalTotalDifficulty == nil { diff --git a/rollup/rcfg/config.go b/rollup/rcfg/config.go index 7b67c0c49..46b6a5fd8 100644 --- a/rollup/rcfg/config.go +++ b/rollup/rcfg/config.go @@ -33,6 +33,18 @@ var ( OverheadSlot = common.BigToHash(big.NewInt(2)) ScalarSlot = common.BigToHash(big.NewInt(3)) + // MorphTokenAddress is the address of the morph token contract + MorphTokenAddress = common.HexToAddress("0x5300000000000000000000000000000000000013") + InflationMintedEpochsSolt = common.BigToHash(big.NewInt(108)) + + // L2StakingAddress is the address of the l2 staking contract + L2StakingAddress = common.HexToAddress("0x5300000000000000000000000000000000000015") + RewardStartedSlot = common.BigToHash(big.NewInt(151)) + RewardStartTimeSlot = common.BigToHash(big.NewInt(152)) + + // SystemAddress is the address of the system + SystemAddress = common.HexToAddress("0x5300000000000000000000000000000000000021") + // New fields added in the Curie hard fork L1BlobBaseFeeSlot = common.BigToHash(big.NewInt(6)) CommitScalarSlot = common.BigToHash(big.NewInt(7)) diff --git a/rollup/tracing/tracing.go b/rollup/tracing/tracing.go index c349366d7..cc3b0c816 100644 --- a/rollup/tracing/tracing.go +++ b/rollup/tracing/tracing.go @@ -11,6 +11,8 @@ import ( "github.com/morph-l2/go-ethereum/common" "github.com/morph-l2/go-ethereum/common/hexutil" "github.com/morph-l2/go-ethereum/consensus" + "github.com/morph-l2/go-ethereum/contracts/l2staking" + "github.com/morph-l2/go-ethereum/contracts/morphtoken" "github.com/morph-l2/go-ethereum/core" "github.com/morph-l2/go-ethereum/core/rawdb" "github.com/morph-l2/go-ethereum/core/state" @@ -53,7 +55,7 @@ func (tw *TracerWrapper) CreateTraceEnvAndGetBlockTrace(chainConfig *params.Chai return nil, err } - return traceEnv.GetBlockTrace(block) + return traceEnv.GetBlockTrace(block, parent) } type TraceEnv struct { @@ -101,6 +103,8 @@ type txTraceTask struct { } func CreateTraceEnvHelper(chainConfig *params.ChainConfig, logConfig *vm.LogConfig, blockCtx vm.BlockContext, startL1QueueIndex uint64, coinbase common.Address, statedb *state.StateDB, rootBefore common.Hash, block *types.Block, commitAfterApply bool) *TraceEnv { + txCount := block.Transactions().Len() + return &TraceEnv{ logConfig: logConfig, commitAfterApply: commitAfterApply, @@ -117,8 +121,8 @@ func CreateTraceEnvHelper(chainConfig *params.ChainConfig, logConfig *vm.LogConf }, Codes: make(map[common.Hash]vm.CodeInfo), ZkTrieTracer: make(map[string]state.ZktrieProofTracer), - ExecutionResults: make([]*types.ExecutionResult, block.Transactions().Len()), - TxStorageTraces: make([]*types.StorageTrace, block.Transactions().Len()), + ExecutionResults: make([]*types.ExecutionResult, txCount), // No extra slot needed, using the actual transaction count + TxStorageTraces: make([]*types.StorageTrace, txCount), StartL1QueueIndex: startL1QueueIndex, } } @@ -173,7 +177,7 @@ func CreateTraceEnv(chainConfig *params.ChainConfig, chainContext core.ChainCont return env, nil } -func (env *TraceEnv) GetBlockTrace(block *types.Block) (*types.BlockTrace, error) { +func (env *TraceEnv) GetBlockTrace(block *types.Block, parent *types.Block) (*types.BlockTrace, error) { // Execute all the transaction contained within the block concurrently var ( txs = block.Transactions() @@ -181,6 +185,12 @@ func (env *TraceEnv) GetBlockTrace(block *types.Block) (*types.BlockTrace, error jobs = make(chan *txTraceTask, len(txs)) errCh = make(chan error, 1) ) + + // Execute StartHook before processing transactions + if err := env.getSystemResult(env.state.Copy(), block, parent); err != nil { + return nil, fmt.Errorf("failed to execute StartHook: %w", err) + } + threads := runtime.NumCPU() if threads > len(txs) { threads = len(txs) @@ -272,6 +282,175 @@ func (env *TraceEnv) GetBlockTrace(block *types.Block) (*types.BlockTrace, error return env.fillBlockTrace(block) } +func (env *TraceEnv) getSystemResult(state *state.StateDB, block *types.Block, parentBlock *types.Block) error { + // Create a context for system-level operations + txctx := &Context{ + BlockHash: block.Hash(), + TxIndex: -1, // Use -1 to indicate this is a system call, not a regular transaction + TxHash: common.Hash{}, // Empty hash for system call + } + + // Create tracer context for system call + tracerContext := tracers.Context{ + BlockHash: block.Hash(), + TxIndex: -1, + TxHash: common.Hash{}, + } + + // Create call tracer + callTracer, err := tracers.New("callTracer", &tracerContext, nil) + if err != nil { + return fmt.Errorf("failed to create callTracer for StartHook: %w", err) + } + + // Create struct logger + structLogger := vm.NewStructLogger(env.logConfig) + tracer := NewMuxTracer(structLogger, callTracer) + + // Create empty transaction context for system call + txContext := vm.TxContext{ + Origin: common.Address{}, + GasPrice: common.Big0, + } + + // Create EVM with tracing enabled + evm := vm.NewEVM(env.blockCtx, txContext, state, env.chainConfig, vm.Config{Debug: true, Tracer: tracer, NoBaseFee: true}) + + // Set transaction context + state.SetTxContext(txctx.TxHash, txctx.TxIndex) + + // Execute StartHook logic + // Check if reward is started + rewardStarted := state.GetState(rcfg.L2StakingAddress, rcfg.RewardStartedSlot).Big() + if rewardStarted.Cmp(common.Big1) == 0 { + // Prepare staking call data + stakingCallData, err := l2staking.PacketData(parentBlock.Coinbase()) + if err != nil { + return fmt.Errorf("failed to pack staking call data: %w", err) + } + + // Call the L2StakingAddress contract from systemAddress + systemAddress := vm.AccountRef(rcfg.SystemAddress) + _, _, err = evm.Call(systemAddress, rcfg.L2StakingAddress, stakingCallData, params.MaxGasLimit, common.Big0) + if err != nil { + return fmt.Errorf("StartHook L2Staking call failed: %w", err) + } + + inflationMintedEpochs := state.GetState(rcfg.MorphTokenAddress, rcfg.InflationMintedEpochsSolt).Big().Uint64() + rewardStartTime := state.GetState(rcfg.L2StakingAddress, rcfg.RewardStartTimeSlot).Big().Uint64() + + // Check if block time crosses reward epoch boundary + // Using a constant for reward epoch (1 day in seconds) + const rewardEpoch uint64 = 86400 + // If a reward epoch boundary is detected, call the MorphToken contract + if block.Time() > rewardStartTime && (block.Time()-rewardStartTime)/rewardEpoch > inflationMintedEpochs { + log.Info("Calling MorphToken contract due to epoch boundary crossing") + + callData, err := morphtoken.PacketData() + if err != nil { + return fmt.Errorf("failed to pack token call data: %w", err) + } + + // Call the MorphToken contract from systemAddress + _, _, err = evm.Call(systemAddress, rcfg.MorphTokenAddress, callData, params.MaxGasLimit, common.Big0) + if err != nil { + return fmt.Errorf("StartHook MorphToken call failed: %w", err) + } + } + } + + // We can simply ignore the call trace result since we're not storing it anymore + // Just check for errors + if _, err := callTracer.GetResult(); err != nil { + return fmt.Errorf("failed to get callTracer result for StartHook: %w", err) + } + + // We don't need to store the StartHook result in ExecutionResults anymore + // since we're collecting the state and storage proofs directly in StorageTrace + + // Collect bytecodes + env.cMu.Lock() + for codeHash, codeInfo := range structLogger.TracedBytecodes() { + if codeHash != (common.Hash{}) { + env.Codes[codeHash] = codeInfo + } + } + env.cMu.Unlock() + + // Collect account proofs + proofAccounts := structLogger.UpdatedAccounts() + proofAccounts[evm.FeeRecipient()] = struct{}{} // Include fee recipient + + for addr := range proofAccounts { + addrStr := addr.String() + + env.pMu.Lock() + if _, existed := env.Proofs[addrStr]; !existed { + proof, err := state.GetProof(addr) + if err != nil { + log.Error("Proof not available for StartHook", "address", addrStr, "error", err) + // Still mark the proofs map with nil array + } + env.Proofs[addrStr] = types.WrapProof(proof) + } + env.pMu.Unlock() + } + + // Collect storage proofs + proofStorages := structLogger.UpdatedStorages() + for addr, keys := range proofStorages { + env.sMu.Lock() + trie, err := state.GetStorageTrieForProof(addr) + if err != nil { + log.Error("Storage trie not available for StartHook", "error", err, "address", addr) + env.sMu.Unlock() + continue + } + zktrieTracer := state.NewProofTracer(trie) + env.sMu.Unlock() + + for key := range keys { + addrStr := addr.String() + keyStr := key.String() + value := state.GetState(addr, key) + isDelete := bytes.Equal(value.Bytes(), common.Hash{}.Bytes()) + + env.sMu.Lock() + m, existed := env.StorageProofs[addrStr] + if !existed { + m = make(map[string][]hexutil.Bytes) + env.StorageProofs[addrStr] = m + } + if zktrieTracer.Available() && !env.ZkTrieTracer[addrStr].Available() { + env.ZkTrieTracer[addrStr] = state.NewProofTracer(trie) + } + + if _, existed := m[keyStr]; !existed { + var proof [][]byte + var err error + if zktrieTracer.Available() { + proof, err = state.GetSecureTrieProof(zktrieTracer, key) + } else { + proof, err = state.GetSecureTrieProof(trie, key) + } + if err != nil { + log.Error("Storage proof not available for StartHook", "error", err, "address", addrStr, "key", keyStr) + } + m[keyStr] = types.WrapProof(proof) + if zktrieTracer.Available() { + if isDelete { + zktrieTracer.MarkDeletion(key) + } + env.ZkTrieTracer[addrStr].Merge(zktrieTracer) + } + } + env.sMu.Unlock() + } + } + + return nil +} + func (env *TraceEnv) getTxResult(state *state.StateDB, index int, block *types.Block) error { tx := block.Transactions()[index] msg, _ := tx.AsMessage(env.signer, block.BaseFee()) @@ -496,6 +675,7 @@ func (env *TraceEnv) getTxResult(state *state.StateDB, index int, block *types.B } getTxResultTracerResultTimer.UpdateSince(tracerResultTimer) + // Store the result at the actual index (no +1 offset needed now) env.ExecutionResults[index] = &types.ExecutionResult{ From: sender, To: receiver, @@ -570,6 +750,7 @@ func (env *TraceEnv) fillBlockTrace(block *types.Block) (*types.BlockTrace, erro if env.chainConfig.ChainID != nil { chainID = env.chainConfig.ChainID.Uint64() } + blockTrace := &types.BlockTrace{ ChainID: chainID, Version: params.ArchiveVersion(params.CommitHash), @@ -586,10 +767,14 @@ func (env *TraceEnv) fillBlockTrace(block *types.Block) (*types.BlockTrace, erro StorageTrace: env.StorageTrace, ExecutionResults: env.ExecutionResults, TxStorageTraces: env.TxStorageTraces, - Transactions: txs, StartL1QueueIndex: env.StartL1QueueIndex, + Transactions: txs, } + // NOTE: The Transactions field is not set here due to type mismatch + // In a real implementation, you would need to properly convert the transactions + // to the expected type. This will need to be fixed based on your actual BlockTrace definition. + blockTrace.Bytecodes = append(blockTrace.Bytecodes, &types.BytecodeTrace{ CodeSize: 0, KeccakCodeHash: codehash.EmptyKeccakCodeHash,