Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions node/core/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,3 +418,202 @@ func (e *Executor) getParamsAndValsAtHeight(height int64) (*tmproto.BatchParams,
func (e *Executor) L2Client() *types.RetryableClient {
return e.l2Client
}

// ============================================================================
// L2NodeV2 interface implementation for sequencer mode
// ============================================================================

// RequestBlockDataV2 requests block data based on parent hash.
// This differs from RequestBlockData which uses height.
// Using parent hash allows for proper fork chain handling in the future.
func (e *Executor) RequestBlockDataV2(parentHashBytes []byte) (*l2node.BlockV2, bool, error) {
if e.l1MsgReader == nil {
return nil, false, fmt.Errorf("RequestBlockDataV2 is not allowed to be called")
}
parentHash := common.BytesToHash(parentHashBytes)

// Read L1 messages
fromIndex := e.nextL1MsgIndex
l1Messages := e.l1MsgReader.ReadL1MessagesInRange(fromIndex, fromIndex+e.maxL1MsgNumPerBlock-1)
transactions := make(eth.Transactions, len(l1Messages))

collectedL1Msgs := false
if len(l1Messages) > 0 {
queueIndex := fromIndex
for i, l1Message := range l1Messages {
transaction := eth.NewTx(&l1Message.L1MessageTx)
transactions[i] = transaction
if queueIndex != l1Message.QueueIndex {
e.logger.Error("unexpected l1message queue index", "expected", queueIndex, "actual", l1Message.QueueIndex)
return nil, false, types.ErrInvalidL1MessageOrder
}
queueIndex++
}
collectedL1Msgs = true
}

// Call geth to assemble block based on parent hash
l2Block, err := e.l2Client.AssembleL2BlockV2(context.Background(), parentHash, transactions)
if err != nil {
e.logger.Error("failed to assemble block v2", "parentHash", parentHash.Hex(), "error", err)
return nil, false, err
}

e.logger.Info("AssembleL2BlockV2 success ",
"number", l2Block.Number,
"hash", l2Block.Hash.Hex(),
"parentHash", l2Block.ParentHash.Hex(),
"tx length", len(l2Block.Transactions),
"collectedL1Msgs", collectedL1Msgs,
)

return executableL2DataToBlockV2(l2Block), collectedL1Msgs, nil
}

// ApplyBlockV2 applies a block to the L2 execution layer.
// This is used in sequencer mode after block validation.
func (e *Executor) ApplyBlockV2(block *l2node.BlockV2) error {
// Convert BlockV2 to ExecutableL2Data for geth
execBlock := blockV2ToExecutableL2Data(block)

// Check if block is already applied
height, err := e.l2Client.BlockNumber(context.Background())
if err != nil {
return err
}

if execBlock.Number <= height {
e.logger.Info("ignore it, the block was already applied", "block number", execBlock.Number)
return nil
}

// We only accept continuous blocks
if execBlock.Number > height+1 {
return types.ErrWrongBlockNumber
}

// Apply the block (no batch hash in sequencer mode for now)
err = e.l2Client.NewL2Block(context.Background(), execBlock, nil)
if err != nil {
e.logger.Error("failed to apply block v2", "error", err)
return err
}

// Update L1 message index
e.updateNextL1MessageIndex(execBlock)

e.metrics.Height.Set(float64(execBlock.Number))
e.logger.Info("ApplyBlockV2 success", "number", execBlock.Number, "hash", execBlock.Hash.Hex())

return nil
}

// GetBlockByNumber retrieves a block by its number from the L2 execution layer.
// Uses standard eth_getBlockByNumber JSON-RPC.
func (e *Executor) GetBlockByNumber(height uint64) (*l2node.BlockV2, error) {
block, err := e.l2Client.BlockByNumber(context.Background(), big.NewInt(int64(height)))
if err != nil {
e.logger.Error("failed to get block by number", "height", height, "error", err)
return nil, err
}
return ethBlockToBlockV2(block)
}

// GetLatestBlockV2 returns the latest block from the L2 execution layer.
// Uses standard eth_getBlockByNumber JSON-RPC with nil (latest).
func (e *Executor) GetLatestBlockV2() (*l2node.BlockV2, error) {
block, err := e.l2Client.BlockByNumber(context.Background(), nil)
if err != nil {
e.logger.Error("failed to get latest block", "error", err)
return nil, err
}
return ethBlockToBlockV2(block)
}

// ==================== Type Conversion Functions ====================

// ethBlockToBlockV2 converts eth.Block to BlockV2
func ethBlockToBlockV2(block *eth.Block) (*l2node.BlockV2, error) {
if block == nil {
return nil, fmt.Errorf("block is nil")
}
header := block.Header()

// Encode transactions using MarshalBinary (handles EIP-2718 typed transactions correctly)
// Initialize as empty slice (not nil) to ensure JSON serialization produces [] instead of null
txs := make([][]byte, 0, len(block.Transactions()))
for _, tx := range block.Transactions() {
bz, err := tx.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("failed to marshal tx, error: %v", err)
}
txs = append(txs, bz)
}

return &l2node.BlockV2{
ParentHash: header.ParentHash,
Miner: header.Coinbase,
Number: header.Number.Uint64(),
GasLimit: header.GasLimit,
BaseFee: header.BaseFee,
Timestamp: header.Time,
Transactions: txs,
StateRoot: header.Root,
GasUsed: header.GasUsed,
ReceiptRoot: header.ReceiptHash,
LogsBloom: header.Bloom.Bytes(),
NextL1MessageIndex: header.NextL1MsgIndex,
Hash: block.Hash(),
}, nil
}

