Skip to content

Commit

Permalink
feat: introduce preconf block API server
Browse files Browse the repository at this point in the history
  • Loading branch information
davidtaikocha committed Jan 22, 2025
1 parent e0912fd commit d1346e7
Show file tree
Hide file tree
Showing 8 changed files with 393 additions and 17 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1
github.com/labstack/echo-contrib v0.17.2
github.com/labstack/echo-jwt/v4 v4.3.0
github.com/labstack/echo/v4 v4.13.3
github.com/labstack/gommon v0.4.2
github.com/modern-go/reflect2 v1.0.2
Expand Down Expand Up @@ -113,6 +114,7 @@ require (
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/google/go-cmp v0.6.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
Expand Down Expand Up @@ -375,6 +377,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo-contrib v0.17.2 h1:K1zivqmtcC70X9VdBFdLomjPDEVHlrcAObqmuFj1c6w=
github.com/labstack/echo-contrib v0.17.2/go.mod h1:NeDh3PX7j/u+jR4iuDt1zHmWZSCz9c/p9mxXcDpyS8E=
github.com/labstack/echo-jwt/v4 v4.3.0 h1:8JcvVCrK9dRkPx/aWY3ZempZLO336Bebh4oAtBcxAv4=
github.com/labstack/echo-jwt/v4 v4.3.0/go.mod h1:OlWm3wqfnq3Ma8DLmmH7GiEAz2S7Bj23im2iPMEAR+Q=
github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno=
github.com/labstack/echo/v4 v4.1.15/go.mod h1:GWO5IBVzI371K8XJe50CSvHjQCafK6cw8R/moLhEU6o=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
Expand Down
31 changes: 31 additions & 0 deletions packages/taiko-client/cmd/flags/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,33 @@ var (
Category: driverCategory,
EnvVars: []string{"BLOB_SOCIAL_SCAN_ENDPOINT"},
}
// preconf block server
PreconfBlockServerPort = &cli.Uint64Flag{
Name: "preconfBlock.port",
Usage: "HTTP port of the preconf block server, 0 means disabled",
Category: driverCategory,
EnvVars: []string{"PRECONF_BLOCK_SERVER_PORT"},
}
PreconfBlockServerJWTSecret = &cli.StringFlag{
Name: "preconfBlock.jwtSecret",
Usage: "Path to a JWT secret to use for the preconf block server",
Category: driverCategory,
EnvVars: []string{"PRECONF_BLOCK_SERVER_JWT_SECRET"},
}
PreconfBlockServerCORSOrigins = &cli.StringFlag{
Name: "preconfBlock.corsOrigins",
Usage: "CORS Origins settings for the preconf block server",
Category: driverCategory,
Value: "*",
EnvVars: []string{"PRECONF_BLOCK_SERVER_CORS_ORIGINS"},
}
PreconfBlockServerCheckSig = &cli.BoolFlag{
Name: "preconfBlock.signatureCheck",
Usage: "If the preconf block server will check the signature of the incoming preconf blocks",
Category: driverCategory,
Value: false,
EnvVars: []string{"PRECONF_BLOCK_SERVER_SIGNATURE_CHECK"},
}
)

