From 6b41652fc02b7f1fd92788982fde6d4e522a575e Mon Sep 17 00:00:00 2001 From: David Date: Mon, 28 Oct 2024 14:39:36 +0800 Subject: [PATCH] feat: draft implementation of `InsertSoftBlockFromTransactionsBatch` --- .../taiko-client/bindings/encoding/input.go | 13 ++ .../bindings/encoding/protocol_config.go | 11 ++ .../driver/chain_syncer/blob/syncer.go | 185 +++++++++++++++++- .../taiko-client/driver/soft_blocks/api.go | 107 +++++++++- .../taiko-client/driver/soft_blocks/server.go | 16 +- 5 files changed, 318 insertions(+), 14 deletions(-) diff --git a/packages/taiko-client/bindings/encoding/input.go b/packages/taiko-client/bindings/encoding/input.go index 8cfc0e8811a..809df8d537b 100644 --- a/packages/taiko-client/bindings/encoding/input.go +++ b/packages/taiko-client/bindings/encoding/input.go @@ -294,6 +294,9 @@ var ( {Name: "TaikoData.Transition", Type: transitionComponentsType}, {Name: "TaikoData.TierProof", Type: tierProofComponentsType}, } + stringType, _ = abi.NewType("string", "TAIKO_DIFFICULTY", nil) + uint64Type, _ = abi.NewType("uint64", "local.b.numBlocks", nil) + difficultyCalculationInputArgs = abi.Arguments{{Type: stringType}, {Type: uint64Type}} ) // Contract ABIs. @@ -423,6 +426,16 @@ func EncodeProveBlockInput( return b, nil } +// EncodeDifficultCalcutionParams performs the solidity `abi.encode` for the +// `block.difficulty` hash payload. +func EncodeDifficultyCalcutionParams(numBlocks uint64) ([]byte, error) { + b, err := difficultyCalculationInputArgs.Pack("TAIKO_DIFFICULTY", numBlocks) + if err != nil { + return nil, fmt.Errorf("failed to abi.encode `block.difficulty` hash payload, %w", err) + } + return b, nil +} + // UnpackTxListBytes unpacks the input data of a TaikoL1.proposeBlock transaction, and returns the txList bytes. func UnpackTxListBytes(txData []byte) ([]byte, error) { method, err := TaikoL1ABI.MethodById(txData) diff --git a/packages/taiko-client/bindings/encoding/protocol_config.go b/packages/taiko-client/bindings/encoding/protocol_config.go index 27d0c4684ba..256bd088e31 100644 --- a/packages/taiko-client/bindings/encoding/protocol_config.go +++ b/packages/taiko-client/bindings/encoding/protocol_config.go @@ -58,6 +58,7 @@ var ( OntakeForkHeight: 538_304, BaseFeeConfig: bindings.LibSharedDataBaseFeeConfig{ AdjustmentQuotient: 8, + SharingPctg: 75, GasIssuancePerSecond: 5_000_000, MinGasExcess: 1_340_000_000, MaxGasIssuancePerBlock: 600_000_000, @@ -76,3 +77,13 @@ func GetProtocolConfig(chainID uint64) *bindings.TaikoDataConfig { return InternlDevnetProtocolConfig } } + +// EncodeBaseFeeConfig encodes the block.extraData field from the given base fee config. +func EncodeBaseFeeConfig(baseFeeConfig *bindings.LibSharedDataBaseFeeConfig) [32]byte { + var ( + bytes32Value [32]byte + uintValue = new(big.Int).SetUint64(uint64(baseFeeConfig.SharingPctg)) + ) + copy(bytes32Value[32-len(uintValue.Bytes()):], uintValue.Bytes()) + return bytes32Value +} diff --git a/packages/taiko-client/driver/chain_syncer/blob/syncer.go b/packages/taiko-client/driver/chain_syncer/blob/syncer.go index 4acd8e6b7fb..f6d37dc9318 100644 --- a/packages/taiko-client/driver/chain_syncer/blob/syncer.go +++ b/packages/taiko-client/driver/chain_syncer/blob/syncer.go @@ -17,6 +17,7 @@ import ( consensus "github.com/ethereum/go-ethereum/consensus/taiko" "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/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" @@ -106,11 +107,6 @@ func (s *Syncer) ProcessL1Blocks(ctx context.Context) error { } } -// InsertSoftBlock inserts a soft block into the L2 execution engine's blockchain. -func (s *Syncer) InsertSoftBlock(ctx context.Context) (*types.Header, error) { - return nil, nil -} - // processL1Blocks is the inner method which responsible for processing // all new L1 blocks. func (s *Syncer) processL1Blocks(ctx context.Context) error { @@ -492,7 +488,7 @@ func (s *Syncer) createExecutionPayloads( "timestamp", attributes.BlockMetadata.Timestamp, "mixHash", attributes.BlockMetadata.MixHash, "baseFee", utils.WeiToGWei(attributes.BaseFeePerGas), - "extraData", string(attributes.BlockMetadata.ExtraData), + "extraData", common.Bytes2Hex(attributes.BlockMetadata.ExtraData), "l1OriginHeight", attributes.L1Origin.L1BlockHeight, "l1OriginHash", attributes.L1Origin.L1BlockHash, ) @@ -693,3 +689,180 @@ func (s *Syncer) checkReorg( return reorgCheckResult, nil } + +// InsertSoftBlockFromTransactionsBatch inserts a soft block into the L2 execution engine's blockchain +// from the given transactions batch. +func (s *Syncer) InsertSoftBlockFromTransactionsBatch( + // Transactions batch parameters + ctx context.Context, + blockID uint64, + batchID uint64, // TODO(DavidCai): verify batch ID + txListBytes []byte, + batchMarker string, // TODO(DavidCai): handle batch marker + // Block parameters + timestamp uint64, + coinbase common.Address, + // Anchor parameters + anchorBlockID uint64, + anchorStateRoot common.Hash, +) (*types.Header, error) { + parent, err := s.rpc.L2.HeaderByNumber(ctx, new(big.Int).Sub(new(big.Int).SetUint64(blockID), common.Big1)) + if err != nil { + return nil, err + } + + if parent.Number.Uint64()+1 != blockID { + return nil, fmt.Errorf("parent block number (%d) is not equal to blockID - 1 (%d)", parent.Number.Uint64(), blockID) + } + + // Calculate the other block parameters + difficultyHashPaylaod, err := encoding.EncodeDifficultyCalcutionParams(blockID) + if err != nil { + return nil, fmt.Errorf("failed to encode `block.difficulty` calculation parameters: %w", err) + } + + var ( + txList []*types.Transaction + fc = &engine.ForkchoiceStateV1{HeadBlockHash: parent.Hash()} + difficulty = crypto.Keccak256Hash(difficultyHashPaylaod) + protocolConfig = encoding.GetProtocolConfig(s.rpc.L2.ChainID.Uint64()) + extraData = encoding.EncodeBaseFeeConfig(&protocolConfig.BaseFeeConfig) + ) + + if err := rlp.DecodeBytes(txListBytes, &txList); err != nil { + return nil, fmt.Errorf("failed to RLP decode txList bytes: %w", err) + } + + baseFee, err := s.rpc.CalculateBaseFee( + ctx, + parent, + new(big.Int).SetUint64(anchorBlockID), + true, + &protocolConfig.BaseFeeConfig, + timestamp, + ) + if err != nil { + return nil, fmt.Errorf("failed to calculate base fee: %w", err) + } + + // Insert the anchor transaction at the head of the transactions list. + if batchID == 0 { + // Assemble a TaikoL2.anchorV2 transaction. + anchorTx, err := s.anchorConstructor.AssembleAnchorV2Tx( + ctx, + new(big.Int).SetUint64(anchorBlockID), + anchorStateRoot, + parent.GasUsed, + &protocolConfig.BaseFeeConfig, + new(big.Int).SetUint64(blockID), + baseFee, + ) + if err != nil { + return nil, fmt.Errorf("failed to create TaikoL2.anchorV2 transaction: %w", err) + } + + txList = append([]*types.Transaction{anchorTx}, txList...) + if txListBytes, err = rlp.EncodeToBytes(txList); err != nil { + log.Error("Encode txList error", "blockID", blockID, "error", err) + return nil, err + } + } else { + prevSoftBlock, err := s.rpc.L2.BlockByNumber(ctx, new(big.Int).SetUint64(blockID-1)) + if err != nil { + return nil, fmt.Errorf("failed to fetch previous soft block (%d): %w", blockID, err) + } + txList = append(prevSoftBlock.Transactions(), txList...) + } + + attributes := &engine.PayloadAttributes{ + Timestamp: timestamp, + Random: difficulty, + SuggestedFeeRecipient: coinbase, + Withdrawals: []*types.Withdrawal{}, + BlockMetadata: &engine.BlockMetadata{ + Beneficiary: coinbase, + GasLimit: uint64(protocolConfig.BlockMaxGasLimit) + consensus.AnchorGasLimit, + Timestamp: timestamp, + TxList: txListBytes, + MixHash: difficulty, + ExtraData: extraData[:], + }, + BaseFeePerGas: baseFee, + L1Origin: nil, // TODO(DavidCai): check L1 origin + } + + log.Info( + "Soft block payloadAttributes", + "blockID", blockID, + "timestamp", attributes.Timestamp, + "random", attributes.Random, + "suggestedFeeRecipient", attributes.SuggestedFeeRecipient, + "withdrawals", len(attributes.Withdrawals), + "gasLimit", attributes.BlockMetadata.GasLimit, + "timestamp", attributes.BlockMetadata.Timestamp, + "mixHash", attributes.BlockMetadata.MixHash, + "baseFee", utils.WeiToGWei(attributes.BaseFeePerGas), + "extraData", common.Bytes2Hex(attributes.BlockMetadata.ExtraData), + ) + + // Step 1, prepare a payload + fcRes, err := s.rpc.L2Engine.ForkchoiceUpdate(ctx, fc, attributes) + if err != nil { + return nil, fmt.Errorf("failed to update fork choice: %w", err) + } + if fcRes.PayloadStatus.Status != engine.VALID { + return nil, fmt.Errorf("unexpected ForkchoiceUpdate response status: %s", fcRes.PayloadStatus.Status) + } + if fcRes.PayloadID == nil { + return nil, errors.New("empty payload ID") + } + + // Step 2, get the payload + payload, err := s.rpc.L2Engine.GetPayload(ctx, fcRes.PayloadID) + if err != nil { + return nil, fmt.Errorf("failed to get payload: %w", err) + } + + log.Info( + "Soft block payload", + "blockID", blockID, + "baseFee", utils.WeiToGWei(payload.BaseFeePerGas), + "number", payload.Number, + "hash", payload.BlockHash, + "gasLimit", payload.GasLimit, + "gasUsed", payload.GasUsed, + "timestamp", payload.Timestamp, + "withdrawalsHash", payload.WithdrawalsHash, + ) + + // Step 3, execute the payload + execStatus, err := s.rpc.L2Engine.NewPayload(ctx, payload) + if err != nil { + return nil, fmt.Errorf("failed to create a new payload: %w", err) + } + if execStatus.Status != engine.VALID { + return nil, fmt.Errorf("unexpected NewPayload response status: %s", execStatus.Status) + } + + lastVerifiedBlockHash, err := s.rpc.GetLastVerifiedBlockHash(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch last verified block hash: %w", err) + } + + fc = &engine.ForkchoiceStateV1{ + HeadBlockHash: payload.BlockHash, + SafeBlockHash: payload.BlockHash, // TODO(DavidCai): use last canonical block hash. + FinalizedBlockHash: lastVerifiedBlockHash, + } + + // Update the fork choice + fcRes, err = s.rpc.L2Engine.ForkchoiceUpdate(ctx, fc, nil) + if err != nil { + return nil, err + } + if fcRes.PayloadStatus.Status != engine.VALID { + return nil, fmt.Errorf("unexpected ForkchoiceUpdate response status: %s", fcRes.PayloadStatus.Status) + } + + return s.rpc.L2.HeaderByHash(ctx, payload.BlockHash) +} diff --git a/packages/taiko-client/driver/soft_blocks/api.go b/packages/taiko-client/driver/soft_blocks/api.go index b1ed43653a6..f4fc4bfa270 100644 --- a/packages/taiko-client/driver/soft_blocks/api.go +++ b/packages/taiko-client/driver/soft_blocks/api.go @@ -1,10 +1,15 @@ package softblocks import ( + "math/big" "net/http" "github.com/ethereum/go-ethereum/common" "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/rlp" "github.com/labstack/echo/v4" ) @@ -21,8 +26,8 @@ const ( type SoftBlockParams struct { // @param timestamp uint64 Timestamp of the soft block Timestamp uint64 `json:"timestamp"` - // @param coinbase uint64 Coinbase of the soft block - Coinbase string `json:"coinbase"` + // @param coinbase string Coinbase of the soft block + Coinbase common.Address `json:"coinbase"` // @param anchorBlockID uint64 `_anchorBlockId` parameter of the `anchorV2` transaction in soft block AnchorBlockID uint64 `json:"anchorBlockID"` @@ -47,6 +52,26 @@ type TransactionBatch struct { BlockParams *SoftBlockParams `json:"blockParams"` } +// ValidateSignature validates the signature of the transaction batch. +func (b *TransactionBatch) ValidateSignature() (bool, error) { + batchWithoutSig := *b + batchWithoutSig.Signature = "" + + payload, err := rlp.EncodeToBytes(batchWithoutSig) + if err != nil { + return false, err + } + + log.Debug("Validating signature", "payload", payload, "signature", b.Signature) + + pubKey, err := crypto.SigToPub(payload, common.FromHex(b.Signature)) + if err != nil { + return false, err + } + + return crypto.PubkeyToAddress(*pubKey).Hex() == b.BlockParams.Coinbase.Hex(), nil +} + // BuildSoftBlockRequestBody represents a request body when handling // soft blocks creation requests. type BuildSoftBlockRequestBody struct { @@ -78,17 +103,91 @@ type BuildSoftBlockResponseBody struct { // @Success 200 {object} BuildSoftBlockResponseBody // @Router /softBlocks [post] func (s *SoftBlockAPIServer) BuildSoftBlock(c echo.Context) error { + // Parse the request body. + reqBody := new(BuildSoftBlockRequestBody) + if err := c.Bind(reqBody); err != nil { + return c.JSON(http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) + } + + log.Info( + "New soft block building request", + "blockID", reqBody.TransactionBatch.BlockID, + "batchID", reqBody.TransactionBatch.ID, + "batchMarker", reqBody.TransactionBatch.BatchMarker, + "transactionsListBytes", len(reqBody.TransactionBatch.TransactionsList), + "signature", reqBody.TransactionBatch.Signature, + "timestamp", reqBody.TransactionBatch.BlockParams.Timestamp, + "coinbase", reqBody.TransactionBatch.BlockParams.Coinbase, + "anchorBlockID", reqBody.TransactionBatch.BlockParams.AnchorBlockID, + "anchorStateRoot", reqBody.TransactionBatch.BlockParams.AnchorStateRoot, + ) + + // Request body validation. + if reqBody.TransactionBatch.BlockParams == nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "blockParams is required"}) + } + if reqBody.TransactionBatch.BlockParams.AnchorBlockID == 0 { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "non-zero anchorBlockID is required"}) + } + if reqBody.TransactionBatch.BlockParams.AnchorStateRoot == (common.Hash{}) { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "empty anchorStateRoot"}) + } + if reqBody.TransactionBatch.BlockParams.Timestamp == 0 { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "non-zero timestamp is required"}) + } + if reqBody.TransactionBatch.BlockParams.Coinbase == (common.Address{}) { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "empty coinbase"}) + } + + ok, err := reqBody.TransactionBatch.ValidateSignature() + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + if !ok { + log.Warn( + "Invalid signature", + "signature", reqBody.TransactionBatch.Signature, + "coinbase", reqBody.TransactionBatch.BlockParams.Coinbase.Hex(), + ) + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid signature"}) + } + // Check if the L2 execution engine is syncing from L1. progress, err := s.rpc.L2ExecutionEngineSyncProgress(c.Request().Context()) if err != nil { - return err + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } if progress.IsSyncing() { return c.JSON(http.StatusBadRequest, map[string]string{"error": "L2 execution engine is syncing"}) } + var txListBytes []byte + if s.rpc.L2.ChainID.Cmp(params.HeklaNetworkID) == 0 { + txListBytes = s.txListDecompressor.TryDecompressHekla( + new(big.Int).SetUint64(reqBody.TransactionBatch.BlockID), + reqBody.TransactionBatch.TransactionsList, + true, + ) + } else { + txListBytes = s.txListDecompressor.TryDecompress( + new(big.Int).SetUint64(reqBody.TransactionBatch.BlockID), + reqBody.TransactionBatch.TransactionsList, + true, + ) + } + // Insert the soft block. - header, err := s.chainSyncer.BlobSyncer().InsertSoftBlock(c.Request().Context()) + header, err := s.chainSyncer.BlobSyncer().InsertSoftBlockFromTransactionsBatch( + c.Request().Context(), + reqBody.TransactionBatch.BlockID, + reqBody.TransactionBatch.ID, + txListBytes, + string(reqBody.TransactionBatch.BatchMarker), + reqBody.TransactionBatch.BlockParams.Timestamp, + reqBody.TransactionBatch.BlockParams.Coinbase, + reqBody.TransactionBatch.BlockParams.AnchorBlockID, + reqBody.TransactionBatch.BlockParams.AnchorStateRoot, + ) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) } diff --git a/packages/taiko-client/driver/soft_blocks/server.go b/packages/taiko-client/driver/soft_blocks/server.go index 1cb620b0729..f4211604daa 100644 --- a/packages/taiko-client/driver/soft_blocks/server.go +++ b/packages/taiko-client/driver/soft_blocks/server.go @@ -7,7 +7,9 @@ import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "github.com/taikoxyz/taiko-mono/packages/taiko-client/bindings/encoding" chainSyncer "github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/chain_syncer" + txListDecompressor "github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/txlist_decompressor" "github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/rpc" ) @@ -23,16 +25,22 @@ import ( // @license.url https://github.com/taikoxyz/taiko-mono/blob/main/LICENSE.md // SoftBlockAPIServer represents a soft blcok server instance. type SoftBlockAPIServer struct { - echo *echo.Echo - chainSyncer *chainSyncer.L2ChainSyncer - rpc *rpc.Client + echo *echo.Echo + chainSyncer *chainSyncer.L2ChainSyncer + rpc *rpc.Client + txListDecompressor *txListDecompressor.TxListDecompressor } // New creates a new soft blcok server instance. -func New(chainSyncer *chainSyncer.L2ChainSyncer) (*SoftBlockAPIServer, error) { +func New(chainSyncer *chainSyncer.L2ChainSyncer, cli *rpc.Client) (*SoftBlockAPIServer, error) { server := &SoftBlockAPIServer{ echo: echo.New(), chainSyncer: chainSyncer, + txListDecompressor: txListDecompressor.NewTxListDecompressor( + uint64(encoding.GetProtocolConfig(cli.L2.ChainID.Uint64()).BlockMaxGasLimit), + rpc.BlockMaxTxListBytes, + cli.L2.ChainID, + ), } server.echo.HideBanner = true