// blockV2ToExecutableL2Data converts BlockV2 to ExecutableL2Data
func blockV2ToExecutableL2Data(block *l2node.BlockV2) *catalyst.ExecutableL2Data {
if block == nil {
return nil
}
// Ensure Transactions is not nil (JSON requires [] not null)
txs := block.Transactions
if txs == nil {
txs = make([][]byte, 0)
}
return &catalyst.ExecutableL2Data{
ParentHash: block.ParentHash,
Miner: block.Miner,
Number: block.Number,
GasLimit: block.GasLimit,
BaseFee: block.BaseFee,
Timestamp: block.Timestamp,
Transactions: txs,
StateRoot: block.StateRoot,
GasUsed: block.GasUsed,
ReceiptRoot: block.ReceiptRoot,
LogsBloom: block.LogsBloom,
WithdrawTrieRoot: block.WithdrawTrieRoot,
NextL1MessageIndex: block.NextL1MessageIndex,
Hash: block.Hash,
}
}

// executableL2DataToBlockV2 converts ExecutableL2Data to BlockV2
func executableL2DataToBlockV2(data *catalyst.ExecutableL2Data) *l2node.BlockV2 {
if data == nil {
return nil
}
return &l2node.BlockV2{
ParentHash: data.ParentHash,
Miner: data.Miner,
Number: data.Number,
GasLimit: data.GasLimit,
BaseFee: data.BaseFee,
Timestamp: data.Timestamp,
Transactions: data.Transactions,
StateRoot: data.StateRoot,
GasUsed: data.GasUsed,
ReceiptRoot: data.ReceiptRoot,
LogsBloom: data.LogsBloom,
WithdrawTrieRoot: data.WithdrawTrieRoot,
NextL1MessageIndex: data.NextL1MessageIndex,
Hash: data.Hash,
}
}
43 changes: 42 additions & 1 deletion node/types/retryable_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,25 @@ func (rc *RetryableClient) HeaderByNumber(ctx context.Context, blockNumber *big.
if retryErr := backoff.Retry(func() error {
resp, respErr := rc.ethClient.HeaderByNumber(ctx, blockNumber)
if respErr != nil {
rc.logger.Info("failed to call BlockNumber", "error", respErr)
rc.logger.Info("failed to call HeaderByNumber", "error", respErr)
if retryableError(respErr) {
return respErr
}
err = respErr
}
ret = resp
return nil
}, rc.b); retryErr != nil {
return nil, retryErr
}
return
}

