Skip to content

Commit

Permalink
feat: Use file input in payForBlobs CLI to allow to submit multiple b…
Browse files Browse the repository at this point in the history
…lobs (#2856)

Close [#2434](#2434)

## Changes made
- Add blobJson proto type define how user can submit blob in file
- Add file path as argument input for `payForBlob` command
- Add `parseSubmitBlobs` to parse content from the file to array of
defined `blobJSON`. Each blobJSON contain a namespaceID and blob in hex
encoded format ( can modify what the user can put in the file later on,
this need further conversation on this )
- Modifed the `broadcastPFB` func to accept multiple blobs instead

<!-- 
Please provide an explanation of the PR, including the appropriate
context,
background, goal, and rationale. If there is an issue with this
information,
please provide a tl;dr and link the issue. 
-->

## Testing
test the command with single-node script running and got this result (
thanks @sontrinh16 for providing the test result )

![image](https://github.com/celestiaorg/celestia-app/assets/145053932/720d6ccd-3bcc-4ac3-999d-90cc844cfad4)

using the test_blob.json file contain:
`{
    "Blobs": [
        {
            "namespaceId": "0x00010203040506070809",
            "blob": "0x48656c6c6f2c20576f726c6421"
        },
        {
            "namespaceId": "0x00010203040506070809",
            "blob": "0x48656c6c6f2c20576f726c6421"
        }
    ]
}`
## Checklist

<!-- 
Please complete the checklist to ensure that the PR is ready to be
reviewed.

IMPORTANT:
PRs should be left in Draft until the below checklist is completed.
-->

- [ ] New and updated code has appropriate documentation
- [x] New and updated code has new and/or updated testing
- [x] Required CI checks are passing
- [x] Visual proof for any user facing features like CLI or
documentation updates
- [x] Linked issues closed with keywords

---------

Co-authored-by: sontrinh16 <trinhleson2000@gmail.com>
Co-authored-by: sontrinh16 <48055119+sontrinh16@users.noreply.github.com>
Co-authored-by: Rootul P <rootulp@gmail.com>
  • Loading branch information
4 people authored Dec 5, 2023
1 parent 4e966f9 commit 4d08a7f
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 27 deletions.
128 changes: 104 additions & 24 deletions x/blob/client/cli/payforblob.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"bufio"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/spf13/cobra"
Expand All @@ -28,69 +30,147 @@ const (
// FlagNamespaceVersion allows the user to override the namespace version when
// submitting a PayForBlob.
FlagNamespaceVersion = "namespace-version"

// FlagFileInput allows the user to provide file path to the json file
// for submitting multiple blobs.
FlagFileInput = "input-file"
)

func CmdPayForBlob() *cobra.Command {
cmd := &cobra.Command{
Use: "PayForBlobs namespaceID blob",
Use: "PayForBlobs [namespaceID blob]",
// This example command can be run in a new terminal after running single-node.sh
Example: "celestia-appd tx blob PayForBlobs 0x00010203040506070809 0x48656c6c6f2c20576f726c6421 \\\n" +
"\t--chain-id private \\\n" +
"\t--from validator \\\n" +
"\t--keyring-backend test \\\n" +
"\t--fees 21000utia \\\n" +
"\t--yes",
Short: "Pay for a data blob to be published to Celestia.",
Long: "Pay for a data blob to be published to Celestia.\n" +
"namespaceID is the user-specifiable portion of a version 0 namespace. It must be a hex encoded string of 10 bytes.\n" +
"blob must be a hex encoded string of any length.\n" +
// TODO: allow for more than one blob to be sumbmitted via the CLI
"This command currently only supports a single blob per invocation.\n",
"\t--yes \n\n" +
"celestia-appd tx blob PayForBlobs --input-file path/to/blobs.json \\\n" +
"\t--chain-id private \\\n" +
"\t--from validator \\\n" +
"\t--keyring-backend test \\\n" +
"\t--fees 21000utia \\\n" +
"\t--yes \n",
Short: "Pay for a data blob(s) to be published to Celestia.",
Long: "Pay for a data blob(s) to be published to Celestia.\n" +
"User can use namespaceID and blob as argument for single blob submission \n" +
"or use --input-file flag with the path to a json file for multiple blobs submission, \n" +
`where the json file contains:
{
"Blobs": [
{
"namespaceID": "0x00010203040506070809",
"blob": "0x48656c6c6f2c20576f726c6421"
},
{
"namespaceID": "0x00010203040506070809",
"blob": "0x48656c6c6f2c20576f726c6421"
}
]
}
namespaceID is the user-specifiable portion of a version 0 namespace. It must be a hex encoded string of 10 bytes.\n
blob must be a hex encoded string of any length.\n
`,
Aliases: []string{"PayForBlob"},
Args: func(cmd *cobra.Command, args []string) error {
path, err := cmd.Flags().GetString(FlagFileInput)
if err != nil {
return err
}

// If there is a file path input we'll check for the file extension
if path != "" {
if filepath.Ext(path) != ".json" {
return fmt.Errorf("invalid file extension, require json got %s", filepath.Ext(path))
}

return nil
}

if len(args) < 2 {
return fmt.Errorf("PayForBlobs requires two arguments: namespaceID and blob")
return errors.New("PayForBlobs requires two arguments: namespaceID and blob")
}

return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
arg0 := strings.TrimPrefix(args[0], "0x")
namespaceID, err := hex.DecodeString(arg0)
if err != nil {
return fmt.Errorf("failed to decode hex namespace ID: %w", err)
}
namespaceVersion, err := cmd.Flags().GetUint8(FlagNamespaceVersion)
if err != nil {
return err
}
namespace, err := getNamespace(namespaceID, namespaceVersion)

shareVersion, err := cmd.Flags().GetUint8(FlagShareVersion)
if err != nil {
return err
}

arg1 := strings.TrimPrefix(args[1], "0x")
rawblob, err := hex.DecodeString(arg1)
path, err := cmd.Flags().GetString(FlagFileInput)
if err != nil {
return fmt.Errorf("failure to decode hex blob: %w", err)
return err
}

shareVersion, _ := cmd.Flags().GetUint8(FlagShareVersion)
blob, err := types.NewBlob(namespace, rawblob, shareVersion)
// In case of no file input, get the namespaceID and blob from the arguments
if path == "" {
blob, err := getBlobFromArguments(args[0], args[1], namespaceVersion, shareVersion)
if err != nil {
return err
}

return broadcastPFB(cmd, blob)
}

paresdBlobs, err := parseSubmitBlobs(path)
if err != nil {
return err
}

return broadcastPFB(cmd, blob)
var blobs []*blob.Blob
for _, paresdBlob := range paresdBlobs {
blob, err := getBlobFromArguments(paresdBlob.NamespaceID, paresdBlob.Blob, namespaceVersion, shareVersion)
if err != nil {
return err
}
blobs = append(blobs, blob)
}

return broadcastPFB(cmd, blobs...)
},
}

flags.AddTxFlagsToCmd(cmd)
cmd.PersistentFlags().Uint8(FlagNamespaceVersion, 0, "Specify the namespace version (default 0)")
cmd.PersistentFlags().Uint8(FlagShareVersion, 0, "Specify the share version (default 0)")
cmd.PersistentFlags().String(FlagFileInput, "", "Specify the file input")
_ = cmd.MarkFlagRequired(flags.FlagFrom)
return cmd
}

func getBlobFromArguments(namespaceIDArg, blobArg string, namespaceVersion, shareVersion uint8) (*blob.Blob, error) {
namespaceID, err := hex.DecodeString(strings.TrimPrefix(namespaceIDArg, "0x"))
if err != nil {
return nil, fmt.Errorf("failed to decode hex namespace ID: %w", err)
}
namespace, err := getNamespace(namespaceID, namespaceVersion)
if err != nil {
return nil, err
}
hexStr := strings.TrimPrefix(blobArg, "0x")
rawblob, err := hex.DecodeString(hexStr)
if err != nil {
return nil, fmt.Errorf("failure to decode hex blob value %s: %s", hexStr, err.Error())
}

blob, err := types.NewBlob(namespace, rawblob, shareVersion)
if err != nil {
return nil, fmt.Errorf("failure to create blob with hex blob value %s: %s", hexStr, err.Error())
}

return blob, nil
}

func getNamespace(namespaceID []byte, namespaceVersion uint8) (appns.Namespace, error) {
switch namespaceVersion {
case appns.NamespaceVersionZero:
Expand All @@ -108,15 +188,15 @@ func getNamespace(namespaceID []byte, namespaceVersion uint8) (appns.Namespace,

// broadcastPFB creates the new PFB message type that will later be broadcast to tendermint nodes
// this private func is used in CmdPayForBlob
func broadcastPFB(cmd *cobra.Command, b *blob.Blob) error {
func broadcastPFB(cmd *cobra.Command, b ...*blob.Blob) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

// TODO: allow the user to override the share version via a new flag
// See https://github.com/celestiaorg/celestia-app/issues/1041
pfbMsg, err := types.NewMsgPayForBlobs(clientCtx.FromAddress.String(), b)
pfbMsg, err := types.NewMsgPayForBlobs(clientCtx.FromAddress.String(), b...)
if err != nil {
return err
}
Expand All @@ -131,7 +211,7 @@ func broadcastPFB(cmd *cobra.Command, b *blob.Blob) error {
return err
}

blobTx, err := blob.MarshalBlobTx(txBytes, b)
blobTx, err := blob.MarshalBlobTx(txBytes, b...)
if err != nil {
return err
}
Expand Down
32 changes: 32 additions & 0 deletions x/blob/client/cli/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package cli

import (
"encoding/json"
"os"
)

// Define the raw content from the file input.
type blobs struct {
Blobs []blobJSON
}

type blobJSON struct {
NamespaceID string
Blob string
}

func parseSubmitBlobs(path string) ([]blobJSON, error) {
var rawBlobs blobs

content, err := os.ReadFile(path)
if err != nil {
return []blobJSON{}, err
}

err = json.Unmarshal(content, &rawBlobs)
if err != nil {
return []blobJSON{}, err
}

return rawBlobs.Blobs, err
}
74 changes: 71 additions & 3 deletions x/blob/client/testutil/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package testutil
import (
"encoding/hex"
"fmt"
"os"
"strconv"
"testing"

"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/gogo/protobuf/proto"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"

clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
Expand All @@ -35,6 +37,29 @@ type IntegrationTestSuite struct {
kr keyring.Keyring
}

// Create a .json file for testing
func createTestFile(t testing.TB, s string, isValid bool) *os.File {
t.Helper()

tempdir, err := os.MkdirTemp("", "")
require.NoError(t, err)
t.Cleanup(func() { _ = os.RemoveAll(tempdir) })

var fp *os.File

if isValid {
fp, err = os.CreateTemp(tempdir, "*.json")
} else {
fp, err = os.CreateTemp(tempdir, "")
}
require.NoError(t, err)
_, err = fp.WriteString(s)

require.Nil(t, err)

return fp
}

func NewIntegrationTestSuite(cfg cosmosnet.Config) *IntegrationTestSuite {
return &IntegrationTestSuite{cfg: cfg}
}
Expand All @@ -57,9 +82,26 @@ func (s *IntegrationTestSuite) TearDownSuite() {
func (s *IntegrationTestSuite) TestSubmitPayForBlob() {
require := s.Require()
validator := s.network.Validators[0]
hexNamespace := hex.EncodeToString(appns.RandomBlobNamespaceID())

hexBlob := "0204033704032c0b162109000908094d425837422c2116"

validBlob := fmt.Sprintf(`
{
"Blobs": [
{
"namespaceID": "%s",
"blob": "%s"
},
{
"namespaceID": "%s",
"blob": "%s"
}
]
}
`, hex.EncodeToString(appns.RandomBlobNamespaceID()), hexBlob, hex.EncodeToString(appns.RandomBlobNamespaceID()), hexBlob)
validPropFile := createTestFile(s.T(), validBlob, true)
invalidPropFile := createTestFile(s.T(), validBlob, false)

testCases := []struct {
name string
args []string
Expand All @@ -68,9 +110,9 @@ func (s *IntegrationTestSuite) TestSubmitPayForBlob() {
respType proto.Message
}{
{
name: "valid transaction",
name: "single blob valid transaction",
args: []string{
hexNamespace,
hex.EncodeToString(appns.RandomBlobNamespaceID()),
hexBlob,
fmt.Sprintf("--from=%s", username),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
Expand All @@ -81,6 +123,32 @@ func (s *IntegrationTestSuite) TestSubmitPayForBlob() {
expectedCode: 0,
respType: &sdk.TxResponse{},
},
{
name: "multiple blobs valid transaction",
args: []string{
fmt.Sprintf("--from=%s", username),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(2))).String()),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", paycli.FlagFileInput, validPropFile.Name()),
},
expectErr: false,
expectedCode: 0,
respType: &sdk.TxResponse{},
},
{
name: "multiple blobs with invalid file path extension",
args: []string{
fmt.Sprintf("--from=%s", username),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(2))).String()),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", paycli.FlagFileInput, invalidPropFile.Name()),
},
expectErr: true,
expectedCode: 0,
respType: &sdk.TxResponse{},
},
}

for _, tc := range testCases {
Expand Down

0 comments on commit 4d08a7f

Please sign in to comment.