From c5b8d9b8174957ac68d51d6ccc0179f25f368879 Mon Sep 17 00:00:00 2001 From: Teja2045 <106052623+Teja2045@users.noreply.github.com> Date: Thu, 19 Sep 2024 17:41:22 +0530 Subject: [PATCH] feat: decouple avail light client (#41) * feat: decouble avail light client * lint: fixe lint issues * lint: fix lint * fix: fix encoding error * lint: fix lint issues * lint: fix lint issues * lint: fix lint * refactor: add da client as parameter * chore: fix tests * chore: remove irrelavent commant --- keeper/vote_extension.go | 2 +- relayer/avail/da.go | 8 + relayer/avail/light_client.go | 93 +++++ relayer/avail/types.go | 17 + .../http_client_handler.go} | 25 +- relayer/init_test.go | 5 +- relayer/publish.go | 4 +- relayer/relayer.go | 9 +- relayer/submit_data.go | 332 +----------------- simapp/app/app.go | 12 +- 10 files changed, 161 insertions(+), 346 deletions(-) create mode 100644 relayer/avail/da.go create mode 100644 relayer/avail/light_client.go create mode 100644 relayer/avail/types.go rename relayer/{http_client.go => http/http_client_handler.go} (62%) diff --git a/keeper/vote_extension.go b/keeper/vote_extension.go index 8a6ff14..4444645 100644 --- a/keeper/vote_extension.go +++ b/keeper/vote_extension.go @@ -64,7 +64,7 @@ func (h *VoteExtHandler) ExtendVoteHandler() sdk.ExtendVoteHandler { return abciResponseVoteExt, nil } - ok, err := h.Keeper.relayer.IsDataAvailable(ctx, from, end, availHeight, h.Keeper.relayer.AvailConfig.LightClientURL) // TODO: read light client url from config + ok, err := h.Keeper.relayer.IsDataAvailable(ctx, from, end, availHeight) if ok { h.logger.Info("submitted data to Avail verified successfully at", "block_height", availHeight, diff --git a/relayer/avail/da.go b/relayer/avail/da.go new file mode 100644 index 0000000..0e2519f --- /dev/null +++ b/relayer/avail/da.go @@ -0,0 +1,8 @@ +package avail + +// implement this interface to interact with avail chain +type DA interface { + Submit(data []byte) (BlockMetaData, error) + IsDataAvailable(data []byte, availBlockHeight int) (bool, error) + GetBlock(availBlockHeight int) (Block, error) +} diff --git a/relayer/avail/light_client.go b/relayer/avail/light_client.go new file mode 100644 index 0000000..5eb8861 --- /dev/null +++ b/relayer/avail/light_client.go @@ -0,0 +1,93 @@ +package avail + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + http_client "github.com/vitwit/avail-da-module/relayer/http" +) + +// AvailLightClient is a concrete implementation of the availDA interface. +// It facilitates interaction with the Avail Network by utilizing the Avail light client. +// +// Fields: +// - HttpClient: An HTTP client handler used for making requests to the Avail light client. +// - LightClientURL: The URL of the Avail light client that this module communicates with. +type LightClient struct { + HTTPClient *http_client.Handler + LightClientURL string +} + +func NewLightClient(lightClientURL string, httpClient *http_client.Handler) *LightClient { + return &LightClient{ + HTTPClient: httpClient, + LightClientURL: lightClientURL, + } +} + +func (lc *LightClient) IsDataAvailable(data []byte, availBlockHeight int) (bool, error) { + availBlock, err := lc.GetBlock(availBlockHeight) + if err != nil { + return false, err + } + + base64CosmosBlockData := base64.StdEncoding.EncodeToString(data) + + // TODO: any better / optimized way to check if data is really available? + return isDataIncludedInBlock(availBlock, base64CosmosBlockData), nil +} + +func (lc *LightClient) GetBlock(availBlockHeight int) (Block, error) { + // Construct the URL with the block number + url := fmt.Sprintf("%s/v2/blocks/%v/data?fields=data", lc.LightClientURL, availBlockHeight) + + // Perform the GET request, returning the body directly + body, err := lc.HTTPClient.Get(url) + if err != nil { + return Block{}, fmt.Errorf("failed to fetch data from the avail: %w", err) + } + + // Decode the response body into the AvailBlock struct + var block Block + err = json.Unmarshal(body, &block) + if err != nil { + return Block{}, fmt.Errorf("failed to decode block response: %w", err) + } + + return block, nil +} + +func (lc *LightClient) Submit(data []byte) (BlockMetaData, error) { + var blockInfo BlockMetaData + + datab := base64.StdEncoding.EncodeToString(data) + jsonData := []byte(fmt.Sprintf(`{"data":"%s"}`, datab)) + url := fmt.Sprintf("%s/v2/submit", lc.LightClientURL) + + // Make the POST request + responseBody, err := lc.HTTPClient.Post(url, jsonData) + if err != nil { + fmt.Printf("Error: %v\n", err) + return blockInfo, err + } + + // Unmarshal the JSON data into the struct + err = json.Unmarshal(responseBody, &blockInfo) + if err != nil { + return BlockMetaData{}, err + } + + return blockInfo, nil +} + +// bruteforce comparison check +func isDataIncludedInBlock(availBlock Block, base64cosmosData string) bool { + for _, data := range availBlock.Extrinsics { + if data.Data == base64cosmosData { + return true + } + } + + return false +} diff --git a/relayer/avail/types.go b/relayer/avail/types.go new file mode 100644 index 0000000..b39ad6d --- /dev/null +++ b/relayer/avail/types.go @@ -0,0 +1,17 @@ +package avail + +type BlockMetaData struct { + BlockNumber int `json:"block_number"` + BlockHash string `json:"block_hash"` + Hash string `json:"hash"` + Index int `json:"index"` +} + +type Block struct { + Block int64 `json:"block_number"` + Extrinsics []Extrinsics `json:"data_transactions"` +} + +type Extrinsics struct { + Data string `json:"data"` +} diff --git a/relayer/http_client.go b/relayer/http/http_client_handler.go similarity index 62% rename from relayer/http_client.go rename to relayer/http/http_client_handler.go index 58dcc4f..fa79cb7 100644 --- a/relayer/http_client.go +++ b/relayer/http/http_client_handler.go @@ -1,4 +1,4 @@ -package relayer +package httpclient import ( "bytes" @@ -9,13 +9,13 @@ import ( ) // HTTPClientHandler struct -type HTTPClientHandler struct { +type Handler struct { client *http.Client } // NewHTTPClientHandler creates a new HTTPClientHandler with default settings -func NewHTTPClientHandler() *HTTPClientHandler { - return &HTTPClientHandler{ +func NewHandler() *Handler { + return &Handler{ client: &http.Client{ Timeout: 100 * time.Second, }, @@ -23,7 +23,7 @@ func NewHTTPClientHandler() *HTTPClientHandler { } // Get method -func (h *HTTPClientHandler) Get(url string) ([]byte, error) { +func (h *Handler) Get(url string) ([]byte, error) { resp, err := h.client.Get(url) if err != nil { return nil, fmt.Errorf("GET request error: %v", err) @@ -38,7 +38,7 @@ func (h *HTTPClientHandler) Get(url string) ([]byte, error) { } // Post method -func (h *HTTPClientHandler) Post(url string, jsonData []byte) ([]byte, error) { +func (h *Handler) Post(url string, jsonData []byte) ([]byte, error) { req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) @@ -58,16 +58,3 @@ func (h *HTTPClientHandler) Post(url string, jsonData []byte) ([]byte, error) { } return body, nil } - -// RequestData struct for the POST request payload -type RequestData struct { - Data []byte `json:"data,omitempty"` - Extrinsic string `json:"extrinsic,omitempty"` -} - -type BlockInfo struct { - BlockNumber int `json:"block_number"` - BlockHash string `json:"block_hash"` - Hash string `json:"hash"` - Index int `json:"index"` -} diff --git a/relayer/init_test.go b/relayer/init_test.go index 588c5f5..1975c24 100644 --- a/relayer/init_test.go +++ b/relayer/init_test.go @@ -18,13 +18,14 @@ import ( cada "github.com/vitwit/avail-da-module" module "github.com/vitwit/avail-da-module/module" relayer "github.com/vitwit/avail-da-module/relayer" + httpclient "github.com/vitwit/avail-da-module/relayer/http" ) type RelayerTestSuite struct { suite.Suite ctx sdk.Context - httpHandler relayer.HTTPClientHandler + httpHandler *httpclient.Handler addrs []sdk.AccAddress encCfg moduletestutil.TestEncodingConfig addressCodec addresstypes.Codec @@ -54,7 +55,7 @@ func (s *RelayerTestSuite) SetupTest() { s.baseApp.SetInterfaceRegistry(s.encCfg.InterfaceRegistry) s.addrs = simtestutil.CreateIncrementalAccounts(7) - s.httpHandler = *relayer.NewHTTPClientHandler() + s.httpHandler = httpclient.NewHandler() s.relayer = &relayer.Relayer{} } diff --git a/relayer/publish.go b/relayer/publish.go index 41e6515..2fa94ba 100644 --- a/relayer/publish.go +++ b/relayer/publish.go @@ -80,12 +80,12 @@ func (r *Relayer) postBlocks(ctx sdk.Context, blocks []int64, cdc codec.BinaryCo bb := r.GetBlocksDataFromLocal(ctx, blocks) - blockInfo, err := r.SubmitDataToAvailClient(r.AvailConfig.Seed, r.AvailConfig.AppID, bb, blocks, r.AvailConfig.LightClientURL) + blockInfo, err := r.SubmitDataToAvailClient(bb, blocks) if err != nil { r.logger.Error("Error while submitting block(s) to Avail DA", "height_start", blocks[0], "height_end", blocks[len(blocks)-1], - "appID", strconv.Itoa(r.AvailConfig.AppID), + "appID", strconv.Itoa(r.AvailConfig.AppID), err, ) // execute tx about failure submission diff --git a/relayer/relayer.go b/relayer/relayer.go index c4e99d9..d5e42e9 100644 --- a/relayer/relayer.go +++ b/relayer/relayer.go @@ -6,7 +6,7 @@ import ( "cosmossdk.io/log" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" - servertypes "github.com/cosmos/cosmos-sdk/server/types" + "github.com/vitwit/avail-da-module/relayer/avail" "github.com/vitwit/avail-da-module/relayer/local" "github.com/vitwit/avail-da-module/types" ) @@ -31,6 +31,7 @@ type Relayer struct { submittedBlocksCache map[int64]bool localProvider *local.CosmosProvider + availDAClient avail.DA clientCtx client.Context availChainID string @@ -42,11 +43,10 @@ type Relayer struct { func NewRelayer( logger log.Logger, cdc codec.BinaryCodec, - appOpts servertypes.AppOptions, + cfg types.AvailConfiguration, nodeDir string, + daClient avail.DA, ) (*Relayer, error) { - cfg := types.AvailConfigFromAppOpts(appOpts) - // local sdk-based chain provider localProvider, err := local.NewProvider(cdc, cfg.CosmosNodeRPC) if err != nil { @@ -64,6 +64,7 @@ func NewRelayer( submittedBlocksCache: make(map[int64]bool), NodeDir: nodeDir, AvailConfig: cfg, + availDAClient: daClient, }, nil } diff --git a/relayer/submit_data.go b/relayer/submit_data.go index 313e90e..22f9b51 100644 --- a/relayer/submit_data.go +++ b/relayer/submit_data.go @@ -1,112 +1,46 @@ package relayer import ( - "encoding/base64" - "encoding/json" - "fmt" "strconv" - "time" - gsrpc "github.com/centrifuge/go-substrate-rpc-client/v4" - "github.com/centrifuge/go-substrate-rpc-client/v4/registry" - "github.com/centrifuge/go-substrate-rpc-client/v4/registry/retriever" - "github.com/centrifuge/go-substrate-rpc-client/v4/registry/state" - "github.com/centrifuge/go-substrate-rpc-client/v4/signature" - "github.com/centrifuge/go-substrate-rpc-client/v4/types" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/vitwit/avail-da-module/relayer/avail" ) -func (r *Relayer) SubmitDataToAvailClient(_ string, _ int, data []byte, blocks []int64, lightClientURL string) (BlockInfo, error) { - var blockInfo BlockInfo - - handler := NewHTTPClientHandler() - datab := base64.StdEncoding.EncodeToString(data) - - jsonData := []byte(fmt.Sprintf(`{"data":"%s"}`, datab)) - - url := fmt.Sprintf("%s/v2/submit", lightClientURL) - - // Make the POST request - responseBody, err := handler.Post(url, jsonData) - if err != nil { - fmt.Printf("Error: %v\n", err) - return blockInfo, err - } - - // Unmarshal the JSON data into the struct - err = json.Unmarshal(responseBody, &blockInfo) +func (r *Relayer) SubmitDataToAvailClient(data []byte, blocks []int64) (avail.BlockMetaData, error) { + blockInfo, err := r.availDAClient.Submit(data) if err != nil { r.logger.Error("Error while posting block(s) to Avail DA", "height_start", blocks[0], "height_end", blocks[len(blocks)-1], "appID", strconv.Itoa(r.AvailConfig.AppID), ) - } - if err == nil { - r.logger.Info("Successfully posted block(s) to Avail DA", - "height_start", blocks[0], - "height_end", blocks[len(blocks)-1], - "appID", strconv.Itoa(r.AvailConfig.AppID), - "block_hash", blockInfo.BlockHash, - "block_number", blockInfo.BlockNumber, - "hash", blockInfo.Hash, - ) - } - - return blockInfo, nil -} - -func (r *Relayer) GetSubmittedData(lightClientURL string, blockNumber int) (BlockData, error) { - handler := NewHTTPClientHandler() - - // Construct the URL with the block number - url := fmt.Sprintf("%s/v2/blocks/%v/data?fields=data", lightClientURL, blockNumber) - - // Perform the GET request, returning the body directly - body, err := handler.Get(url) - if err != nil { - return BlockData{}, fmt.Errorf("failed to fetch data from the avail: %w", err) + return blockInfo, err } - // Decode the response body into the BlockData struct - var blockData BlockData - err = json.Unmarshal(body, &blockData) - if err != nil { - return BlockData{}, fmt.Errorf("failed to decode block response: %w", err) - } + r.logger.Info("Successfully posted block(s) to Avail DA", + "height_start", blocks[0], + "height_end", blocks[len(blocks)-1], + "appID", strconv.Itoa(r.AvailConfig.AppID), + "block_hash", blockInfo.BlockHash, + "block_number", blockInfo.BlockNumber, + "hash", blockInfo.Hash, + ) - return blockData, nil + return blockInfo, nil } // IsDataAvailable is to query the avail light client and check if the data is made available at the given height -func (r *Relayer) IsDataAvailable(ctx sdk.Context, from, to, availHeight uint64, lightClientURL string) (bool, error) { - availBlock, err := r.GetSubmittedData(lightClientURL, int(availHeight)) - if err != nil { - return false, err - } - +func (r *Relayer) IsDataAvailable(ctx sdk.Context, from, to, availHeight uint64) (bool, error) { var blocks []int64 for i := from; i <= to; i++ { blocks = append(blocks, int64(i)) } cosmosBlocksData := r.GetBlocksDataFromLocal(ctx, blocks) - base64CosmosBlockData := base64.StdEncoding.EncodeToString(cosmosBlocksData) - - // TODO: any better / optimized way to check if data is really available? - return isDataIncludedInBlock(availBlock, base64CosmosBlockData), nil -} - -// bruteforce comparison check -func isDataIncludedInBlock(availBlock BlockData, base64cosmosData string) bool { - for _, data := range availBlock.Extrinsics { - if data.Data == base64cosmosData { - return true - } - } - return false + return r.availDAClient.IsDataAvailable(cosmosBlocksData, int(availHeight)) } // Define the struct that matches the JSON structure @@ -114,239 +48,3 @@ type GetBlock struct { BlockNumber int `json:"block_number"` DataTransactions []string `json:"data_transactions"` } - -// submitData creates a transaction and makes a Avail data submission -func (r *Relayer) SubmitDataToAvailDA(apiURL, seed string, availAppID int, data []byte, blocks []int64) error { - api, err := gsrpc.NewSubstrateAPI(apiURL) - if err != nil { - r.logger.Error("cannot create api:%w", err) - return err - } - - fmt.Println("appurl", apiURL, "seed", seed) - fmt.Println("created substrate api") - - meta, err := api.RPC.State.GetMetadataLatest() - if err != nil { - r.logger.Error("cannot get metadata:%w", err) - return err - } - - // Set data and appID according to need - // data, _ := RandToken(size) - appID := 0 - - // if app id is greater than 0 then it must be created before submitting data - if availAppID != 0 { - appID = availAppID - } - - c, err := types.NewCall(meta, "DataAvailability.submit_data", types.NewBytes(data)) - if err != nil { - r.logger.Error("cannot create new call:%w", err) - return err - } - - // Create the extrinsic - ext := types.NewExtrinsic(c) - - genesisHash, err := api.RPC.Chain.GetBlockHash(0) - if err != nil { - r.logger.Error("cannot get block hash:%w", err) - return err - } - - rv, err := api.RPC.State.GetRuntimeVersionLatest() - if err != nil { - r.logger.Error("cannot get runtime version:%w", err) - return err - } - - keyringPair, err := signature.KeyringPairFromSecret(seed, 42) - if err != nil { - r.logger.Error("cannot create KeyPair:%w", err) - return err - } - - key, err := types.CreateStorageKey(meta, "System", "Account", keyringPair.PublicKey) - if err != nil { - r.logger.Error("cannot create storage key:%w", err) - return err - } - - var accountInfo types.AccountInfo - ok, err := api.RPC.State.GetStorageLatest(key, &accountInfo) - fmt.Println("ok", ok, err) - if err != nil || !ok { - r.logger.Error("cannot get latest storage:%w", err) - return err - } - - nonce := uint32(accountInfo.Nonce) - - o := types.SignatureOptions{ - BlockHash: genesisHash, - Era: types.ExtrinsicEra{IsMortalEra: false}, - GenesisHash: genesisHash, - Nonce: types.NewUCompactFromUInt(uint64(nonce)), - SpecVersion: rv.SpecVersion, - Tip: types.NewUCompactFromUInt(0), - AppID: types.NewUCompactFromUInt(uint64(0)), - TransactionVersion: rv.TransactionVersion, - } - - // Sign the transaction using Alice's default account - err = ext.Sign(keyringPair, o) - if err != nil { - r.logger.Error("cannot sign:%w", err) - return err - } - - // Send the extrinsic - sub, err := api.RPC.Author.SubmitAndWatchExtrinsic(ext) - if err != nil { - r.logger.Error("cannot submit extrinsic:%v", err) - return err - } - - fmt.Printf("** Data submitted to Avail DA using APPID: ** %v \n", appID) - if err == nil { - r.logger.Info("Posted block(s) to Avail DA", - "height_start", blocks[0], - "height_end", blocks[len(blocks)-1], - "appID", strconv.Itoa(r.AvailConfig.AppID), - ) - } - - defer sub.Unsubscribe() - timeout := time.After(100 * time.Second) - for { - select { - case status := <-sub.Chan(): - if status.IsInBlock { - fmt.Printf("Txn inside block %v\n", status.AsInBlock.Hex()) - } else if status.IsFinalized { - retriever, err := retriever.NewDefaultEventRetriever(state.NewEventProvider(api.RPC.State), api.RPC.State) - if err != nil { - r.logger.Error("Couldn't create event retriever: %s", err) - return err - } - - h := status.AsFinalized - events, err := retriever.GetEvents(h) - if err != nil { - r.logger.Error("Couldn't retrieve events") - } - - for _, event := range events { - if event.Name == "DataAvailability.DataSubmitted" { - from, _ := registry.ProcessDecodedFieldValue[*types.AccountID]( - event.Fields, - func(_ int, field *registry.DecodedField) bool { - return field.Name == "sp_core.crypto.AccountId32.who" - }, - func(value any) (*types.AccountID, error) { - fields, ok := value.(registry.DecodedFields) - - if !ok { - return nil, fmt.Errorf("unexpected value: %v", value) - } - - accByteSlice, err := registry.GetDecodedFieldAsSliceOfType[types.U8](fields, func(fieldIndex int, _ *registry.DecodedField) bool { - return fieldIndex == 0 - }) - if err != nil { - return nil, err - } - - var accBytes []byte - - for _, accByte := range accByteSlice { - accBytes = append(accBytes, byte(accByte)) - } - - return types.NewAccountID(accBytes) - }, - ) - a := from.ToHexString() - - // // add, _ := types.NewAddressFromHexAccountID(a) - // fmt.Println(from) - fmt.Printf("from address read from event: %s \n", a) - dataHash, err := registry.ProcessDecodedFieldValue[*types.Hash]( - event.Fields, - func(fieldIndex int, _ *registry.DecodedField) bool { - return fieldIndex == 1 - }, - func(value any) (*types.Hash, error) { - fields, ok := value.(registry.DecodedFields) - if !ok { - return nil, fmt.Errorf("unexpected value: %v", value) - } - - hashByteSlice, err := registry.GetDecodedFieldAsSliceOfType[types.U8](fields, func(fieldIndex int, _ *registry.DecodedField) bool { - return fieldIndex == 0 - }) - if err != nil { - return nil, err - } - - var hashBytes []byte - for _, hashByte := range hashByteSlice { - hashBytes = append(hashBytes, byte(hashByte)) - } - - hash := types.NewHash(hashBytes) - return &hash, nil - }, - ) - - switch { - case err != nil: - fmt.Printf("DataHash parsing err: %s\n", err.Error()) - case dataHash == nil: - fmt.Println("DataHash is nil") - default: - fmt.Printf("DataHash read from event: %s \n", dataHash.Hex()) - } - - } - } - fmt.Printf("Txn inside finalized block\n") - hash := status.AsFinalized - err = GetDataFromAvailDA(hash, api, string(data)) - if err != nil { - r.logger.Error("cannot get data:%v", err) - return err - } - return nil - } - case <-timeout: - r.logger.Warn("timeout of 100 seconds reached without getting finalized status for extrinsic") - return nil - } - } -} - -func GetDataFromAvailDA(hash types.Hash, api *gsrpc.SubstrateAPI, data string) error { - block, err := api.RPC.Chain.GetBlock(hash) - if err != nil { - return fmt.Errorf("cannot get block by hash:%w", err) - } - - for _, ext := range block.Block.Extrinsics { - // these values below are specific indexes only for data submission, differs with each extrinsic - if ext.Method.CallIndex.SectionIndex == 29 && ext.Method.CallIndex.MethodIndex == 1 { - arg := ext.Method.Args - str := string(arg) - slice := str[2:] - - fmt.Println("Data retrieved:") - if slice == data { - fmt.Println("Data found in block") - return nil - } - } - } - return fmt.Errorf("data not found") -} diff --git a/simapp/app/app.go b/simapp/app/app.go index d161e4b..f268922 100644 --- a/simapp/app/app.go +++ b/simapp/app/app.go @@ -141,6 +141,9 @@ import ( availblobkeeper "github.com/vitwit/avail-da-module/keeper" availblobmodule "github.com/vitwit/avail-da-module/module" availblobrelayer "github.com/vitwit/avail-da-module/relayer" + "github.com/vitwit/avail-da-module/relayer/avail" + httpclient "github.com/vitwit/avail-da-module/relayer/http" + availblobmoduletypes "github.com/vitwit/avail-da-module/types" ) const ( @@ -681,11 +684,18 @@ func NewChainApp( AddRoute(icahosttypes.SubModuleName, icaHostStack) app.IBCKeeper.SetRouter(ibcRouter) + httpClient := httpclient.NewHandler() + + // Avail-DA client + cfg := availblobmoduletypes.AvailConfigFromAppOpts(appOpts) + availDAClient := avail.NewLightClient(cfg.LightClientURL, httpClient) + app.Availblobrelayer, err = availblobrelayer.NewRelayer( logger, appCodec, - appOpts, + cfg, NodeDir, + availDAClient, ) if err != nil { panic(err)