diff --git a/packages/guardian-prover-health-check/.default.env b/packages/guardian-prover-health-check/.default.env index cef98351a0b..c8b2ee5a5db 100644 --- a/packages/guardian-prover-health-check/.default.env +++ b/packages/guardian-prover-health-check/.default.env @@ -1,5 +1,5 @@ HTTP_PORT=4103 -PROMETHEUS_HTTP_PORT=6062 +METRICS_HTTP_PORT=6062 DATABASE_USER=root DATABASE_PASSWORD=root DATABASE_NAME=healthcheck @@ -7,7 +7,7 @@ DATABASE_HOST=localhost:3306 DATABASE_MAX_IDLE_CONNS=50 DATABASE_MAX_OPEN_CONNS=3000 DATABASE_CONN_MAX_LIFETIME_IN_MS=100000 -GUARDIAN_PROVER_CONTRACT_ADDRESS=0x0E801D84Fa97b50751Dbf25036d067dCf18858bF -GUARDIAN_PROVER_ENDPOINTS=https://guardian-prover-1.internal.taiko.xyz,https://guardian-prover-2.internal.taiko.xyz,https://guardian-prover-3.internal.taiko.xyz,https://guardian-prover-4.internal.taiko.xyz,https://guardian-prover-5.internal.taiko.xyz -RPC_URL=wss://l1ws.internal.taiko.xyz +GUARDIAN_PROVER_CONTRACT_ADDRESS=0xDf8038e9f4535040D7421A89ead398b3A38366EC +L1_RPC_URL=wss://l1ws.internal.taiko.xyz +L2_RPC_URL=wss://ws.internal.taiko.xyz INTERVAL=12s \ No newline at end of file diff --git a/packages/guardian-prover-health-check/cmd/flags/healthcheck.go b/packages/guardian-prover-health-check/cmd/flags/healthcheck.go index 78f913e43b2..a0861296d69 100644 --- a/packages/guardian-prover-health-check/cmd/flags/healthcheck.go +++ b/packages/guardian-prover-health-check/cmd/flags/healthcheck.go @@ -8,13 +8,6 @@ import ( // required flags var ( - GuardianProverEndpoints = &cli.StringSliceFlag{ - Name: "guardianProverEndpoints", - Usage: "List of guardian prover endpoints", - Category: healthCheckCategory, - EnvVars: []string{"GUARDIAN_PROVER_ENDPOINTS"}, - Required: true, - } GuardianProverContractAddress = &cli.StringFlag{ Name: "guardianProverContractAddress", Usage: "Address of the GuardianProver contract", @@ -22,11 +15,18 @@ var ( EnvVars: []string{"GUARDIAN_PROVER_CONTRACT_ADDRESS"}, Required: true, } - RPCUrl = &cli.StringFlag{ - Name: "rpcUrl", - Usage: "RPC Url", + L1RPCUrl = &cli.StringFlag{ + Name: "l1RpcUrl", + Usage: "L1 RPC Url", + Category: healthCheckCategory, + EnvVars: []string{"L1_RPC_URL"}, + Required: true, + } + L2RPCUrl = &cli.StringFlag{ + Name: "l2RpcUrl", + Usage: "L2 RPC Url", Category: healthCheckCategory, - EnvVars: []string{"RPC_URL"}, + EnvVars: []string{"L2_RPC_URL"}, Required: true, } ) @@ -52,21 +52,13 @@ var ( Value: "*", EnvVars: []string{"HTTP_CORS_ORIGINS"}, } - Interval = &cli.DurationFlag{ - Name: "interval", - Usage: "Health check interval duration", - Category: healthCheckCategory, - Value: 12 * time.Second, - EnvVars: []string{"INTERVAL"}, - } ) var HealthCheckFlags = MergeFlags(CommonFlags, []cli.Flag{ HTTPPort, CORSOrigins, Backoff, - GuardianProverEndpoints, GuardianProverContractAddress, - Interval, - RPCUrl, + L1RPCUrl, + L2RPCUrl, }) diff --git a/packages/guardian-prover-health-check/guardianprover.go b/packages/guardian-prover-health-check/guardianprover.go index d206ee42acb..fd9d29aee5c 100644 --- a/packages/guardian-prover-health-check/guardianprover.go +++ b/packages/guardian-prover-health-check/guardianprover.go @@ -1,14 +1,44 @@ package guardianproverhealthcheck import ( + "encoding/base64" + "errors" "math/big" - "net/url" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" ) type GuardianProver struct { - Address common.Address - ID *big.Int - Endpoint *url.URL + Address common.Address + ID *big.Int +} + +func SignatureToGuardianProver( + msg []byte, + b64EncodedSig string, + guardianProvers []GuardianProver, +) (*GuardianProver, error) { + b64DecodedSig, err := base64.StdEncoding.DecodeString(b64EncodedSig) + if err != nil { + return nil, err + } + + // recover the public key from the signature + r, err := crypto.SigToPub(msg, b64DecodedSig) + if err != nil { + return nil, err + } + + // convert it to address type + recoveredAddr := crypto.PubkeyToAddress(*r) + + // see if any of our known guardian provers have that recovered address + for _, p := range guardianProvers { + if recoveredAddr.Cmp(p.Address) == 0 { + return &p, nil + } + } + + return nil, errors.New("signature does not recover to known guardian prover") } diff --git a/packages/guardian-prover-health-check/healthchecker/config.go b/packages/guardian-prover-health-check/healthchecker/config.go index 8a8eb2b648f..c16c2bbbdbf 100644 --- a/packages/guardian-prover-health-check/healthchecker/config.go +++ b/packages/guardian-prover-health-check/healthchecker/config.go @@ -3,7 +3,6 @@ package healthchecker import ( "database/sql" "strings" - "time" "github.com/taikoxyz/taiko-mono/packages/guardian-prover-health-check/cmd/flags" "github.com/taikoxyz/taiko-mono/packages/guardian-prover-health-check/db" @@ -30,10 +29,9 @@ type Config struct { CORSOrigins []string Backoff uint64 HTTPPort uint64 - GuardianProverEndpoints []string GuardianProverContractAddress string - RPCUrl string - Interval time.Duration + L1RPCUrl string + L2RPCUrl string OpenDBFunc func() (DB, error) } @@ -48,11 +46,10 @@ func NewConfigFromCliContext(c *cli.Context) (*Config, error) { DatabaseMaxOpenConns: c.Uint64(flags.DatabaseMaxOpenConns.Name), DatabaseMaxConnLifetime: c.Uint64(flags.DatabaseConnMaxLifetime.Name), CORSOrigins: strings.Split(c.String(flags.CORSOrigins.Name), ","), - GuardianProverEndpoints: c.StringSlice(flags.GuardianProverEndpoints.Name), GuardianProverContractAddress: c.String(flags.GuardianProverContractAddress.Name), - RPCUrl: c.String(flags.RPCUrl.Name), + L1RPCUrl: c.String(flags.L1RPCUrl.Name), + L2RPCUrl: c.String(flags.L2RPCUrl.Name), HTTPPort: c.Uint64(flags.HTTPPort.Name), - Interval: c.Duration(flags.Interval.Name), OpenDBFunc: func() (DB, error) { return db.OpenDBConnection(db.DBConnectionOpts{ Name: c.String(flags.DatabaseUsername.Name), diff --git a/packages/guardian-prover-health-check/healthchecker/config_test.go b/packages/guardian-prover-health-check/healthchecker/config_test.go index 6776e04c064..b7455dcfd4e 100644 --- a/packages/guardian-prover-health-check/healthchecker/config_test.go +++ b/packages/guardian-prover-health-check/healthchecker/config_test.go @@ -1,7 +1,6 @@ package healthchecker import ( - "strings" "testing" "github.com/stretchr/testify/assert" @@ -12,7 +11,6 @@ import ( var ( guardianProverAddress = "0x63FaC9201494f0bd17B9892B9fae4d52fe3BD377" - guardianProverEndpoints = "http://endpoint.com,http://endpoint2.com" databaseMaxIdleConns = "10" databaseMaxOpenConns = "10" databaseMaxConnLifetime = "30" @@ -40,8 +38,8 @@ func TestNewConfigFromCliContext(t *testing.T) { assert.Equal(t, "dbpass", c.DatabasePassword) assert.Equal(t, "dbname", c.DatabaseName) assert.Equal(t, "dbhost", c.DatabaseHost) - assert.Equal(t, "rpcUrl", c.RPCUrl) - assert.Equal(t, strings.Split(guardianProverEndpoints, ","), c.GuardianProverEndpoints) + assert.Equal(t, "l1RpcUrl", c.L1RPCUrl) + assert.Equal(t, "l2RpcUrl", c.L2RPCUrl) assert.Equal(t, guardianProverAddress, c.GuardianProverContractAddress) assert.Equal(t, []string{"*"}, c.CORSOrigins) assert.Equal(t, uint64(10), c.DatabaseMaxIdleConns) @@ -64,13 +62,13 @@ func TestNewConfigFromCliContext(t *testing.T) { "--" + flags.DatabasePassword.Name, "dbpass", "--" + flags.DatabaseHost.Name, "dbhost", "--" + flags.DatabaseName.Name, "dbname", - "--" + flags.RPCUrl.Name, "rpcUrl", + "--" + flags.L1RPCUrl.Name, "l1RpcUrl", + "--" + flags.L2RPCUrl.Name, "l2RpcUrl", "--" + flags.CORSOrigins.Name, "*", "--" + flags.DatabaseMaxOpenConns.Name, databaseMaxOpenConns, "--" + flags.DatabaseMaxIdleConns.Name, databaseMaxIdleConns, "--" + flags.DatabaseConnMaxLifetime.Name, databaseMaxConnLifetime, "--" + flags.HTTPPort.Name, HTTPPort, "--" + flags.GuardianProverContractAddress.Name, guardianProverAddress, - "--" + flags.GuardianProverEndpoints.Name, guardianProverEndpoints, })) } diff --git a/packages/guardian-prover-health-check/healthchecker/healthchecker.go b/packages/guardian-prover-health-check/healthchecker/healthchecker.go index 9d4c10a9314..52da0277541 100644 --- a/packages/guardian-prover-health-check/healthchecker/healthchecker.go +++ b/packages/guardian-prover-health-check/healthchecker/healthchecker.go @@ -2,20 +2,14 @@ package healthchecker import ( "context" - "encoding/base64" - "encoding/json" "errors" "fmt" - "io" "log/slog" "math/big" "net/http" - "net/url" - "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/labstack/echo/v4" guardianproverhealthcheck "github.com/taikoxyz/taiko-mono/packages/guardian-prover-health-check" @@ -25,15 +19,10 @@ import ( "github.com/urfave/cli/v2" ) -var ( - msg = crypto.Keccak256Hash([]byte("HEART_BEAT")).Bytes() -) - type HealthChecker struct { ctx context.Context cancelCtx context.CancelFunc healthCheckRepo guardianproverhealthcheck.HealthCheckRepository - interval time.Duration guardianProverContract *guardianprover.GuardianProver numGuardians uint64 guardianProvers []guardianproverhealthcheck.GuardianProver @@ -41,11 +30,6 @@ type HealthChecker struct { httpPort uint64 } -type healthCheckResponse struct { - ProverAddress string `json:"prover"` - HeartBeatSignature string `json:"heartBeatSignature"` -} - func (h *HealthChecker) Name() string { return "healthchecker" } @@ -78,19 +62,29 @@ func InitFromConfig(ctx context.Context, h *HealthChecker, cfg *Config) (err err return err } + signedBlockRepo, err := repo.NewSignedBlockRepository(db) + if err != nil { + return err + } + statRepo, err := repo.NewStatRepository(db) if err != nil { return err } - ethClient, err := ethclient.Dial(cfg.RPCUrl) + l1EthClient, err := ethclient.Dial(cfg.L1RPCUrl) + if err != nil { + return err + } + + l2EthClient, err := ethclient.Dial(cfg.L2RPCUrl) if err != nil { return err } guardianProverContract, err := guardianprover.NewGuardianProver( common.HexToAddress(cfg.GuardianProverContractAddress), - ethClient, + l1EthClient, ) if err != nil { return err @@ -114,22 +108,18 @@ func InitFromConfig(ctx context.Context, h *HealthChecker, cfg *Config) (err err return err } - endpoint, err := url.Parse(cfg.GuardianProverEndpoints[i]) - if err != nil { - return err - } - guardianProvers = append(guardianProvers, guardianproverhealthcheck.GuardianProver{ - Address: guardianAddress, - ID: guardianId, - Endpoint: endpoint, + Address: guardianAddress, + ID: guardianId, }) } h.httpSrv, err = hchttp.NewServer(hchttp.NewServerOpts{ Echo: echo.New(), + EthClient: l2EthClient, HealthCheckRepo: healthCheckRepo, StatRepo: statRepo, + SignedBlockRepo: signedBlockRepo, GuardianProvers: guardianProvers, }) @@ -140,7 +130,6 @@ func InitFromConfig(ctx context.Context, h *HealthChecker, cfg *Config) (err err h.guardianProvers = guardianProvers h.numGuardians = numGuardians.Uint64() h.healthCheckRepo = healthCheckRepo - h.interval = cfg.Interval h.guardianProverContract = guardianProverContract h.httpPort = cfg.HTTPPort @@ -156,109 +145,5 @@ func (h *HealthChecker) Start() error { } }() - go h.checkGuardianProversOnInterval() - return nil } - -func (h *HealthChecker) checkGuardianProversOnInterval() { - t := time.NewTicker(h.interval) - - for { - select { - case <-h.ctx.Done(): - return - case <-t.C: - for _, g := range h.guardianProvers { - resp, recoveredAddr, err := h.checkGuardianProver(g) - if err != nil { - slog.Error( - "error checking guardian prover endpoint", - "endpoint", g.Endpoint, - "id", g.ID, - "address", g.Address.Hex(), - "recoveredAddr", recoveredAddr, - "error", err, - ) - } - - var sig string = "" - - if resp != nil { - sig = resp.HeartBeatSignature - } - - err = h.healthCheckRepo.Save( - guardianproverhealthcheck.SaveHealthCheckOpts{ - GuardianProverID: g.ID.Uint64(), - Alive: sig != "", - ExpectedAddress: g.Address.Hex(), - RecoveredAddress: recoveredAddr, - SignedResponse: sig, - }, - ) - - if err != nil { - slog.Error("error saving failed health check to database", - "endpoint", g.Endpoint, - "id", g.ID, - "address", g.Address.Hex(), - "recoveredAddr", recoveredAddr, - "sig", sig, - "error", err, - ) - } else { - slog.Info("saved health check to database", - "endpoint", g.Endpoint, - "id", g.ID, - "address", g.Address.Hex(), - "recoveredAddr", recoveredAddr, - "sig", sig, - ) - } - } - } - } -} - -func (h *HealthChecker) checkGuardianProver( - g guardianproverhealthcheck.GuardianProver, -) (*healthCheckResponse, string, error) { - slog.Info("checking guardian prover", "id", g.ID, "endpoint", g.Endpoint) - - healthCheckResponse := &healthCheckResponse{} - - resp, err := http.Get(g.Endpoint.String() + "/status") - if err != nil { - // save fail to db - return healthCheckResponse, "", err - } - - b, err := io.ReadAll(resp.Body) - if err != nil { - return healthCheckResponse, "", err - } - - if err := json.Unmarshal(b, healthCheckResponse); err != nil { - return healthCheckResponse, "", err - } - - if g.Address.Cmp(common.HexToAddress(healthCheckResponse.ProverAddress)) != 0 { - slog.Error("address mismatch", "expected", g.Address.Hex(), "received", healthCheckResponse.ProverAddress) - return healthCheckResponse, "", errors.New("prover address provided was not the address expected") - } - - b64DecodedSig, err := base64.StdEncoding.DecodeString(healthCheckResponse.HeartBeatSignature) - if err != nil { - return healthCheckResponse, "", err - } - - pubKey, err := crypto.SigToPub(msg, b64DecodedSig) - if err != nil { - return healthCheckResponse, "", err - } - - recoveredAddr := crypto.PubkeyToAddress(*pubKey) - - return healthCheckResponse, recoveredAddr.Hex(), nil -} diff --git a/packages/guardian-prover-health-check/http/get_blocks.go b/packages/guardian-prover-health-check/http/get_blocks.go deleted file mode 100644 index d11978a8fa8..00000000000 --- a/packages/guardian-prover-health-check/http/get_blocks.go +++ /dev/null @@ -1,90 +0,0 @@ -package http - -import ( - "encoding/json" - "io" - "net/http" - - "github.com/ethereum/go-ethereum/common" - echo "github.com/labstack/echo/v4" -) - -type signedBlock struct { - BlockID uint64 `json:"blockID"` - BlockHash string `json:"blockHash"` - Signature string `json:"signature"` - Prover common.Address `json:"proverAddress"` -} - -type guardianProverInfo struct { - GuardianProverID uint64 `json:"guardianProverID"` - signedBlocks []signedBlock -} - -type block struct { - BlockHash string `json:"blockHash"` - Signature string `json:"signature"` - GuardianProverID uint64 `json:"guardianProverID"` -} - -// map of blockID to guardianProverInfo -type blockInfo map[uint64][]block - -// GetBlocks -// -// returns signed block data by each guardian prover. -// -// @Summary Get signed blocks -// @ID get-blocks -// @Accept json -// @Produce json -// @Success 200 {object} []guardianproverhealthcheck.GuardianProver -// @Router /blocks [get] - -func (srv *Server) GetBlocks(c echo.Context) error { - signedBlocks := []guardianProverInfo{} - // call each guardian prover and get their most recently signed blocks. - for _, g := range srv.guardianProvers { - r := []signedBlock{} - - resp, err := http.Get(g.Endpoint.String() + "/signedBlocks") - if err != nil { - return c.JSON(http.StatusBadRequest, err) - } - - b, err := io.ReadAll(resp.Body) - if err != nil { - return c.JSON(http.StatusBadRequest, err) - } - - if err := json.Unmarshal(b, &r); err != nil { - return c.JSON(http.StatusBadRequest, err) - } - - signedBlocks = append(signedBlocks, guardianProverInfo{ - GuardianProverID: g.ID.Uint64(), - signedBlocks: r, - }) - } - - blocks := make(blockInfo) - // then iterate over each one and create a more easily parsable api response - // for the frontend to consume. - for _, v := range signedBlocks { - for _, sb := range v.signedBlocks { - b := block{ - GuardianProverID: v.GuardianProverID, - BlockHash: sb.BlockHash, - Signature: sb.Signature, - } - - if _, ok := blocks[sb.BlockID]; !ok { - blocks[sb.BlockID] = make([]block, 0) - } - - blocks[sb.BlockID] = append(blocks[sb.BlockID], b) - } - } - - return c.JSON(http.StatusOK, blocks) -} diff --git a/packages/guardian-prover-health-check/http/get_signed_blocks.go b/packages/guardian-prover-health-check/http/get_signed_blocks.go new file mode 100644 index 00000000000..0cbeeaa2340 --- /dev/null +++ b/packages/guardian-prover-health-check/http/get_signed_blocks.go @@ -0,0 +1,112 @@ +package http + +import ( + "math/big" + "net/http" + "strconv" + + echo "github.com/labstack/echo/v4" + "github.com/labstack/gommon/log" + guardianproverhealthcheck "github.com/taikoxyz/taiko-mono/packages/guardian-prover-health-check" +) + +var ( + numBlocks uint64 = 100 +) + +type block struct { + BlockHash string `json:"blockHash"` + Signature string `json:"signature"` + GuardianProverID uint64 `json:"guardianProverID"` +} + +// map of blockID to signed block data +type blockResponse map[uint64][]block + +// GetSignedBlocks +// +// returns signed block data by each guardian prover. +// +// @Summary Get signed blocks +// @ID get-signed-blocks +// @Accept json +// @Produce json +// @Success 200 {object} blockResponse +// @Router /signedBlocks[get] + +func (srv *Server) GetSignedBlocks(c echo.Context) error { + // getSignedBlocks should rewind either startingBlockID - numBlocksToReturn if startingBlockID + // is passed in, but it is optional, so if it is not, we should get latest and rewind from + // there. + var start uint64 = 0 + + if c.QueryParam("start") != "" { + var err error + + start, err = strconv.ParseUint(c.QueryParam("start"), 10, 64) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err) + } + } + + // if no start timestamp was provided, we can get the latest block, and return + // defaultNumBlocksToReturn blocks signed before latest, if our guardian prover has signed them. + if start == 0 { + latestBlock, err := srv.ethClient.BlockByNumber(c.Request().Context(), nil) + if err != nil { + if err != nil { + log.Error("Failed to get latest L2 block", "error", err) + return echo.NewHTTPError(http.StatusInternalServerError, err) + } + } + + // if latestBlock is greater than the number of blocks to return, we only want to return + // the most recent N blocks signed by this guardian prover. + if latestBlock.NumberU64() > numBlocks { + blockNum := latestBlock.NumberU64() - numBlocks + + block, err := srv.ethClient.BlockByNumber( + c.Request().Context(), + new(big.Int).SetUint64(blockNum), + ) + if err != nil { + log.Error("Failed to get L2 block", "error", err, "blockNum", blockNum) + return echo.NewHTTPError(http.StatusInternalServerError, err) + } + + start = block.NumberU64() + } + } + + signedBlocks, err := srv.signedBlockRepo.GetByStartingBlockID( + guardianproverhealthcheck.GetSignedBlocksByStartingBlockIDOpts{ + StartingBlockID: start, + }, + ) + + if err != nil { + log.Error("Failed to get latest L2 block", "error", err) + return echo.NewHTTPError(http.StatusInternalServerError, err) + } + + // sort signed blocks for easier to consume data + blocks := make(blockResponse) + // then iterate over each one and create a more easily parsable api response + // for the frontend to consume, arranged by a mapping of block ID + // to the signed blocks for each prover by that block ID. + for _, v := range signedBlocks { + b := block{ + GuardianProverID: v.GuardianProverID, + BlockHash: v.BlockHash, + Signature: v.Signature, + } + + if _, ok := blocks[v.BlockID]; !ok { + blocks[v.BlockID] = make([]block, 0) + } + + blocks[v.BlockID] = append(blocks[v.BlockID], b) + } + + return c.JSON(http.StatusOK, blocks) +} diff --git a/packages/guardian-prover-health-check/http/post_health_check.go b/packages/guardian-prover-health-check/http/post_health_check.go new file mode 100644 index 00000000000..f3d49edbe38 --- /dev/null +++ b/packages/guardian-prover-health-check/http/post_health_check.go @@ -0,0 +1,65 @@ +package http + +import ( + "net/http" + + "github.com/ethereum/go-ethereum/crypto" + echo "github.com/labstack/echo/v4" + guardianproverhealthcheck "github.com/taikoxyz/taiko-mono/packages/guardian-prover-health-check" +) + +var ( + msg = crypto.Keccak256Hash([]byte("HEART_BEAT")).Bytes() +) + +type healthCheckReq struct { + ProverAddress string `json:"prover"` + HeartBeatSignature string `json:"heartBeatSignature"` +} + +// PostHealthCheck +// +// post a health check from a guardian prover +// +// @Summary Post healthcheck +// @ID post-health-check +// @Accept json +// @Produce json +// @Success 200 null +// @Router /healthCheck [post] + +func (srv *Server) PostHealthCheck(c echo.Context) error { + req := &healthCheckReq{} + + // bind incoming request + if err := c.Bind(req); err != nil { + return c.JSON(http.StatusBadRequest, err) + } + + recoveredGuardianProver, err := guardianproverhealthcheck.SignatureToGuardianProver( + msg, + req.HeartBeatSignature, + srv.guardianProvers, + ) + + // if not, we want to return an error + if err != nil { + return c.JSON(http.StatusBadRequest, err) + } + + // otherwise, we can store it in the database. + // expected address and recovered address will be the same until we have an auth + // mechanism which will allow us to store health checks that ecrecover to an unexpected + // address. + if err := srv.healthCheckRepo.Save(guardianproverhealthcheck.SaveHealthCheckOpts{ + GuardianProverID: recoveredGuardianProver.ID.Uint64(), + Alive: true, + ExpectedAddress: recoveredGuardianProver.Address.Hex(), + RecoveredAddress: recoveredGuardianProver.Address.Hex(), + SignedResponse: req.HeartBeatSignature, + }); err != nil { + return c.JSON(http.StatusBadRequest, err) + } + + return c.JSON(http.StatusOK, nil) +} diff --git a/packages/guardian-prover-health-check/http/post_signed_block.go b/packages/guardian-prover-health-check/http/post_signed_block.go new file mode 100644 index 00000000000..7e92145926d --- /dev/null +++ b/packages/guardian-prover-health-check/http/post_signed_block.go @@ -0,0 +1,64 @@ +package http + +import ( + "log/slog" + "net/http" + + "github.com/ethereum/go-ethereum/common" + echo "github.com/labstack/echo/v4" + guardianproverhealthcheck "github.com/taikoxyz/taiko-mono/packages/guardian-prover-health-check" +) + +type signedBlock struct { + BlockID uint64 `json:"blockID"` + BlockHash string `json:"blockHash"` + Signature string `json:"signature"` + Prover common.Address `json:"proverAddress"` +} + +// PostSignedBlock +// +// post a signed block to store in the database +// +// @Summary Post signed block +// @ID post-signed-block +// @Accept json +// @Produce json +// @Success 200 null +// @Router /signedBlock [post] + +func (srv *Server) PostSignedBlock(c echo.Context) error { + req := &signedBlock{} + + // bind incoming request + if err := c.Bind(req); err != nil { + slog.Error("error binding request", "error", err) + return c.JSON(http.StatusBadRequest, err) + } + + recoveredGuardianProver, err := guardianproverhealthcheck.SignatureToGuardianProver( + common.HexToHash(req.BlockHash).Bytes(), + req.Signature, + srv.guardianProvers, + ) + + // if not, we want to return an error + if err != nil { + slog.Error("error recovering guardian prover", "error", err) + return c.JSON(http.StatusBadRequest, err) + } + + // otherwise, we can store it in the database. + if err := srv.signedBlockRepo.Save(guardianproverhealthcheck.SaveSignedBlockOpts{ + GuardianProverID: recoveredGuardianProver.ID.Uint64(), + BlockID: req.BlockID, + BlockHash: req.BlockHash, + Signature: req.Signature, + RecoveredAddress: recoveredGuardianProver.Address.Hex(), + }); err != nil { + slog.Error("error saving signed block to db", "error", err) + return c.JSON(http.StatusBadRequest, err) + } + + return c.JSON(http.StatusOK, nil) +} diff --git a/packages/guardian-prover-health-check/http/post_signed_block_test.go b/packages/guardian-prover-health-check/http/post_signed_block_test.go new file mode 100644 index 00000000000..88e10bdf4b7 --- /dev/null +++ b/packages/guardian-prover-health-check/http/post_signed_block_test.go @@ -0,0 +1,46 @@ +package http + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/cyberhorsey/webutils/testutils" + "github.com/labstack/echo/v4" +) + +func Test_PostSignedBlock(t *testing.T) { + srv := newTestServer("") + + tests := []struct { + name string + body signedBlock + wantStatus int + }{ + { + "signatureNotRecoverableToGuardianProverAddress", + signedBlock{ + BlockID: 1, + BlockHash: "0x123", + Signature: "0x123", + }, + http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := testutils.NewUnauthenticatedRequest( + echo.POST, + "/signedBlock", + tt.body, + ) + + rec := httptest.NewRecorder() + + srv.ServeHTTP(rec, req) + + testutils.AssertStatusAndBody(t, rec, tt.wantStatus, []string{}) + }) + } +} diff --git a/packages/guardian-prover-health-check/http/routes.go b/packages/guardian-prover-health-check/http/routes.go index 4148c8128c7..8cb96da3a3c 100644 --- a/packages/guardian-prover-health-check/http/routes.go +++ b/packages/guardian-prover-health-check/http/routes.go @@ -14,5 +14,9 @@ func (srv *Server) configureRoutes() { srv.echo.GET("/stats/:id", srv.GetStatsByGuardianProverID) - srv.echo.GET("/signedBlocks", srv.GetBlocks) + srv.echo.GET("/signedBlocks", srv.GetSignedBlocks) + + srv.echo.POST("/signedBlock", srv.PostSignedBlock) + + srv.echo.POST("/healthCheck", srv.PostHealthCheck) } diff --git a/packages/guardian-prover-health-check/http/server.go b/packages/guardian-prover-health-check/http/server.go index 6da9945115e..8815b46ab52 100644 --- a/packages/guardian-prover-health-check/http/server.go +++ b/packages/guardian-prover-health-check/http/server.go @@ -5,6 +5,7 @@ import ( "net/http" "os" + "github.com/ethereum/go-ethereum/ethclient" "github.com/labstack/echo/v4/middleware" guardianproverhealthcheck "github.com/taikoxyz/taiko-mono/packages/guardian-prover-health-check" @@ -25,14 +26,18 @@ import ( // Server represents an guardian prover health check http server instance. type Server struct { echo *echo.Echo + ethClient *ethclient.Client healthCheckRepo guardianproverhealthcheck.HealthCheckRepository + signedBlockRepo guardianproverhealthcheck.SignedBlockRepository statRepo guardianproverhealthcheck.StatRepository guardianProvers []guardianproverhealthcheck.GuardianProver } type NewServerOpts struct { Echo *echo.Echo + EthClient *ethclient.Client HealthCheckRepo guardianproverhealthcheck.HealthCheckRepository + SignedBlockRepo guardianproverhealthcheck.SignedBlockRepository StatRepo guardianproverhealthcheck.StatRepository CorsOrigins []string GuardianProvers []guardianproverhealthcheck.GuardianProver @@ -41,9 +46,11 @@ type NewServerOpts struct { func NewServer(opts NewServerOpts) (*Server, error) { srv := &Server{ echo: opts.Echo, + ethClient: opts.EthClient, healthCheckRepo: opts.HealthCheckRepo, statRepo: opts.StatRepo, guardianProvers: opts.GuardianProvers, + signedBlockRepo: opts.SignedBlockRepo, } corsOrigins := opts.CorsOrigins diff --git a/packages/guardian-prover-health-check/http/server_test.go b/packages/guardian-prover-health-check/http/server_test.go index 564c131b2e5..c71717e7e83 100644 --- a/packages/guardian-prover-health-check/http/server_test.go +++ b/packages/guardian-prover-health-check/http/server_test.go @@ -20,6 +20,7 @@ func newTestServer(url string) *Server { echo: echo.New(), healthCheckRepo: mock.NewHealthCheckRepository(), statRepo: mock.NewStatRepository(), + signedBlockRepo: mock.NewSignedBlockRepository(), guardianProvers: make([]guardianproverhealthcheck.GuardianProver, 0), } diff --git a/packages/guardian-prover-health-check/migrations/1666651001_create_signed_blocks_table.sql b/packages/guardian-prover-health-check/migrations/1666651001_create_signed_blocks_table.sql new file mode 100644 index 00000000000..f62d84bc460 --- /dev/null +++ b/packages/guardian-prover-health-check/migrations/1666651001_create_signed_blocks_table.sql @@ -0,0 +1,20 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE IF NOT EXISTS signed_blocks ( + id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + guardian_prover_id int NOT NULL, + block_id int NOT NULL, + signature varchar(5000) NOT NULL, + block_hash varchar (42) NOT NULL, + recovered_address varchar(42) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE key `guardian_prover_id_block_id` (`guardian_prover_id`, `block_id`), + UNIQUE key `guardian_prover_id_block_hash` (`guardian_prover_id`, `block_hash`) +); + +-- +goose StatementEnd +-- +goose Down +-- +goose StatementBegin +DROP TABLE signed_blocks; +-- +goose StatementEnd \ No newline at end of file diff --git a/packages/guardian-prover-health-check/mock/signed_block_repo.go b/packages/guardian-prover-health-check/mock/signed_block_repo.go new file mode 100644 index 00000000000..1d942f58a0e --- /dev/null +++ b/packages/guardian-prover-health-check/mock/signed_block_repo.go @@ -0,0 +1,42 @@ +package mock + +import ( + guardianproverhealthcheck "github.com/taikoxyz/taiko-mono/packages/guardian-prover-health-check" +) + +type SignedBlockRepo struct { + signedBlocks []*guardianproverhealthcheck.SignedBlock +} + +func NewSignedBlockRepository() *SignedBlockRepo { + return &SignedBlockRepo{ + signedBlocks: make([]*guardianproverhealthcheck.SignedBlock, 0), + } +} + +func (r *SignedBlockRepo) Save(opts guardianproverhealthcheck.SaveSignedBlockOpts) error { + r.signedBlocks = append(r.signedBlocks, &guardianproverhealthcheck.SignedBlock{ + GuardianProverID: opts.GuardianProverID, + BlockID: opts.BlockID, + BlockHash: opts.BlockHash, + Signature: opts.Signature, + RecoveredAddress: opts.RecoveredAddress, + }, + ) + + return nil +} + +func (r *SignedBlockRepo) GetByStartingBlockID( + opts guardianproverhealthcheck.GetSignedBlocksByStartingBlockIDOpts, +) ([]*guardianproverhealthcheck.SignedBlock, error) { + sb := make([]*guardianproverhealthcheck.SignedBlock, 0) + + for _, v := range r.signedBlocks { + if v.BlockID >= opts.StartingBlockID { + sb = append(sb, v) + } + } + + return sb, nil +} diff --git a/packages/guardian-prover-health-check/repo/signed_block.go b/packages/guardian-prover-health-check/repo/signed_block.go new file mode 100644 index 00000000000..acab3ca4252 --- /dev/null +++ b/packages/guardian-prover-health-check/repo/signed_block.go @@ -0,0 +1,51 @@ +package repo + +import ( + guardianproverhealthcheck "github.com/taikoxyz/taiko-mono/packages/guardian-prover-health-check" + "gorm.io/gorm" +) + +type SignedBlockRepository struct { + db DB +} + +func NewSignedBlockRepository(db DB) (*SignedBlockRepository, error) { + if db == nil { + return nil, ErrNoDB + } + + return &SignedBlockRepository{ + db: db, + }, nil +} + +func (r *SignedBlockRepository) startQuery() *gorm.DB { + return r.db.GormDB().Table("signed_blocks") +} + +func (r *SignedBlockRepository) Save(opts guardianproverhealthcheck.SaveSignedBlockOpts) error { + b := &guardianproverhealthcheck.SignedBlock{ + GuardianProverID: opts.GuardianProverID, + BlockID: opts.BlockID, + BlockHash: opts.BlockHash, + RecoveredAddress: opts.RecoveredAddress, + Signature: opts.Signature, + } + if err := r.startQuery().Create(b).Error; err != nil { + return err + } + + return nil +} + +func (r *SignedBlockRepository) GetByStartingBlockID( + opts guardianproverhealthcheck.GetSignedBlocksByStartingBlockIDOpts, +) ([]*guardianproverhealthcheck.SignedBlock, error) { + var sb []*guardianproverhealthcheck.SignedBlock + + if err := r.startQuery().Where("block_id >= ?", opts.StartingBlockID).Find(&sb).Error; err != nil { + return nil, err + } + + return sb, nil +} diff --git a/packages/guardian-prover-health-check/repo/signed_block_test.go b/packages/guardian-prover-health-check/repo/signed_block_test.go new file mode 100644 index 00000000000..2c94c50adc4 --- /dev/null +++ b/packages/guardian-prover-health-check/repo/signed_block_test.go @@ -0,0 +1,69 @@ +package repo + +import ( + "testing" + + guardianproverhealthcheck "github.com/taikoxyz/taiko-mono/packages/guardian-prover-health-check" + "github.com/taikoxyz/taiko-mono/packages/guardian-prover-health-check/db" + "gopkg.in/go-playground/assert.v1" +) + +func Test_NewSignedBlockRepo(t *testing.T) { + tests := []struct { + name string + db DB + wantErr error + }{ + { + "success", + &db.DB{}, + nil, + }, + { + "noDb", + nil, + ErrNoDB, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewSignedBlockRepository(tt.db) + assert.Equal(t, tt.wantErr, err) + }) + } +} + +func TestIntegration_SignedBlock_Save(t *testing.T) { + db, close, err := testMysql(t) + assert.Equal(t, nil, err) + + defer close() + + SignedBlockRepo, err := NewSignedBlockRepository(db) + assert.Equal(t, nil, err) + tests := []struct { + name string + opts guardianproverhealthcheck.SaveSignedBlockOpts + wantErr error + }{ + { + "success", + guardianproverhealthcheck.SaveSignedBlockOpts{ + GuardianProverID: 1, + RecoveredAddress: "0x123", + Signature: "0x456", + BlockID: 1, + BlockHash: "0x987", + }, + nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err = SignedBlockRepo.Save(tt.opts) + assert.Equal(t, tt.wantErr, err) + }) + } +} diff --git a/packages/guardian-prover-health-check/signed_block.go b/packages/guardian-prover-health-check/signed_block.go new file mode 100644 index 00000000000..537ba61457e --- /dev/null +++ b/packages/guardian-prover-health-check/signed_block.go @@ -0,0 +1,33 @@ +package guardianproverhealthcheck + +import ( + "time" +) + +type SignedBlock struct { + GuardianProverID uint64 `json:"guardianProverID"` + BlockID uint64 `json:"blockID"` + BlockHash string `json:"blockHash"` + Signature string `json:"signature"` + RecoveredAddress string `json:"recoveredAddress"` + CreatedAt time.Time `jsom:"createdAt"` +} + +type SaveSignedBlockOpts struct { + GuardianProverID uint64 + BlockID uint64 + BlockHash string + Signature string + RecoveredAddress string +} + +type GetSignedBlocksByStartingBlockIDOpts struct { + StartingBlockID uint64 +} + +// SignedBlockRepository defines database interaction methods to create and get +// signed blocks submitted by guardian provers. +type SignedBlockRepository interface { + Save(opts SaveSignedBlockOpts) error + GetByStartingBlockID(opts GetSignedBlocksByStartingBlockIDOpts) ([]*SignedBlock, error) +}