Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use file input in payForBlobs CLI to allow to submit multiple blobs #2856

Merged
merged 41 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d332413
allow submit multiple blobs in CLI
bao1029p Nov 1, 2023
d66bff5
add print log when fail
bao1029p Nov 7, 2023
a6c4c5e
add file parse for blobs
bao1029p Nov 19, 2023
25324ba
Merge branch 'main' into use_file_input
bao1029p Nov 19, 2023
9f7a870
Merge branch 'main' into use_file_input
bao1029p Nov 21, 2023
967e1e9
Merge branch 'main' into use_file_input
bao1029p Nov 29, 2023
488c541
debugging
sontrinh16 Nov 29, 2023
0be304e
add new proto for blobJSON
sontrinh16 Nov 29, 2023
e6106ca
Merge pull request #1 from decentrio/use_file_input
bao1029p Nov 29, 2023
5ff6bce
minor clean up
bao1029p Nov 29, 2023
0be0e04
minor
bao1029p Nov 29, 2023
64366d6
fix testcase
sontrinh16 Nov 29, 2023
bbc905c
pull upstream
sontrinh16 Nov 29, 2023
c978ada
minor
sontrinh16 Nov 29, 2023
4892a89
Merge branch 'main' into use_file_input
sontrinh16 Nov 30, 2023
db3647e
resolve requested changes
sontrinh16 Nov 30, 2023
79677df
Merge branch 'use_file_input' of https://github.com/bao1029p/celestia…
sontrinh16 Nov 30, 2023
844bd0a
fix lint
sontrinh16 Nov 30, 2023
24f16e6
Merge branch 'main' into use_file_input
sontrinh16 Nov 30, 2023
1ae5e53
add creae json file for testing
sontrinh16 Nov 30, 2023
67cf8a8
Merge branch 'use_file_input' of https://github.com/bao1029p/celestia…
sontrinh16 Nov 30, 2023
762d4f3
minor
sontrinh16 Nov 30, 2023
fb3a4bc
Merge branch 'main' into use_file_input
sontrinh16 Dec 1, 2023
f17a0b3
Merge branch 'main' into use_file_input
sontrinh16 Dec 1, 2023
00ff475
Merge branch 'main' into use_file_input
sontrinh16 Dec 1, 2023
f0e2e9c
remove unnecessary testcase
sontrinh16 Dec 2, 2023
bcccab1
Merge branch 'main' into use_file_input
sontrinh16 Dec 4, 2023
8b5e6c3
resolve state breaking issue by execpt both usage of input
sontrinh16 Dec 4, 2023
84613f0
Merge branch 'use_file_input' of https://github.com/bao1029p/celestia…
sontrinh16 Dec 4, 2023
607e3d7
minor
sontrinh16 Dec 4, 2023
5becf2a
Merge branch 'main' into use_file_input
sontrinh16 Dec 4, 2023
d8fccc9
remove unused flag
sontrinh16 Dec 4, 2023
05b3683
switch to use flag for input file
sontrinh16 Dec 4, 2023
643769d
Merge branch 'use_file_input' of https://github.com/bao1029p/celestia…
sontrinh16 Dec 4, 2023
70f1dd4
minor update
sontrinh16 Dec 4, 2023
37e594d
Merge branch 'main' into use_file_input
sontrinh16 Dec 4, 2023
809e289
minor lint fix
sontrinh16 Dec 4, 2023
dbf7e76
Merge branch 'use_file_input' of https://github.com/bao1029p/celestia…
sontrinh16 Dec 4, 2023
332cc50
minor command description update
sontrinh16 Dec 4, 2023
eb75bef
minor update for consistency
sontrinh16 Dec 4, 2023
5421c88
Update x/blob/client/cli/payforblob.go
rootulp Dec 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading