From f23443b7d1ad47ceb462e6807a66c8e8069b73b1 Mon Sep 17 00:00:00 2001 From: zhouop0 <11733741+zhouop0@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:59:10 +0800 Subject: [PATCH] add claim interface --- .env | 1 + .env.template | 1 + docs/docs.go | 41 +++++++++ docs/swagger.json | 41 +++++++++ docs/swagger.yaml | 26 ++++++ internal/api/dispute_game_handler.go | 132 ++++++++++++++++++++++++++- internal/svc/svc.go | 7 ++ internal/types/config.go | 3 +- main.go | 4 +- pkg/contract/disputeGame_test.go | 43 +++++++-- 10 files changed, 286 insertions(+), 13 deletions(-) diff --git a/.env b/.env index d35204a..66aa7af 100644 --- a/.env +++ b/.env @@ -2,6 +2,7 @@ LOG_FORMAT=console MYSQL_DATA_SOURCE=root:root@tcp(127.0.0.1:3367)/dispute_explorer?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true BLOCKCHAIN=seplia L1_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/lV2e-64nNnEMUA7UG0IT0uwjzlxEI512 +L2_RPC_URL=https://opt-sepolia.g.alchemy.com/v2/FPgbOkDCgG8t0ppZ6TwZXLucr1wl_us4 RPC_RATE_LIMIT=15 RPC_RATE_BURST=5 FROM_BLOCK_NUMBER=6034337 diff --git a/.env.template b/.env.template index 9727d5a..fe3d61b 100644 --- a/.env.template +++ b/.env.template @@ -2,6 +2,7 @@ LOG_FORMAT=console MYSQL_DATA_SOURCE= BLOCKCHAIN= L1_RPC_URL= +L2_RPC_URL= RPC_RATE_LIMIT=15 RPC_RATE_BURST=5 FROM_BLOCK_NUMBER=6034337 diff --git a/docs/docs.go b/docs/docs.go index 5e4604f..34439b9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -96,6 +96,47 @@ const docTemplate = `{ } } }, + "/disputegames/calculate/claim": { + "post": { + "description": "calculate dispute game honest claim by postion", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/disputegames/claimroot/:blockNumber": { + "get": { + "description": "calculate l2 block claim roo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "calculate l2 block claim root", + "parameters": [ + { + "type": "integer", + "description": "dispute game l2 block number", + "name": "blockNumber", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/disputegames/count": { "get": { "description": "Get dispute games count group by status and per day", diff --git a/docs/swagger.json b/docs/swagger.json index 5baa724..8af9c21 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -85,6 +85,47 @@ } } }, + "/disputegames/calculate/claim": { + "post": { + "description": "calculate dispute game honest claim by postion", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/disputegames/claimroot/:blockNumber": { + "get": { + "description": "calculate l2 block claim roo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "calculate l2 block claim root", + "parameters": [ + { + "type": "integer", + "description": "dispute game l2 block number", + "name": "blockNumber", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/disputegames/count": { "get": { "description": "Get dispute games count group by status and per day", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1d80c52..4cb068f 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -85,6 +85,32 @@ paths: "200": description: OK summary: Get credit details + /disputegames/calculate/claim: + post: + consumes: + - application/json + description: calculate dispute game honest claim by postion + produces: + - application/json + responses: + "200": + description: OK + /disputegames/claimroot/:blockNumber: + get: + consumes: + - application/json + description: calculate l2 block claim roo + parameters: + - description: dispute game l2 block number + in: path + name: blockNumber + type: integer + produces: + - application/json + responses: + "200": + description: OK + summary: calculate l2 block claim root /disputegames/count: get: consumes: diff --git a/internal/api/dispute_game_handler.go b/internal/api/dispute_game_handler.go index 75f629c..2824c49 100644 --- a/internal/api/dispute_game_handler.go +++ b/internal/api/dispute_game_handler.go @@ -1,8 +1,20 @@ package api import ( + "context" + "fmt" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/optimism-java/dispute-explorer/pkg/contract" + "math/big" "net/http" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/pkg/errors" + "github.com/spf13/cast" "github.com/gin-gonic/gin" @@ -12,12 +24,16 @@ import ( ) type DisputeGameHandler struct { - DB *gorm.DB + DB *gorm.DB + L1RPC *ethclient.Client + L2RPC *ethclient.Client } -func NewDisputeGameHandler(db *gorm.DB) *DisputeGameHandler { +func NewDisputeGameHandler(db *gorm.DB, l1rpc *ethclient.Client, l2rpc *ethclient.Client) *DisputeGameHandler { return &DisputeGameHandler{ - DB: db, + DB: db, + L1RPC: l1rpc, + L2RPC: l2rpc, } } @@ -251,3 +267,113 @@ func (h DisputeGameHandler) ListGameEvents(c *gin.Context) { "records": events, }) } + +// @Summary calculate l2 block claim root +// @schemes +// @Description calculate l2 block claim roo +// @Accept json +// @Produce json +// @Param blockNumber path int false "dispute game l2 block number" +// @Success 200 +// @Router /disputegames/claimroot/:blockNumber [get] +func (h DisputeGameHandler) GetClaimRoot(c *gin.Context) { + blockNumber := c.Param("blockNumber") + res, err := h.getClaimRoot(cast.ToInt64(blockNumber)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("error %s", err.Error()), + }) + } + + c.JSON(http.StatusOK, gin.H{ + "output": res, + }) +} + +func (h DisputeGameHandler) getClaimRoot(blockNumber int64) (string, error) { + block, err := h.L2RPC.BlockByNumber(context.Background(), big.NewInt(cast.ToInt64(blockNumber))) + if err != nil { + return "", fmt.Errorf("block number is nil %d", blockNumber) + } + var getProofResponse *eth.AccountResult + err = h.L2RPC.Client().CallContext(context.Background(), &getProofResponse, "eth_getProof", + predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, block.Hash().String()) + if err != nil { + return "", fmt.Errorf("call eth_getProof error:%s", errors.WithStack(err)) + } + output := ð.OutputV0{ + StateRoot: eth.Bytes32(block.Root()), + MessagePasserStorageRoot: eth.Bytes32(getProofResponse.StorageHash), + BlockHash: block.Hash(), + } + return fmt.Sprint(eth.OutputRoot(output)), nil +} + +type CalculateClaim struct { + DisputeGame string `json:"disputeGame"` + Position int64 `json:"position"` +} + +// @Sumary calculate claim by position +// @Schemes +// @Description calculate dispute game honest claim by postion +// @Accept json +// @Produce json +// @Success 200 +// @Router /disputegames/calculate/claim [post] +func (h DisputeGameHandler) GetGamesClaimByPosition(c *gin.Context) { + json := &CalculateClaim{} + err := c.BindJSON(&json) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("error %s", err.Error()), + }) + } + res, err := h.gamesClaimByPosition(json) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("error %s", err.Error()), + }) + } + c.JSON(http.StatusOK, gin.H{ + "DisputeGame": json.DisputeGame, + "Position": json.Position, + "claims": res, + }) +} + +func (h DisputeGameHandler) gamesClaimByPosition(req *CalculateClaim) (string, error) { + newDisputeGame, err := contract.NewDisputeGame(common.HexToAddress(req.DisputeGame), h.L1RPC) + if err != nil { + return "", err + } + prestateBlock, err := newDisputeGame.StartingBlockNumber(&bind.CallOpts{}) + if err != nil { + return "", err + } + poststateBlock, err := newDisputeGame.L2BlockNumber(&bind.CallOpts{}) + if err != nil { + return "", err + } + splitDepth, err := newDisputeGame.SplitDepth(&bind.CallOpts{}) + if err != nil { + return "", err + } + splitDepths := types.Depth(splitDepth.Uint64()) + + pos := types.NewPositionFromGIndex(big.NewInt(req.Position)) + traceIndex := pos.TraceIndex(splitDepths) + if !traceIndex.IsUint64() { + return "", fmt.Errorf("err:%s", traceIndex) + } + outputBlock := traceIndex.Uint64() + prestateBlock.Uint64() + 1 + if outputBlock > poststateBlock.Uint64() { + outputBlock = poststateBlock.Uint64() + } + + root, err := h.getClaimRoot(cast.ToInt64(outputBlock)) + if err != nil { + return "", err + } + return root, nil +} diff --git a/internal/svc/svc.go b/internal/svc/svc.go index 090a9b3..c062f52 100644 --- a/internal/svc/svc.go +++ b/internal/svc/svc.go @@ -19,6 +19,7 @@ var svc *ServiceContext type ServiceContext struct { Config *types.Config L1RPC *ethclient.Client + L2RPC *ethclient.Client DB *gorm.DB LatestBlockNumber int64 SyncedBlockNumber int64 @@ -49,9 +50,15 @@ func NewServiceContext(ctx context.Context, cfg *types.Config) *ServiceContext { log.Panicf("[svc] get eth client panic: %s\n", err) } + rpc2, err := ethclient.Dial(cfg.L2RPCUrl) + if err != nil { + log.Panicf("[svc] get eth client panic: %s\n", err) + } + svc = &ServiceContext{ Config: cfg, L1RPC: rpc, + L2RPC: rpc2, DB: storage, Context: ctx, } diff --git a/internal/types/config.go b/internal/types/config.go index ad49655..c4f62a9 100644 --- a/internal/types/config.go +++ b/internal/types/config.go @@ -11,12 +11,13 @@ type Config struct { LogLevel string `env:"LOG_LEVEL" envDefault:"info"` // "console","json" LogFormat string `env:"LOG_FORMAT" envDefault:"console"` - MySQLDataSource string `env:"MYSQL_DATA_SOURCE" envDefault:"root:root@tcp(127.0.0.1:3366)/dispute_explorer?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true"` + MySQLDataSource string `env:"MYSQL_DATA_SOURCE" envDefault:"root:root@tcp(127.0.0.1:3367)/dispute_explorer?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true"` MySQLMaxIdleConns int `env:"MYSQL_MAX_IDLE_CONNS" envDefault:"10"` MySQLMaxOpenConns int `env:"MYSQL_MAX_OPEN_CONNS" envDefault:"20"` MySQLConnMaxLifetime int `env:"MYSQL_CONN_MAX_LIFETIME" envDefault:"3600"` Blockchain string `env:"BLOCKCHAIN" envDefault:"sepolia"` L1RPCUrl string `env:"L1_RPC_URL" envDefault:"https://eth-sepolia.g.alchemy.com/v2/PNunSRFo0FWRJMu5yrwBd6jF7G78YHrv"` + L2RPCUrl string `env:"L2_RPC_URL" envDefault:"https://opt-sepolia.g.alchemy.com/v2/FPgbOkDCgG8t0ppZ6TwZXLucr1wl_us4"` RPCRateLimit int `env:"RPC_RATE_LIMIT" envDefault:"15"` RPCRateBurst int `env:"RPC_RATE_BURST" envDefault:"5"` FromBlockNumber int64 `env:"FROM_BLOCK_NUMBER" envDefault:"6034337"` diff --git a/main.go b/main.go index fefb491..57a920c 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,7 @@ func main() { handler.Run(sCtx) log.Info("listener running...\n") router := gin.Default() - disputeGameHandler := api.NewDisputeGameHandler(sCtx.DB) + disputeGameHandler := api.NewDisputeGameHandler(sCtx.DB, sCtx.L1RPC, sCtx.L2RPC) docs.SwaggerInfo.Title = "Dispute Game Swagger API" docs.SwaggerInfo.Description = "This is a dispute-explorer server." docs.SwaggerInfo.BasePath = "/" @@ -39,6 +39,8 @@ func main() { router.GET("/disputegames/statistics/bond/inprogress", disputeGameHandler.GetBondInProgressPerDays) router.GET("/disputegames/daylycount", disputeGameHandler.GetCountDisputeGameGroupByStatus) router.GET("/disputegames/events", disputeGameHandler.ListGameEvents) + router.GET("/disputegames/claimroot/:blockNumber", disputeGameHandler.GetClaimRoot) + router.POST("/disputegames/calculate/claim", disputeGameHandler.GetGamesClaimByPosition) router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) diff --git a/pkg/contract/disputeGame_test.go b/pkg/contract/disputeGame_test.go index e591d11..d829858 100644 --- a/pkg/contract/disputeGame_test.go +++ b/pkg/contract/disputeGame_test.go @@ -6,6 +6,8 @@ import ( "fmt" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/predeploys" "github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" @@ -96,20 +98,45 @@ func TestBlockRange(t *testing.T) { } fmt.Printf("outputblock:%d\n", outputBlock) fmt.Printf("blockhash:%s", hexutil.Uint64(outputBlock)) - //outputRoot 0xc58adb6387728df32318772a7beefa386072b4347e39f64a753bfd82c8acdb07 - //func (o *OutputTraceProvider) outputAtBlock(ctx context.Context, block uint64) (common.Hash, error) { - // output, err := o.rollupProvider.OutputAtBlock(ctx, block) - // if err != nil { - // return common.Hash{}, fmt.Errorf("failed to fetch output at block %v: %w", block, err) - // } - // return common.Hash(output.OutputRoot), nil - //} l2rpc, err := ethclient.Dial("https://opt-sepolia.g.alchemy.com/v2/FPgbOkDCgG8t0ppZ6TwZXLucr1wl_us4") require.NoError(t, err) defer l2rpc.Close() l2RPC := client.NewBaseRPCClient(l2rpc.Client()) + rollupClient := sources.NewRollupClient(l2RPC) output, err := rollupClient.OutputAtBlock(context.Background(), outputBlock) fmt.Printf("outputRoot:%s\n", common.Hash(output.OutputRoot)) } + +//blockHash 0x0e494f1663e2e1b876f706668f1abebd762341aaefd9e7463cb3a109383a6f5b +//stateRoot 0xe67c67ddb0ac98bc8c395d782fc32bdcdc4590b93da6abef8da88ef9f62050c2 +//storageHash 0x888e7e703509255745ed639a98c7dd1c8c84c98fae3a884c642b0343fbb69b3c + +func TestHash(t *testing.T) { + l2rpc, err := ethclient.Dial("https://opt-sepolia.g.alchemy.com/v2/FPgbOkDCgG8t0ppZ6TwZXLucr1wl_us4") + require.NoError(t, err) + defer l2rpc.Close() + + block, err := l2rpc.BlockByNumber(context.Background(), big.NewInt(12827274)) + require.NoError(t, err) + l2RPC := client.NewBaseRPCClient(l2rpc.Client()) + + var getProofResponse *eth.AccountResult + err = l2RPC.CallContext(context.Background(), &getProofResponse, "eth_getProof", predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, block.Hash().String()) + require.NoError(t, err) + + output := ð.OutputV0{ + StateRoot: eth.Bytes32(block.Root()), + MessagePasserStorageRoot: eth.Bytes32(getProofResponse.StorageHash), + BlockHash: block.Hash(), + } + + //output := ð.OutputV0{ + // StateRoot: eth.Bytes32(common.HexToHash("0xe67c67ddb0ac98bc8c395d782fc32bdcdc4590b93da6abef8da88ef9f62050c2")), + // MessagePasserStorageRoot: eth.Bytes32(common.HexToHash("0x888e7e703509255745ed639a98c7dd1c8c84c98fae3a884c642b0343fbb69b3c")), + // BlockHash: common.HexToHash("0x0e494f1663e2e1b876f706668f1abebd762341aaefd9e7463cb3a109383a6f5b"), + //} + outputRoot := eth.OutputRoot(output) + fmt.Println(outputRoot) +}