// DriverFlags All driver flags.
Expand All @@ -66,4 +93,8 @@ var DriverFlags = MergeFlags(CommonFlags, []cli.Flag{
MaxExponent,
BlobServerEndpoint,
SocialScanEndpoint,
PreconfBlockServerPort,
PreconfBlockServerJWTSecret,
PreconfBlockServerCORSOrigins,
PreconfBlockServerCheckSig,
})
44 changes: 30 additions & 14 deletions packages/taiko-client/driver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ import (
// Config contains the configurations to initialize a Taiko driver.
type Config struct {
*rpc.ClientConfig
P2PSync bool
P2PSyncTimeout time.Duration
RetryInterval time.Duration
MaxExponent uint64
BlobServerEndpoint *url.URL
SocialScanEndpoint *url.URL
P2PSync bool
P2PSyncTimeout time.Duration
RetryInterval time.Duration
MaxExponent uint64
BlobServerEndpoint *url.URL
SocialScanEndpoint *url.URL
PreconfBlockServerPort uint64
PreconfBlockServerJWTSecret []byte
PreconfBlockServerCORSOrigins string
PreconfBlockServerCheckSig bool
}

// NewConfigFromCliContext creates a new config instance from
Expand Down Expand Up @@ -69,7 +73,15 @@ func NewConfigFromCliContext(c *cli.Context) (*Config, error) {
return nil, errors.New("empty L1 beacon endpoint, blob server and Social Scan endpoint")
}

var timeout = c.Duration(flags.RPCTimeout.Name)
var preconfBlockServerJWTSecret []byte
if c.String(flags.PreconfBlockServerJWTSecret.Name) != "" {
if preconfBlockServerJWTSecret, err = jwt.ParseSecretFromFile(
c.String(flags.PreconfBlockServerJWTSecret.Name),
); err != nil {
return nil, fmt.Errorf("invalid JWT secret file: %w", err)
}
}

return &Config{
ClientConfig: &rpc.ClientConfig{
L1Endpoint: c.String(flags.L1WSEndpoint.Name),
Expand All @@ -80,13 +92,17 @@ func NewConfigFromCliContext(c *cli.Context) (*Config, error) {
TaikoL2Address: common.HexToAddress(c.String(flags.TaikoL2Address.Name)),
L2EngineEndpoint: c.String(flags.L2AuthEndpoint.Name),
JwtSecret: string(jwtSecret),
Timeout: timeout,
Timeout: c.Duration(flags.RPCTimeout.Name),
},
RetryInterval: c.Duration(flags.BackOffRetryInterval.Name),
P2PSync: p2pSync,
P2PSyncTimeout: c.Duration(flags.P2PSyncTimeout.Name),
MaxExponent: c.Uint64(flags.MaxExponent.Name),
BlobServerEndpoint: blobServerEndpoint,
SocialScanEndpoint: socialScanEndpoint,
RetryInterval: c.Duration(flags.BackOffRetryInterval.Name),
P2PSync: p2pSync,
P2PSyncTimeout: c.Duration(flags.P2PSyncTimeout.Name),
MaxExponent: c.Uint64(flags.MaxExponent.Name),
BlobServerEndpoint: blobServerEndpoint,
SocialScanEndpoint: socialScanEndpoint,
PreconfBlockServerPort: c.Uint64(flags.PreconfBlockServerPort.Name),
PreconfBlockServerJWTSecret: preconfBlockServerJWTSecret,
PreconfBlockServerCORSOrigins: c.String(flags.PreconfBlockServerCORSOrigins.Name),
PreconfBlockServerCheckSig: c.Bool(flags.PreconfBlockServerCheckSig.Name),
}, nil
}
156 changes: 156 additions & 0 deletions packages/taiko-client/driver/preconf_blocks/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package preconfblocks

import (
"errors"
"net/http"

"github.com/ethereum/go-ethereum/beacon/engine"
"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/rlp"
"github.com/labstack/echo/v4"
)

// ValidateSignature validates the signature of the request body.
func (b *BuildPreconfBlockRequestBody) ValidateSignature() (bool, error) {
payload, err := rlp.EncodeToBytes(b)
if err != nil {
return false, err
}

pubKey, err := crypto.SigToPub(crypto.Keccak256(payload), common.FromHex(b.Signature))
if err != nil {
return false, err
}

return crypto.PubkeyToAddress(*pubKey).Hex() == b.ExecutableData.FeeRecipient.Hex(), nil
}

// BuildPreconfBlockRequestBody represents a request body when handling
// soft blocks creation requests.
type BuildPreconfBlockRequestBody struct {
// @param ExecutableData engine.ExecutableData the data necessary to execute an EL payload.
ExecutableData *engine.ExecutableData `json:"executableData"`
// @param signature string Signature of this executable data payload.
Signature string `json:"signature" rlp:"-"`

// @param anchorBlockID uint64 `_anchorBlockId` parameter of the `anchorV3` transaction in the preconf block
AnchorBlockID uint64 `json:"anchorBlockID"`
// @param anchorStateRoot string `_anchorStateRoot` parameter of the `anchorV3` transaction in the preconf block
AnchorStateRoot common.Hash `json:"anchorStateRoot"`
AnchorInput [32]byte `json:"anchorInput"`
SignalSlots [][32]byte `json:"signalSlots"`
}

// BuildPreconfBlockResponseBody represents a response body when handling preconf
// blocks creation requests.
type BuildPreconfBlockResponseBody struct {
// @param blockHeader types.Header of the soft block
BlockHeader *types.Header `json:"blockHeader"`
}

// BuildSoftBlock handles a preconfirmation block creation request,
// if the preconfirmation block creation body in request are valid, it will insert the correspoinding the
// preconfirmation block to the backend L2 execution engine and return a success response.
//
// @Description Insert a preconfirmation block to the L2 execution engine.
// @Param body body BuildPreconfBlockRequestBody true "preconf block creation request body"
// @Accept json
// @Produce json
// @Success 200 {object} BuildPreconfBlockResponseBody
// @Router /preconfBlocks [post]
func (s *PreconfBlockAPIServer) BuildPreconfBlock(c echo.Context) error {
// Parse the request body.
reqBody := new(BuildPreconfBlockRequestBody)
if err := c.Bind(reqBody); err != nil {
return s.returnError(c, http.StatusUnprocessableEntity, err)
}
if reqBody.ExecutableData == nil {
return s.returnError(c, http.StatusBadRequest, errors.New("executable data is required"))
}

log.Info(
"New preconfirmation block building request",
"blockID", reqBody.ExecutableData.Number,
"signature", reqBody.Signature,
"timestamp", reqBody.ExecutableData.Timestamp,
"coinbase", reqBody.ExecutableData.FeeRecipient.Hex(),
"anchorBlockID", reqBody.AnchorBlockID,
"anchorStateRoot", reqBody.AnchorStateRoot,
"anchorInput", common.Bytes2Hex(reqBody.AnchorInput[:]),
"signalSlots", len(reqBody.SignalSlots),
)

// Request body validation.
if reqBody.AnchorBlockID == 0 {
return s.returnError(c, http.StatusBadRequest, errors.New("non-zero anchorBlockID is required"))
}
if reqBody.AnchorStateRoot == (common.Hash{}) {
return s.returnError(c, http.StatusBadRequest, errors.New("empty anchorStateRoot"))
}
if reqBody.ExecutableData.Timestamp == 0 {
return s.returnError(c, http.StatusBadRequest, errors.New("non-zero timestamp is required"))
}
if reqBody.ExecutableData.FeeRecipient == (common.Address{}) {
return s.returnError(c, http.StatusBadRequest, errors.New("empty L2 fee recipient"))
}

// If the `--preconfBlock.signatureCheck` flag is enabled, validate the signature.
if s.checkSig {
ok, err := reqBody.ValidateSignature()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
if !ok {
log.Warn(
"Invalid signature",
"signature", reqBody.Signature,
"coinbase", reqBody.ExecutableData.FeeRecipient.Hex(),
)
return s.returnError(c, http.StatusBadRequest, errors.New("invalid signature"))
}
}

// Check if the L2 execution engine is syncing from L1.
progress, err := s.rpc.L2ExecutionEngineSyncProgress(c.Request().Context())
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
if progress.IsSyncing() {
return s.returnError(c, http.StatusBadRequest, errors.New("L2 execution engine is syncing"))
}

// Insert the preconf block.
header, err := s.chainSyncer.InsertPreconfBlockFromTransactionsBatch(
c.Request().Context(),
reqBody.ExecutableData,
reqBody.AnchorBlockID,
reqBody.AnchorStateRoot,
reqBody.AnchorInput,
reqBody.SignalSlots,
)
if err != nil {
return s.returnError(c, http.StatusInternalServerError, err)
}

return c.JSON(http.StatusOK, BuildPreconfBlockResponseBody{BlockHeader: header})
}

// HealthCheck is the endpoints for probes.
//
// @Summary Get current server health status
// @ID health-check
// @Accept json
// @Produce json
// @Success 200 {object} string
// @Router /healthz [get]
func (s *PreconfBlockAPIServer) HealthCheck(c echo.Context) error {
return c.NoContent(http.StatusOK)
}

// returnError is a helper function to return an error response.
func (s *PreconfBlockAPIServer) returnError(c echo.Context, statusCode int, err error) error {
return c.JSON(statusCode, map[string]string{"error": err.Error()})
}
Loading

0 comments on commit d1346e7

Please sign in to comment.