func (rc *RetryableClient) BlockByNumber(ctx context.Context, blockNumber *big.Int) (ret *eth.Block, err error) {
if retryErr := backoff.Retry(func() error {
resp, respErr := rc.ethClient.BlockByNumber(ctx, blockNumber)
if respErr != nil {
rc.logger.Info("failed to call BlockByNumber", "error", respErr)
if retryableError(respErr) {
return respErr
}
Expand Down Expand Up @@ -233,3 +251,26 @@ func retryableError(err error) bool {
// strings.Contains(err.Error(), Timeout)
return !strings.Contains(err.Error(), DiscontinuousBlockError)
}

// ============================================================================
// L2NodeV2 methods for sequencer mode
// ============================================================================

// AssembleL2BlockV2 assembles a L2 block based on parent hash.
func (rc *RetryableClient) AssembleL2BlockV2(ctx context.Context, parentHash common.Hash, transactions eth.Transactions) (ret *catalyst.ExecutableL2Data, err error) {
if retryErr := backoff.Retry(func() error {
resp, respErr := rc.authClient.AssembleL2BlockV2(ctx, parentHash, transactions)
if respErr != nil {
rc.logger.Info("failed to AssembleL2BlockV2", "error", respErr)
if retryableError(respErr) {
return respErr
}
err = respErr
}
ret = resp
return nil
}, rc.b); retryErr != nil {
return nil, retryErr
}
return
}
26 changes: 26 additions & 0 deletions ops/docker-sequencer-test/Dockerfile.l2-geth-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Build Geth for Sequencer Test
# Build context should be bitget/ (parent of morph)
FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu AS builder

# Copy local go-ethereum (not submodule)
COPY ./go-ethereum /go-ethereum
WORKDIR /go-ethereum

# Build geth
RUN go run build/ci.go install ./cmd/geth

# Runtime stage
FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu

RUN apt-get -qq update && apt-get -qq install -y --no-install-recommends \
ca-certificates bash curl \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /go-ethereum/build/bin/geth /usr/local/bin/
COPY ./morph/ops/docker-sequencer-test/entrypoint-l2.sh /entrypoint.sh

VOLUME ["/db"]

ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]

EXPOSE 8545 8546 8551 30303 30303/udp
47 changes: 47 additions & 0 deletions ops/docker-sequencer-test/Dockerfile.l2-node-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Build Stage
FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu AS builder

# First: Copy only go.mod/go.sum files to cache dependencies
# Order matters for cache efficiency

# Copy go-ethereum dependency files
COPY ./go-ethereum/go.mod ./go-ethereum/go.sum /bitget/go-ethereum/

# Copy tendermint dependency files
COPY ./tendermint/go.mod ./tendermint/go.sum /bitget/tendermint/

# Copy morph go.work and all module dependency files
COPY ./morph/go.work ./morph/go.work.sum /bitget/morph/
COPY ./morph/node/go.mod ./morph/node/go.sum /bitget/morph/node/
COPY ./morph/bindings/go.mod ./morph/bindings/go.sum /bitget/morph/bindings/
COPY ./morph/contracts/go.mod ./morph/contracts/go.sum /bitget/morph/contracts/
COPY ./morph/oracle/go.mod ./morph/oracle/go.sum /bitget/morph/oracle/
COPY ./morph/tx-submitter/go.mod ./morph/tx-submitter/go.sum /bitget/morph/tx-submitter/
COPY ./morph/ops/l2-genesis/go.mod ./morph/ops/l2-genesis/go.sum /bitget/morph/ops/l2-genesis/
COPY ./morph/ops/tools/go.mod ./morph/ops/tools/go.sum /bitget/morph/ops/tools/
COPY ./morph/token-price-oracle/go.mod ./morph/token-price-oracle/go.sum /bitget/morph/token-price-oracle/

# Download dependencies (this layer is cached if go.mod/go.sum don't change)
WORKDIR /bitget/morph/node
RUN go mod download -x

# Now copy all source code
COPY ./go-ethereum /bitget/go-ethereum
COPY ./tendermint /bitget/tendermint
COPY ./morph /bitget/morph

# Build (no need to download again, just compile)
WORKDIR /bitget/morph/node
RUN make build

# Final Stage
FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu

RUN apt-get -qq update \
&& apt-get -qq install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /bitget/morph/node/build/bin/tendermint /usr/local/bin/
COPY --from=builder /bitget/morph/node/build/bin/morphnode /usr/local/bin/

CMD ["morphnode", "--home", "/data"]
1 change: 1 addition & 0 deletions ops/docker-sequencer-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This directory is intended for Docker testing purposes only and may be removed in the future.
43 changes: 43 additions & 0 deletions ops/docker-sequencer-test/docker-compose.override.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Override file to use test images
# Copy this to ops/docker/docker-compose.override.yml before running test
version: '3.8'

services:
morph-geth-0:
image: morph-geth-test:latest
build:
context: ../..
dockerfile: ops/docker-sequencer-test/Dockerfile.l2-geth-test

morph-geth-1:
image: morph-geth-test:latest

morph-geth-2:
image: morph-geth-test:latest

# morph-geth-3:
# image: morph-geth-test:latest

node-0:
image: morph-node-test:latest
build:
context: ../..
dockerfile: ops/docker-sequencer-test/Dockerfile.l2-node-test
environment:
- SEQUENCER_PRIVATE_KEY=${SEQUENCER_PRIVATE_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80}

node-1:
image: morph-node-test:latest

node-2:
image: morph-node-test:latest

# node-3:
# image: morph-node-test:latest

sentry-geth-0:
image: morph-geth-test:latest

sentry-node-0:
image: morph-node-test:latest

Loading
Loading