diff --git a/.github/workflows/ci_release.yml b/.github/workflows/ci_release.yml index 8efcf094b9..35c6c65550 100644 --- a/.github/workflows/ci_release.yml +++ b/.github/workflows/ci_release.yml @@ -146,9 +146,8 @@ jobs: contents: write steps: - uses: actions/checkout@v4 - + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | make openrpc-gen > openrpc.json gh release upload ${{github.event.release.tag_name}} openrpc.json - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/api/docgen/examples.go b/api/docgen/examples.go index c4f1e867bf..f78d29543b 100644 --- a/api/docgen/examples.go +++ b/api/docgen/examples.go @@ -60,7 +60,6 @@ var ExampleValues = map[reflect.Type]interface{}{ reflect.TypeOf(42): 42, reflect.TypeOf(byte(7)): byte(7), reflect.TypeOf(float64(42)): float64(42), - reflect.TypeOf(blob.GasPrice(0)): blob.GasPrice(0.002), reflect.TypeOf(true): true, reflect.TypeOf([]byte{}): []byte("byte array"), reflect.TypeOf(node.Full): node.Full, @@ -177,8 +176,16 @@ func init() { if err != nil { panic(err) } - addToExampleValues(libhead.Hash(hash)) + + txConfig := state.NewTxConfig( + state.WithGasPrice(0.002), + state.WithGas(142225), + state.WithKeyName("my_celes_key"), + state.WithSignerAddress("celestia1pjcmwj8w6hyr2c4wehakc5g8cfs36aysgucx66"), + state.WithFeeGranterAddress("celestia1hakc56ax66ypjcmwj8w6hyr2c4g8cfs3wesguc"), + ) + addToExampleValues(txConfig) } func addToExampleValues(v interface{}) { diff --git a/api/gateway/bindings.go b/api/gateway/bindings.go index c01bd2da47..257a20e013 100644 --- a/api/gateway/bindings.go +++ b/api/gateway/bindings.go @@ -13,12 +13,6 @@ func (h *Handler) RegisterEndpoints(rpc *Server) { http.MethodGet, ) - rpc.RegisterHandlerFunc( - submitTxEndpoint, - h.handleSubmitTx, - http.MethodPost, - ) - rpc.RegisterHandlerFunc( healthEndpoint, h.handleHealthRequest, diff --git a/api/gateway/bindings_test.go b/api/gateway/bindings_test.go index e942c174bd..338948c584 100644 --- a/api/gateway/bindings_test.go +++ b/api/gateway/bindings_test.go @@ -27,12 +27,6 @@ func TestRegisterEndpoints(t *testing.T) { method: http.MethodGet, expected: true, }, - { - name: "Submit transaction endpoint", - path: submitTxEndpoint, - method: http.MethodPost, - expected: true, - }, { name: "Get namespaced shares by height endpoint", path: fmt.Sprintf("%s/{%s}/height/{%s}", namespacedSharesEndpoint, namespaceKey, heightKey), diff --git a/api/gateway/state.go b/api/gateway/state.go index 13cf729cc6..ee4708f951 100644 --- a/api/gateway/state.go +++ b/api/gateway/state.go @@ -1,7 +1,6 @@ package gateway import ( - "encoding/hex" "encoding/json" "errors" "net/http" @@ -13,8 +12,7 @@ import ( ) const ( - balanceEndpoint = "/balance" - submitTxEndpoint = "/submit_tx" + balanceEndpoint = "/balance" ) const addrKey = "address" @@ -24,11 +22,6 @@ var ( ErrMissingAddress = errors.New("address not specified") ) -// submitTxRequest represents a request to submit a raw transaction -type submitTxRequest struct { - Tx string `json:"tx"` -} - func (h *Handler) handleBalanceRequest(w http.ResponseWriter, r *http.Request) { var ( bal *state.Balance @@ -70,33 +63,3 @@ func (h *Handler) handleBalanceRequest(w http.ResponseWriter, r *http.Request) { log.Errorw("writing response", "endpoint", balanceEndpoint, "err", err) } } - -func (h *Handler) handleSubmitTx(w http.ResponseWriter, r *http.Request) { - // decode request - var req submitTxRequest - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - writeError(w, http.StatusBadRequest, submitTxEndpoint, err) - return - } - rawTx, err := hex.DecodeString(req.Tx) - if err != nil { - writeError(w, http.StatusBadRequest, submitTxEndpoint, err) - return - } - // perform request - txResp, err := h.state.SubmitTx(r.Context(), rawTx) - if err != nil { - writeError(w, http.StatusInternalServerError, submitTxEndpoint, err) - return - } - resp, err := json.Marshal(txResp) - if err != nil { - writeError(w, http.StatusInternalServerError, submitTxEndpoint, err) - return - } - _, err = w.Write(resp) - if err != nil { - log.Errorw("writing response", "endpoint", submitTxEndpoint, "err", err) - } -} diff --git a/api/rpc_test.go b/api/rpc_test.go index eccd84148e..29191b93a2 100644 --- a/api/rpc_test.go +++ b/api/rpc_test.go @@ -200,14 +200,14 @@ func TestAuthedRPC(t *testing.T) { expectedResp := &state.TxResponse{} if tt.perm > 2 { server.State.EXPECT().Delegate(gomock.Any(), gomock.Any(), - gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedResp, nil) + gomock.Any(), gomock.Any()).Return(expectedResp, nil) txResp, err := rpcClient.State.Delegate(ctx, - state.ValAddress{}, state.Int{}, state.Int{}, 0) + state.ValAddress{}, state.Int{}, state.NewTxConfig()) require.NoError(t, err) require.Equal(t, expectedResp, txResp) } else { _, err := rpcClient.State.Delegate(ctx, - state.ValAddress{}, state.Int{}, state.Int{}, 0) + state.ValAddress{}, state.Int{}, state.NewTxConfig()) require.Error(t, err) require.ErrorContains(t, err, "missing permission") } diff --git a/blob/helper.go b/blob/helper.go index d3b418e8ba..c1de773c24 100644 --- a/blob/helper.go +++ b/blob/helper.go @@ -7,6 +7,7 @@ import ( "github.com/tendermint/tendermint/types" "github.com/celestiaorg/celestia-app/pkg/shares" + apptypes "github.com/celestiaorg/celestia-app/x/blob/types" "github.com/celestiaorg/celestia-node/share" ) @@ -36,6 +37,15 @@ func BlobsToShares(blobs ...*Blob) ([]share.Share, error) { return shares.ToBytes(rawShares), nil } +// ToAppBlobs converts node's blob type to the blob type from celestia-app. +func ToAppBlobs(blobs ...*Blob) []*apptypes.Blob { + appBlobs := make([]*apptypes.Blob, 0, len(blobs)) + for i := range blobs { + appBlobs[i] = &blobs[i].Blob + } + return appBlobs +} + // toAppShares converts node's raw shares to the app shares, skipping padding func toAppShares(shrs ...share.Share) ([]shares.Share, error) { appShrs := make([]shares.Share, 0, len(shrs)) diff --git a/blob/service.go b/blob/service.go index 25bf48fd1f..3a35c140f2 100644 --- a/blob/service.go +++ b/blob/service.go @@ -4,25 +4,22 @@ import ( "context" "errors" "fmt" - "math" + "slices" "sync" - sdkmath "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/types" - auth "github.com/cosmos/cosmos-sdk/x/auth/types" logging "github.com/ipfs/go-log/v2" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" - "github.com/celestiaorg/celestia-app/pkg/appconsts" "github.com/celestiaorg/celestia-app/pkg/shares" - blobtypes "github.com/celestiaorg/celestia-app/x/blob/types" "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/libs/utils" "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/state" ) var ( @@ -33,21 +30,15 @@ var ( tracer = otel.Tracer("blob/service") ) -// GasPrice represents the amount to be paid per gas unit. Fee is set by -// multiplying GasPrice by GasLimit, which is determined by the blob sizes. -type GasPrice float64 - -// DefaultGasPrice returns the default gas price, letting node automatically -// determine the Fee based on the passed blob sizes. -func DefaultGasPrice() GasPrice { - return -1.0 -} +// SubmitOptions aliases TxOptions from state package allowing users +// to specify options for SubmitPFB transaction. +type SubmitOptions = state.TxConfig // Submitter is an interface that allows submitting blobs to the celestia-core. It is used to // avoid a circular dependency between the blob and the state package, since the state package needs // the blob.Blob type for this signature. type Submitter interface { - SubmitPayForBlob(ctx context.Context, fee sdkmath.Int, gasLim uint64, blobs []*Blob) (*types.TxResponse, error) + SubmitPayForBlob(context.Context, []*state.Blob, *state.TxConfig) (*types.TxResponse, error) } type Service struct { @@ -71,48 +62,32 @@ func NewService( } } -// SubmitOptions contains the information about fee and gasLimit price in order to configure the -// Submit request. -type SubmitOptions struct { - Fee int64 - GasLimit uint64 -} - -// DefaultSubmitOptions creates a default fee and gas price values. -func DefaultSubmitOptions() *SubmitOptions { - return &SubmitOptions{ - Fee: -1, - GasLimit: 0, - } -} - // Submit sends PFB transaction and reports the height at which it was included. // Allows sending multiple Blobs atomically synchronously. // Uses default wallet registered on the Node. // Handles gas estimation and fee calculation. -func (s *Service) Submit(ctx context.Context, blobs []*Blob, gasPrice GasPrice) (uint64, error) { +func (s *Service) Submit(ctx context.Context, blobs []*Blob, txConfig *SubmitOptions) (uint64, error) { log.Debugw("submitting blobs", "amount", len(blobs)) - options := DefaultSubmitOptions() - if gasPrice >= 0 { - blobSizes := make([]uint32, len(blobs)) - for i, blob := range blobs { - blobSizes[i] = uint32(len(blob.Data)) + appblobs := make([]*state.Blob, len(blobs)) + for i := range blobs { + if err := blobs[i].Namespace().ValidateForBlob(); err != nil { + return 0, err } - options.GasLimit = blobtypes.EstimateGas(blobSizes, appconsts.DefaultGasPerBlobByte, auth.DefaultTxSizeCostPerByte) - options.Fee = types.NewInt(int64(math.Ceil(float64(gasPrice) * float64(options.GasLimit)))).Int64() + appblobs[i] = &blobs[i].Blob } - resp, err := s.blobSubmitter.SubmitPayForBlob(ctx, types.NewInt(options.Fee), options.GasLimit, blobs) + resp, err := s.blobSubmitter.SubmitPayForBlob(ctx, appblobs, txConfig) if err != nil { return 0, err } return uint64(resp.Height), nil } -// Get retrieves all the blobs for given namespaces at the given height by commitment. -// Get collects all namespaced data from the EDS, constructs blobs -// and compares commitments. `ErrBlobNotFound` can be returned in case blob was not found. +// Get retrieves a blob in a given namespace at the given height by commitment. +// Get collects all namespaced data from the EDS, construct the blob +// and compares the commitment argument. +// `ErrBlobNotFound` can be returned in case blob was not found. func (s *Service) Get( ctx context.Context, height uint64, @@ -136,9 +111,9 @@ func (s *Service) Get( return } -// GetProof retrieves all blobs in the given namespaces at the given height by commitment -// and returns their Proof. It collects all namespaced data from the EDS, constructs blobs -// and compares commitments. +// GetProof returns an NMT inclusion proof for a specified namespace to the respective row roots +// on which the blob spans on at the given height, using the given commitment. +// It employs the same algorithm as service.Get() internally. func (s *Service) GetProof( ctx context.Context, height uint64, @@ -163,7 +138,14 @@ func (s *Service) GetProof( } // GetAll returns all blobs under the given namespaces at the given height. -// GetAll can return blobs and an error in case if some requests failed. +// If all blobs were found without any errors, the user will receive a list of blobs. +// If the BlobService couldn't find any blobs under the requested namespaces, +// the user will receive an empty list of blobs along with an empty error. +// If some of the requested namespaces were not found, the user will receive all the found blobs and an empty error. +// If there were internal errors during some of the requests, +// the user will receive all found blobs along with a combined error message. +// +// All blobs will preserve the order of the namespaces that were requested. func (s *Service) GetAll(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*Blob, error) { header, err := s.headerGetter(ctx, height) if err != nil { @@ -173,40 +155,30 @@ func (s *Service) GetAll(ctx context.Context, height uint64, namespaces []share. var ( resultBlobs = make([][]*Blob, len(namespaces)) resultErr = make([]error, len(namespaces)) + wg = sync.WaitGroup{} ) - - for _, namespace := range namespaces { - log.Debugw("performing GetAll request", "namespace", namespace.String(), "height", height) - } - - wg := sync.WaitGroup{} for i, namespace := range namespaces { wg.Add(1) go func(i int, namespace share.Namespace) { + log.Debugw("retrieving all blobs from", "namespace", namespace.String(), "height", height) defer wg.Done() + blobs, err := s.getBlobs(ctx, namespace, header) - if err != nil { - resultErr[i] = fmt.Errorf("getting blobs for namespace(%s): %w", namespace.String(), err) - return + if err != nil && !errors.Is(err, ErrBlobNotFound) { + log.Errorf("getting blobs for namespaceID(%s): %v", namespace.ID().String(), err) + resultErr[i] = err + } + if len(blobs) > 0 { + log.Infow("retrieved blobs", "height", height, "total", len(blobs)) + resultBlobs[i] = blobs } - - log.Debugw("receiving blobs", "height", height, "total", len(blobs)) - resultBlobs[i] = blobs }(i, namespace) } wg.Wait() - blobs := make([]*Blob, 0) - for _, resBlobs := range resultBlobs { - if len(resBlobs) > 0 { - blobs = append(blobs, resBlobs...) - } - } - - if len(blobs) == 0 { - resultErr = append(resultErr, ErrBlobNotFound) - } - return blobs, errors.Join(resultErr...) + blobs := slices.Concat(resultBlobs...) + err = errors.Join(resultErr...) + return blobs, err } // Included verifies that the blob was included in a specific height. @@ -413,8 +385,5 @@ func (s *Service) getBlobs( sharesParser := &parser{verifyFn: verifyFn} _, _, err = s.retrieve(ctx, header.Height(), namespace, sharesParser) - if len(blobs) == 0 { - return nil, ErrBlobNotFound - } - return blobs, nil + return blobs, err } diff --git a/blob/service_test.go b/blob/service_test.go index 7a99e92e06..fc48f96eb9 100644 --- a/blob/service_test.go +++ b/blob/service_test.go @@ -4,11 +4,13 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "sort" "testing" "time" + "github.com/golang/mock/gomock" ds "github.com/ipfs/go-datastore" ds_sync "github.com/ipfs/go-datastore/sync" "github.com/stretchr/testify/assert" @@ -16,12 +18,14 @@ import ( tmrand "github.com/tendermint/tendermint/libs/rand" "github.com/celestiaorg/celestia-app/pkg/appconsts" + appns "github.com/celestiaorg/celestia-app/pkg/namespace" "github.com/celestiaorg/celestia-app/pkg/shares" "github.com/celestiaorg/go-header/store" "github.com/celestiaorg/celestia-node/blob/blobtest" "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/header/headertest" + shareMock "github.com/celestiaorg/celestia-node/nodebuilder/share/mocks" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/getters" "github.com/celestiaorg/celestia-node/share/ipld" @@ -39,15 +43,15 @@ func TestBlobService_Get(t *testing.T) { appBlobs, err := blobtest.GenerateV0Blobs([]int{blobSize0, blobSize1}, false) require.NoError(t, err) - blobs0, err := convertBlobs(appBlobs...) + blobsWithDiffNamespaces, err := convertBlobs(appBlobs...) require.NoError(t, err) appBlobs, err = blobtest.GenerateV0Blobs([]int{blobSize2, blobSize3}, true) require.NoError(t, err) - blobs1, err := convertBlobs(appBlobs...) + blobsWithSameNamespace, err := convertBlobs(appBlobs...) require.NoError(t, err) - service := createService(ctx, t, append(blobs0, blobs1...)) + service := createService(ctx, t, append(blobsWithDiffNamespaces, blobsWithSameNamespace...)) test := []struct { name string doFn func() (interface{}, error) @@ -56,7 +60,10 @@ func TestBlobService_Get(t *testing.T) { { name: "get single blob", doFn: func() (interface{}, error) { - b, err := service.Get(ctx, 1, blobs0[0].Namespace(), blobs0[0].Commitment) + b, err := service.Get(ctx, 1, + blobsWithDiffNamespaces[0].Namespace(), + blobsWithDiffNamespaces[0].Commitment, + ) return []*Blob{b}, err }, expectedResult: func(res interface{}, err error) { @@ -67,13 +74,13 @@ func TestBlobService_Get(t *testing.T) { assert.True(t, ok) assert.Len(t, blobs, 1) - assert.Equal(t, blobs0[0].Commitment, blobs[0].Commitment) + assert.Equal(t, blobsWithDiffNamespaces[0].Commitment, blobs[0].Commitment) }, }, { name: "get all with the same namespace", doFn: func() (interface{}, error) { - return service.GetAll(ctx, 1, []share.Namespace{blobs1[0].Namespace()}) + return service.GetAll(ctx, 1, []share.Namespace{blobsWithSameNamespace[0].Namespace()}) }, expectedResult: func(res interface{}, err error) { require.NoError(t, err) @@ -84,19 +91,25 @@ func TestBlobService_Get(t *testing.T) { assert.Len(t, blobs, 2) - for i := range blobs1 { - require.Equal(t, blobs1[i].Commitment, blobs[i].Commitment) + for i := range blobsWithSameNamespace { + require.Equal(t, blobsWithSameNamespace[i].Commitment, blobs[i].Commitment) } }, }, { name: "verify indexes", doFn: func() (interface{}, error) { - b0, err := service.Get(ctx, 1, blobs0[0].Namespace(), blobs0[0].Commitment) + b0, err := service.Get(ctx, 1, + blobsWithDiffNamespaces[0].Namespace(), + blobsWithDiffNamespaces[0].Commitment, + ) require.NoError(t, err) - b1, err := service.Get(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + b1, err := service.Get(ctx, 1, + blobsWithDiffNamespaces[1].Namespace(), + blobsWithDiffNamespaces[1].Commitment, + ) require.NoError(t, err) - b23, err := service.GetAll(ctx, 1, []share.Namespace{blobs1[0].Namespace()}) + b23, err := service.GetAll(ctx, 1, []share.Namespace{blobsWithSameNamespace[0].Namespace()}) require.NoError(t, err) return []*Blob{b0, b1, b23[0], b23[1]}, nil }, @@ -132,7 +145,14 @@ func TestBlobService_Get(t *testing.T) { { name: "get all with different namespaces", doFn: func() (interface{}, error) { - b, err := service.GetAll(ctx, 1, []share.Namespace{blobs0[0].Namespace(), blobs0[1].Namespace()}) + nid, err := share.NewBlobNamespaceV0(tmrand.Bytes(7)) + require.NoError(t, err) + b, err := service.GetAll(ctx, 1, + []share.Namespace{ + blobsWithDiffNamespaces[0].Namespace(), nid, + blobsWithDiffNamespaces[1].Namespace(), + }, + ) return b, err }, expectedResult: func(res interface{}, err error) { @@ -141,17 +161,19 @@ func TestBlobService_Get(t *testing.T) { blobs, ok := res.([]*Blob) assert.True(t, ok) assert.NotEmpty(t, blobs) - assert.Len(t, blobs, 2) // check the order - require.True(t, bytes.Equal(blobs[0].Namespace(), blobs0[0].Namespace())) - require.True(t, bytes.Equal(blobs[1].Namespace(), blobs0[1].Namespace())) + require.True(t, bytes.Equal(blobs[0].Namespace(), blobsWithDiffNamespaces[0].Namespace())) + require.True(t, bytes.Equal(blobs[1].Namespace(), blobsWithDiffNamespaces[1].Namespace())) }, }, { name: "get blob with incorrect commitment", doFn: func() (interface{}, error) { - b, err := service.Get(ctx, 1, blobs0[0].Namespace(), blobs0[1].Commitment) + b, err := service.Get(ctx, 1, + blobsWithDiffNamespaces[0].Namespace(), + blobsWithDiffNamespaces[1].Commitment, + ) return []*Blob{b}, err }, expectedResult: func(res interface{}, err error) { @@ -184,7 +206,10 @@ func TestBlobService_Get(t *testing.T) { { name: "get proof", doFn: func() (interface{}, error) { - proof, err := service.GetProof(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + proof, err := service.GetProof(ctx, 1, + blobsWithDiffNamespaces[1].Namespace(), + blobsWithDiffNamespaces[1].Commitment, + ) return proof, err }, expectedResult: func(res interface{}, err error) { @@ -211,17 +236,24 @@ func TestBlobService_Get(t *testing.T) { t.Fatal("could not prove the shares") } - rawShares, err := BlobsToShares(blobs0[1]) + rawShares, err := BlobsToShares(blobsWithDiffNamespaces[1]) require.NoError(t, err) - verifyFn(t, rawShares, proof, blobs0[1].Namespace()) + verifyFn(t, rawShares, proof, blobsWithDiffNamespaces[1].Namespace()) }, }, { name: "verify inclusion", doFn: func() (interface{}, error) { - proof, err := service.GetProof(ctx, 1, blobs0[0].Namespace(), blobs0[0].Commitment) + proof, err := service.GetProof(ctx, 1, + blobsWithDiffNamespaces[0].Namespace(), + blobsWithDiffNamespaces[0].Commitment, + ) require.NoError(t, err) - return service.Included(ctx, 1, blobs0[0].Namespace(), proof, blobs0[0].Commitment) + return service.Included(ctx, 1, + blobsWithDiffNamespaces[0].Namespace(), + proof, + blobsWithDiffNamespaces[0].Commitment, + ) }, expectedResult: func(res interface{}, err error) { require.NoError(t, err) @@ -233,9 +265,16 @@ func TestBlobService_Get(t *testing.T) { { name: "verify inclusion fails with different proof", doFn: func() (interface{}, error) { - proof, err := service.GetProof(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + proof, err := service.GetProof(ctx, 1, + blobsWithDiffNamespaces[1].Namespace(), + blobsWithDiffNamespaces[1].Commitment, + ) require.NoError(t, err) - return service.Included(ctx, 1, blobs0[0].Namespace(), proof, blobs0[0].Commitment) + return service.Included(ctx, 1, + blobsWithDiffNamespaces[0].Namespace(), + proof, + blobsWithDiffNamespaces[0].Commitment, + ) }, expectedResult: func(res interface{}, err error) { require.Error(t, err) @@ -253,7 +292,10 @@ func TestBlobService_Get(t *testing.T) { blob, err := convertBlobs(appBlob...) require.NoError(t, err) - proof, err := service.GetProof(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + proof, err := service.GetProof(ctx, 1, + blobsWithDiffNamespaces[1].Namespace(), + blobsWithDiffNamespaces[1].Commitment, + ) require.NoError(t, err) return service.Included(ctx, 1, blob[0].Namespace(), proof, blob[0].Commitment) }, @@ -267,11 +309,17 @@ func TestBlobService_Get(t *testing.T) { { name: "count proofs for the blob", doFn: func() (interface{}, error) { - proof0, err := service.GetProof(ctx, 1, blobs0[0].Namespace(), blobs0[0].Commitment) + proof0, err := service.GetProof(ctx, 1, + blobsWithDiffNamespaces[0].Namespace(), + blobsWithDiffNamespaces[0].Commitment, + ) if err != nil { return nil, err } - proof1, err := service.GetProof(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + proof1, err := service.GetProof(ctx, 1, + blobsWithDiffNamespaces[1].Namespace(), + blobsWithDiffNamespaces[1].Commitment, + ) if err != nil { return nil, err } @@ -293,23 +341,26 @@ func TestBlobService_Get(t *testing.T) { }, }, { - name: "get all not found", + name: "empty result and err when blobs were not found ", doFn: func() (interface{}, error) { - namespace := share.Namespace(tmrand.Bytes(share.NamespaceSize)) - return service.GetAll(ctx, 1, []share.Namespace{namespace}) + nid, err := share.NewBlobNamespaceV0(tmrand.Bytes(appns.NamespaceVersionZeroIDSize)) + require.NoError(t, err) + return service.GetAll(ctx, 1, []share.Namespace{nid}) }, expectedResult: func(i interface{}, err error) { blobs, ok := i.([]*Blob) require.True(t, ok) - assert.Empty(t, blobs) - require.Error(t, err) - require.ErrorIs(t, err, ErrBlobNotFound) + assert.Nil(t, blobs) + assert.NoError(t, err) }, }, { name: "marshal proof", doFn: func() (interface{}, error) { - proof, err := service.GetProof(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + proof, err := service.GetProof(ctx, 1, + blobsWithDiffNamespaces[1].Namespace(), + blobsWithDiffNamespaces[1].Commitment, + ) require.NoError(t, err) return json.Marshal(proof) }, @@ -320,11 +371,48 @@ func TestBlobService_Get(t *testing.T) { var proof Proof require.NoError(t, json.Unmarshal(jsonData, &proof)) - newProof, err := service.GetProof(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + newProof, err := service.GetProof(ctx, 1, + blobsWithDiffNamespaces[1].Namespace(), + blobsWithDiffNamespaces[1].Commitment, + ) require.NoError(t, err) require.NoError(t, proof.equal(*newProof)) }, }, + { + name: "internal error", + doFn: func() (interface{}, error) { + ctrl := gomock.NewController(t) + shareService := service.shareGetter + shareGetterMock := shareMock.NewMockModule(ctrl) + shareGetterMock.EXPECT(). + GetSharesByNamespace(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, h *header.ExtendedHeader, ns share.Namespace) (share.NamespacedShares, error) { + if ns.Equals(blobsWithDiffNamespaces[0].Namespace()) { + return nil, errors.New("internal error") + } + return shareService.GetSharesByNamespace(ctx, h, ns) + }).AnyTimes() + + service.shareGetter = shareGetterMock + return service.GetAll(ctx, 1, + []share.Namespace{ + blobsWithDiffNamespaces[0].Namespace(), + blobsWithSameNamespace[0].Namespace(), + }, + ) + }, + expectedResult: func(res interface{}, err error) { + blobs, ok := res.([]*Blob) + assert.True(t, ok) + assert.Error(t, err) + assert.Contains(t, err.Error(), "internal error") + assert.Equal(t, blobs[0].Namespace(), blobsWithSameNamespace[0].Namespace()) + assert.NotEmpty(t, blobs) + assert.Len(t, blobs, len(blobsWithSameNamespace)) + }, + }, } for _, tt := range test { @@ -522,8 +610,9 @@ func TestAllPaddingSharesInEDS(t *testing.T) { } service := NewService(nil, getters.NewIPLDGetter(bs), fn) - _, err = service.GetAll(ctx, 1, []share.Namespace{nid}) - require.Error(t, err) + newBlobs, err := service.GetAll(ctx, 1, []share.Namespace{nid}) + require.NoError(t, err) + assert.Empty(t, newBlobs) } func TestSkipPaddingsAndRetrieveBlob(t *testing.T) { diff --git a/cmd/start.go b/cmd/start.go index c3ee37fe72..66a2a44a4e 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -38,7 +38,7 @@ Options passed on start override configuration options only on start and are not // TODO @renaynay: Include option for setting custom `userInput` parameter with // implementation of https://github.com/celestiaorg/celestia-node/issues/415. encConf := encoding.MakeConfig(app.ModuleEncodingRegisters...) - ring, err := keyring.New(app.Name, cfg.State.KeyringBackend, keysPath, os.Stdin, encConf.Codec) + ring, err := keyring.New(app.Name, cfg.State.DefaultBackendName, keysPath, os.Stdin, encConf.Codec) if err != nil { return err } diff --git a/cmd/util.go b/cmd/util.go index 805f67b7eb..750b9d9ae5 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -111,11 +111,7 @@ func PersistentPreRunEnv(cmd *cobra.Command, nodeType node.Type, _ []string) err return err } - err = state.ParseFlags(cmd, &cfg.State) - if err != nil { - return err - } - + state.ParseFlags(cmd, &cfg.State) rpc_cfg.ParseFlags(cmd, &cfg.RPC) gateway.ParseFlags(cmd, &cfg.Gateway) diff --git a/docs/adr/adr-009-public-api.md b/docs/adr/adr-009-public-api.md index be9e8827eb..e449025b53 100644 --- a/docs/adr/adr-009-public-api.md +++ b/docs/adr/adr-009-public-api.md @@ -253,8 +253,7 @@ NetworkHead(ctx context.Context) (*header.ExtendedHeader, error) ctx context.Context, nID namespace.ID, data []byte, - fee types.Int, - gasLimit uint64, + config *state.TxConfig, ) (*state.TxResponse, error) // Transfer sends the given amount of coins from default wallet of the node // to the given account address. @@ -262,8 +261,7 @@ NetworkHead(ctx context.Context) (*header.ExtendedHeader, error) ctx context.Context, to types.Address, amount types.Int, - fee types.Int, - gasLimit uint64, + config *state.TxConfig, ) (*state.TxResponse, error) // StateModule also provides StakingModule @@ -284,8 +282,7 @@ yet. ctx context.Context, delAddr state.ValAddress, amount state.Int, - fee types.Int, - gasLim uint64, + config *state.TxConfig, ) (*state.TxResponse, error) // BeginRedelegate sends a user's delegated tokens to a new validator for redelegation. BeginRedelegate( @@ -293,8 +290,7 @@ yet. srcValAddr, dstValAddr state.ValAddress, amount state.Int, - fee types.Int, - gasLim uint64, + config *state.TxConfig, ) (*state.TxResponse, error) // Undelegate undelegates a user's delegated tokens, unbonding them from the // current validator. @@ -302,8 +298,7 @@ yet. ctx context.Context, delAddr state.ValAddress, amount state.Int, - fee types.Int, - gasLim uint64, + config *state.TxConfig, ) (*state.TxResponse, error) // CancelUnbondingDelegation cancels a user's pending undelegation from a @@ -313,8 +308,7 @@ yet. valAddr state.ValAddress, amount types.Int, height types.Int, - fee types.Int, - gasLim uint64, + config *state.TxConfig, ) (*state.TxResponse, error) // QueryDelegation retrieves the delegation information between a delegator @@ -404,17 +398,16 @@ type BankModule interface { // SubmitPayForBlob builds, signs and submits a PayForBlob transaction. SubmitPayForBlob( ctx context.Context, - nID namespace.ID, - data []byte, - gasLimit uint64, + blobs []*state.Blob, + config *state.TxConfig, ) (*state.TxResponse, error) // Transfer sends the given amount of coins from default wallet of the node // to the given account address. Transfer( ctx context.Context, - to types.Address, - amount types.Int, - gasLimit uint64, + to state.AccAddress, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) } ``` diff --git a/nodebuilder/blob/blob.go b/nodebuilder/blob/blob.go index f87105541a..c4c0352516 100644 --- a/nodebuilder/blob/blob.go +++ b/nodebuilder/blob/blob.go @@ -16,10 +16,18 @@ type Module interface { // Submit sends Blobs and reports the height in which they were included. // Allows sending multiple Blobs atomically synchronously. // Uses default wallet registered on the Node. - Submit(_ context.Context, _ []*blob.Blob, _ blob.GasPrice) (height uint64, _ error) + Submit(_ context.Context, _ []*blob.Blob, _ *blob.SubmitOptions) (height uint64, _ error) // Get retrieves the blob by commitment under the given namespace and height. Get(_ context.Context, height uint64, _ share.Namespace, _ blob.Commitment) (*blob.Blob, error) - // GetAll returns all blobs at the given height under the given namespaces. + // GetAll returns all blobs under the given namespaces at the given height. + // If all blobs were found without any errors, the user will receive a list of blobs. + // If the BlobService couldn't find any blobs under the requested namespaces, + // the user will receive an empty list of blobs along with an empty error. + // If some of the requested namespaces were not found, the user will receive all the found blobs and an empty error. + // If there were internal errors during some of the requests, + // the user will receive all found blobs along with a combined error message. + // + // All blobs will preserve the order of the namespaces that were requested. GetAll(_ context.Context, height uint64, _ []share.Namespace) ([]*blob.Blob, error) // GetProof retrieves proofs in the given namespaces at the given height by commitment. GetProof(_ context.Context, height uint64, _ share.Namespace, _ blob.Commitment) (*blob.Proof, error) @@ -30,7 +38,7 @@ type Module interface { type API struct { Internal struct { - Submit func(context.Context, []*blob.Blob, blob.GasPrice) (uint64, error) `perm:"write"` + Submit func(context.Context, []*blob.Blob, *blob.SubmitOptions) (uint64, error) `perm:"write"` Get func(context.Context, uint64, share.Namespace, blob.Commitment) (*blob.Blob, error) `perm:"read"` GetAll func(context.Context, uint64, []share.Namespace) ([]*blob.Blob, error) `perm:"read"` GetProof func(context.Context, uint64, share.Namespace, blob.Commitment) (*blob.Proof, error) `perm:"read"` @@ -38,8 +46,8 @@ type API struct { } } -func (api *API) Submit(ctx context.Context, blobs []*blob.Blob, gasPrice blob.GasPrice) (uint64, error) { - return api.Internal.Submit(ctx, blobs, gasPrice) +func (api *API) Submit(ctx context.Context, blobs []*blob.Blob, options *blob.SubmitOptions) (uint64, error) { + return api.Internal.Submit(ctx, blobs, options) } func (api *API) Get( diff --git a/nodebuilder/blob/cmd/blob.go b/nodebuilder/blob/cmd/blob.go index 8f9215a05a..5e57f91e0d 100644 --- a/nodebuilder/blob/cmd/blob.go +++ b/nodebuilder/blob/cmd/blob.go @@ -1,7 +1,7 @@ package cmd import ( - "encoding/base64" + "encoding/hex" "errors" "fmt" "path/filepath" @@ -13,45 +13,20 @@ import ( "github.com/celestiaorg/celestia-node/blob" cmdnode "github.com/celestiaorg/celestia-node/cmd" + state "github.com/celestiaorg/celestia-node/nodebuilder/state/cmd" "github.com/celestiaorg/celestia-node/share" ) -var ( - base64Flag bool - - gasPrice float64 - - // flagFileInput allows the user to provide file path to the json file - // for submitting multiple blobs. - flagFileInput = "input-file" -) +// flagFileInput allows the user to provide file path to the json file +// for submitting multiple blobs. +var flagFileInput = "input-file" func init() { Cmd.AddCommand(getCmd, getAllCmd, submitCmd, getProofCmd) - getCmd.PersistentFlags().BoolVar( - &base64Flag, - "base64", - false, - "printed blob's data and namespace as base64 strings", - ) - - getAllCmd.PersistentFlags().BoolVar( - &base64Flag, - "base64", - false, - "printed blob's data and namespace as base64 strings", - ) - - submitCmd.PersistentFlags().Float64Var( - &gasPrice, - "gas.price", - float64(blob.DefaultGasPrice()), - "specifies gas price (in utia) for blob submission.\n"+ - "Gas price will be set to default (0.002) if no value is passed", - ) - - submitCmd.PersistentFlags().String(flagFileInput, "", "Specify the file input") + state.ApplyFlags(submitCmd) + + submitCmd.PersistentFlags().String(flagFileInput, "", "Specifies the file input") } var Cmd = &cobra.Command{ @@ -62,9 +37,19 @@ var Cmd = &cobra.Command{ } var getCmd = &cobra.Command{ - Use: "get [height] [namespace] [commitment]", - Args: cobra.ExactArgs(3), - Short: "Returns the blob for the given namespace by commitment at a particular height.", + Use: "get [height] [namespace] [commitment]", + Args: cobra.ExactArgs(3), + Short: "Returns the blob for the given namespace by commitment at a particular height.\n" + + "Note:\n* Both namespace and commitment input parameters are expected to be in their hex representation.", + PreRunE: func(_ *cobra.Command, args []string) error { + if !strings.HasPrefix(args[1], "0x") { + return fmt.Errorf("only hex namespace is supported") + } + if !strings.HasPrefix(args[2], "0x") { + return fmt.Errorf("only hex commitment is supported") + } + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { client, err := cmdnode.ParseClientFromCtx(cmd.Context()) if err != nil { @@ -77,33 +62,32 @@ var getCmd = &cobra.Command{ return fmt.Errorf("error parsing a height: %w", err) } - if !strings.HasPrefix(args[1], "0x") { - return fmt.Errorf("only hex namespace is supported") - } namespace, err := cmdnode.ParseV0Namespace(args[1]) if err != nil { return fmt.Errorf("error parsing a namespace: %w", err) } - commitment, err := base64.StdEncoding.DecodeString(args[2]) + commitment, err := hex.DecodeString(args[2][2:]) if err != nil { return fmt.Errorf("error parsing a commitment: %w", err) } blob, err := client.Blob.Get(cmd.Context(), height, namespace, commitment) - - formatter := formatData(args[1]) - if base64Flag || err != nil { - formatter = nil - } - return cmdnode.PrintOutput(blob, err, formatter) + return cmdnode.PrintOutput(blob, err, formatData(args[1])) }, } var getAllCmd = &cobra.Command{ - Use: "get-all [height] [namespace]", - Args: cobra.ExactArgs(2), - Short: "Returns all blobs for the given namespace at a particular height.", + Use: "get-all [height] [namespace]", + Args: cobra.ExactArgs(2), + Short: "Returns all blobs for the given namespace at a particular height.\n" + + "Note:\n* Namespace input parameter is expected to be in its hex representation.", + PreRunE: func(_ *cobra.Command, args []string) error { + if !strings.HasPrefix(args[1], "0x") { + return fmt.Errorf("only hex namespace is supported") + } + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { client, err := cmdnode.ParseClientFromCtx(cmd.Context()) if err != nil { @@ -117,19 +101,12 @@ var getAllCmd = &cobra.Command{ } namespace, err := cmdnode.ParseV0Namespace(args[1]) - if !strings.HasPrefix(args[1], "0x") { - return fmt.Errorf("only hex namespace is supported") - } if err != nil { return fmt.Errorf("error parsing a namespace: %w", err) } blobs, err := client.Blob.GetAll(cmd.Context(), height, []share.Namespace{namespace}) - formatter := formatData(args[1]) - if base64Flag || err != nil { - formatter = nil - } - return cmdnode.PrintOutput(blobs, err, formatter) + return cmdnode.PrintOutput(blobs, err, formatData(args[1])) }, } @@ -156,7 +133,14 @@ var submitCmd = &cobra.Command{ return nil }, - Short: "Submit the blob(s) at the given namespace(s).\n" + + PreRunE: func(_ *cobra.Command, args []string) error { + if !strings.HasPrefix(args[0], "0x") { + return fmt.Errorf("only hex namespace is supported") + } + return nil + }, + Short: "Submit the blob(s) at the given namespace(s) and " + + "returns the header height in which the blob(s) was/were include + the respective commitment(s).\n" + "User can use namespace and blobData 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: @@ -174,7 +158,8 @@ var submitCmd = &cobra.Command{ ] }` + "Note:\n" + - "* fee and gas limit params will be calculated automatically.\n", + "* Namespace input parameter is expected to be its their hex representation.\n" + + "* Commitment(s) output parameter(s) will be in the hex representation.", RunE: func(cmd *cobra.Command, args []string) error { client, err := cmdnode.ParseClientFromCtx(cmd.Context()) if err != nil { @@ -200,26 +185,27 @@ var submitCmd = &cobra.Command{ jsonBlobs = append(jsonBlobs, blobJSON{Namespace: args[0], BlobData: args[1]}) } - var blobs []*blob.Blob - var commitments []blob.Commitment + var resultBlobs []*blob.Blob + var commitments []string for _, jsonBlob := range jsonBlobs { blob, err := getBlobFromArguments(jsonBlob.Namespace, jsonBlob.BlobData) if err != nil { return err } - blobs = append(blobs, blob) - commitments = append(commitments, blob.Commitment) + resultBlobs = append(resultBlobs, blob) + hexedCommitment := hex.EncodeToString(blob.Commitment) + commitments = append(commitments, "0x"+hexedCommitment) } height, err := client.Blob.Submit( cmd.Context(), - blobs, - blob.GasPrice(gasPrice), + resultBlobs, + state.GetTxConfig(), ) response := struct { - Height uint64 `json:"height"` - Commitments []blob.Commitment `json:"commitments"` + Height uint64 `json:"height"` + Commitments []string `json:"commitments"` }{ Height: height, Commitments: commitments, @@ -243,9 +229,19 @@ func getBlobFromArguments(namespaceArg, blobArg string) (*blob.Blob, error) { } var getProofCmd = &cobra.Command{ - Use: "get-proof [height] [namespace] [commitment]", - Args: cobra.ExactArgs(3), - Short: "Retrieves the blob in the given namespaces at the given height by commitment and returns its Proof.", + Use: "get-proof [height] [namespace] [commitment]", + Args: cobra.ExactArgs(3), + Short: "Retrieves the blob in the given namespaces at the given height by commitment and returns its Proof.\n" + + "Note:\n* Both namespace and commitment input parameters are expected to be in their hex representation.", + PreRunE: func(_ *cobra.Command, args []string) error { + if !strings.HasPrefix(args[1], "0x") { + return fmt.Errorf("only hex namespace is supported") + } + if !strings.HasPrefix(args[2], "0x") { + return fmt.Errorf("only hex commitment is supported") + } + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { client, err := cmdnode.ParseClientFromCtx(cmd.Context()) if err != nil { @@ -263,7 +259,7 @@ var getProofCmd = &cobra.Command{ return fmt.Errorf("error parsing a namespace: %w", err) } - commitment, err := base64.StdEncoding.DecodeString(args[2]) + commitment, err := hex.DecodeString(args[2][2:]) if err != nil { return fmt.Errorf("error parsing a commitment: %w", err) } @@ -279,7 +275,7 @@ func formatData(ns string) func(interface{}) interface{} { Namespace string `json:"namespace"` Data string `json:"data"` ShareVersion uint32 `json:"share_version"` - Commitment []byte `json:"commitment"` + Commitment string `json:"commitment"` Index int `json:"index"` } @@ -291,7 +287,7 @@ func formatData(ns string) func(interface{}) interface{} { Namespace: ns, Data: string(b.Data), ShareVersion: b.ShareVersion, - Commitment: b.Commitment, + Commitment: "0x" + hex.EncodeToString(b.Commitment), Index: b.Index(), } } @@ -303,7 +299,7 @@ func formatData(ns string) func(interface{}) interface{} { Namespace: ns, Data: string(b.Data), ShareVersion: b.ShareVersion, - Commitment: b.Commitment, + Commitment: "0x" + hex.EncodeToString(b.Commitment), Index: b.Index(), } } diff --git a/nodebuilder/blob/mocks/api.go b/nodebuilder/blob/mocks/api.go index 0898e70459..8b46c42d6c 100644 --- a/nodebuilder/blob/mocks/api.go +++ b/nodebuilder/blob/mocks/api.go @@ -10,6 +10,7 @@ import ( blob "github.com/celestiaorg/celestia-node/blob" share "github.com/celestiaorg/celestia-node/share" + state "github.com/celestiaorg/celestia-node/state" gomock "github.com/golang/mock/gomock" ) @@ -97,7 +98,7 @@ func (mr *MockModuleMockRecorder) Included(arg0, arg1, arg2, arg3, arg4 interfac } // Submit mocks base method. -func (m *MockModule) Submit(arg0 context.Context, arg1 []*blob.Blob, arg2 blob.GasPrice) (uint64, error) { +func (m *MockModule) Submit(arg0 context.Context, arg1 []*blob.Blob, arg2 *state.TxConfig) (uint64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Submit", arg0, arg1, arg2) ret0, _ := ret[0].(uint64) diff --git a/nodebuilder/config_test.go b/nodebuilder/config_test.go index e7b64b0aed..519466d88e 100644 --- a/nodebuilder/config_test.go +++ b/nodebuilder/config_test.go @@ -52,7 +52,7 @@ func TestUpdateConfig(t *testing.T) { // ensure this config field is now set after updating the config require.Equal(t, newCfg.Share.PeerManagerParams, cfg.Share.PeerManagerParams) // ensure old custom values were not changed - require.Equal(t, "thisshouldnthavechanged", cfg.State.KeyringAccName) + require.Equal(t, "thisshouldnthavechanged", cfg.State.DefaultKeyName) require.Equal(t, "7979", cfg.RPC.Port) require.True(t, cfg.Gateway.Enabled) } @@ -65,8 +65,8 @@ var outdatedConfig = ` GRPCPort = "0" [State] - KeyringAccName = "thisshouldnthavechanged" - KeyringBackend = "test" + DefaultKeyName = "thisshouldnthavechanged" + DefaultBackendName = "test" [P2P] ListenAddresses = ["/ip4/0.0.0.0/udp/2121/quic-v1", "/ip6/::/udp/2121/quic-v1", "/ip4/0.0.0.0/tcp/2121", diff --git a/nodebuilder/da/service.go b/nodebuilder/da/service.go index d43ba694a6..ebb47b9e3f 100644 --- a/nodebuilder/da/service.go +++ b/nodebuilder/da/service.go @@ -15,6 +15,7 @@ import ( "github.com/celestiaorg/celestia-node/blob" nodeblob "github.com/celestiaorg/celestia-node/nodebuilder/blob" "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/state" ) var _ da.DA = (*Service)(nil) @@ -110,7 +111,9 @@ func (s *Service) Submit( return nil, err } - height, err := s.blobServ.Submit(ctx, blobs, blob.GasPrice(gasPrice)) + opts := state.NewTxConfig(state.WithGasPrice(gasPrice)) + + height, err := s.blobServ.Submit(ctx, blobs, opts) if err != nil { log.Error("failed to submit blobs", "height", height, "gas price", gasPrice) return nil, err diff --git a/nodebuilder/init.go b/nodebuilder/init.go index 9ac6741b91..2de71a3998 100644 --- a/nodebuilder/init.go +++ b/nodebuilder/init.go @@ -192,11 +192,11 @@ func initDir(path string) error { func generateKeys(cfg Config, ksPath string) error { encConf := encoding.MakeConfig(app.ModuleEncodingRegisters...) - if cfg.State.KeyringBackend == keyring.BackendTest { + if cfg.State.DefaultBackendName == keyring.BackendTest { log.Warn("Detected plaintext keyring backend. For elevated security properties, consider using" + " the `file` keyring backend.") } - ring, err := keyring.New(app.Name, cfg.State.KeyringBackend, ksPath, os.Stdin, encConf.Codec) + ring, err := keyring.New(app.Name, cfg.State.DefaultBackendName, ksPath, os.Stdin, encConf.Codec) if err != nil { return err } @@ -228,6 +228,6 @@ func generateKeys(cfg Config, ksPath string) error { // generateNewKey generates and returns a new key on the given keyring called // "my_celes_key". func generateNewKey(ring keyring.Keyring) (*keyring.Record, string, error) { - return ring.NewMnemonic(state.DefaultAccountName, keyring.English, sdk.GetConfig().GetFullBIP44Path(), + return ring.NewMnemonic(state.DefaultKeyName, keyring.English, sdk.GetConfig().GetFullBIP44Path(), keyring.DefaultBIP39Passphrase, hd.Secp256k1) } diff --git a/nodebuilder/init_test.go b/nodebuilder/init_test.go index dcac1466af..bc345626c2 100644 --- a/nodebuilder/init_test.go +++ b/nodebuilder/init_test.go @@ -76,7 +76,7 @@ func TestInit_generateNewKey(t *testing.T) { cfg := DefaultConfig(node.Bridge) encConf := encoding.MakeConfig(app.ModuleEncodingRegisters...) - ring, err := keyring.New(app.Name, cfg.State.KeyringBackend, t.TempDir(), os.Stdin, encConf.Codec) + ring, err := keyring.New(app.Name, cfg.State.DefaultBackendName, t.TempDir(), os.Stdin, encConf.Codec) require.NoError(t, err) originalKey, mn, err := generateNewKey(ring) @@ -93,7 +93,7 @@ func TestInit_generateNewKey(t *testing.T) { assert.Contains(t, addr.String(), "celestia") // ensure account is recoverable from mnemonic - ring2, err := keyring.New(app.Name, cfg.State.KeyringBackend, t.TempDir(), os.Stdin, encConf.Codec) + ring2, err := keyring.New(app.Name, cfg.State.DefaultBackendName, t.TempDir(), os.Stdin, encConf.Codec) require.NoError(t, err) duplicateKey, err := ring2.NewAccount("test", mn, keyring.DefaultBIP39Passphrase, sdk.GetConfig().GetFullBIP44Path(), hd.Secp256k1) diff --git a/nodebuilder/state/cmd/state.go b/nodebuilder/state/cmd/state.go index 858212209e..3e254d554b 100644 --- a/nodebuilder/state/cmd/state.go +++ b/nodebuilder/state/cmd/state.go @@ -11,7 +11,14 @@ import ( "github.com/celestiaorg/celestia-node/state" ) -var amount uint64 +var ( + signer string + keyName string + gas uint64 + gasPrice float64 + feeGranterAddress string + amount uint64 +) func init() { Cmd.AddCommand( @@ -37,6 +44,16 @@ func init() { "specifies the spend limit(in utia) for the grantee.\n"+ "The default value is 0 which means the grantee does not have a spend limit.", ) + + // apply option flags for all txs that require `TxConfig`. + ApplyFlags( + transferCmd, + cancelUnbondingDelegationCmd, + beginRedelegateCmd, + undelegateCmd, + delegateCmd, + grantFeeCmd, + revokeGrantFeeCmd) } var Cmd = &cobra.Command{ @@ -102,9 +119,9 @@ var balanceForAddressCmd = &cobra.Command{ } var transferCmd = &cobra.Command{ - Use: "transfer [address] [amount] [fee] [gasLimit]", + Use: "transfer [address] [amount]", Short: "Sends the given amount of coins from default wallet of the node to the given account address.", - Args: cobra.ExactArgs(4), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { client, err := cmdnode.ParseClientFromCtx(cmd.Context()) if err != nil { @@ -121,29 +138,21 @@ var transferCmd = &cobra.Command{ if err != nil { return fmt.Errorf("error parsing an amount: %w", err) } - fee, err := strconv.ParseInt(args[2], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a fee: %w", err) - } - gasLimit, err := strconv.ParseUint(args[3], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a gas limit: %w", err) - } txResponse, err := client.State.Transfer( cmd.Context(), addr.Address.(state.AccAddress), math.NewInt(amount), - math.NewInt(fee), gasLimit, + GetTxConfig(), ) return cmdnode.PrintOutput(txResponse, err, nil) }, } var cancelUnbondingDelegationCmd = &cobra.Command{ - Use: "cancel-unbonding-delegation [address] [amount] [height] [fee] [gasLimit]", + Use: "cancel-unbonding-delegation [address] [amount] [height]", Short: "Cancels a user's pending undelegation from a validator.", - Args: cobra.ExactArgs(5), + Args: cobra.ExactArgs(3), RunE: func(cmd *cobra.Command, args []string) error { client, err := cmdnode.ParseClientFromCtx(cmd.Context()) if err != nil { @@ -163,17 +172,7 @@ var cancelUnbondingDelegationCmd = &cobra.Command{ height, err := strconv.ParseInt(args[2], 10, 64) if err != nil { - return fmt.Errorf("error parsing a fee: %w", err) - } - - fee, err := strconv.ParseInt(args[3], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a fee: %w", err) - } - - gasLimit, err := strconv.ParseUint(args[4], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a gas limit: %w", err) + return fmt.Errorf("error parsing height: %w", err) } txResponse, err := client.State.CancelUnbondingDelegation( @@ -181,17 +180,16 @@ var cancelUnbondingDelegationCmd = &cobra.Command{ addr.Address.(state.ValAddress), math.NewInt(amount), math.NewInt(height), - math.NewInt(fee), - gasLimit, + GetTxConfig(), ) return cmdnode.PrintOutput(txResponse, err, nil) }, } var beginRedelegateCmd = &cobra.Command{ - Use: "begin-redelegate [srcAddress] [dstAddress] [amount] [fee] [gasLimit]", + Use: "begin-redelegate [srcAddress] [dstAddress] [amount]", Short: "Sends a user's delegated tokens to a new validator for redelegation", - Args: cobra.ExactArgs(5), + Args: cobra.ExactArgs(3), RunE: func(cmd *cobra.Command, args []string) error { client, err := cmdnode.ParseClientFromCtx(cmd.Context()) if err != nil { @@ -214,29 +212,19 @@ var beginRedelegateCmd = &cobra.Command{ return fmt.Errorf("error parsing an amount: %w", err) } - fee, err := strconv.ParseInt(args[3], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a fee: %w", err) - } - gasLimit, err := strconv.ParseUint(args[4], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a gas limit: %w", err) - } - txResponse, err := client.State.BeginRedelegate( cmd.Context(), srcAddr.Address.(state.ValAddress), dstAddr.Address.(state.ValAddress), math.NewInt(amount), - math.NewInt(fee), - gasLimit, + GetTxConfig(), ) return cmdnode.PrintOutput(txResponse, err, nil) }, } var undelegateCmd = &cobra.Command{ - Use: "undelegate [valAddress] [amount] [fee] [gasLimit]", + Use: "undelegate [valAddress] [amount]", Short: "Undelegates a user's delegated tokens, unbonding them from the current validator.", Args: cobra.ExactArgs(4), RunE: func(cmd *cobra.Command, args []string) error { @@ -255,28 +243,19 @@ var undelegateCmd = &cobra.Command{ if err != nil { return fmt.Errorf("error parsing an amount: %w", err) } - fee, err := strconv.ParseInt(args[2], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a fee: %w", err) - } - gasLimit, err := strconv.ParseUint(args[3], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a gas limit: %w", err) - } txResponse, err := client.State.Undelegate( cmd.Context(), addr.Address.(state.ValAddress), math.NewInt(amount), - math.NewInt(fee), - gasLimit, + GetTxConfig(), ) return cmdnode.PrintOutput(txResponse, err, nil) }, } var delegateCmd = &cobra.Command{ - Use: "delegate [valAddress] [amount] [fee] [gasLimit]", + Use: "delegate [valAddress] [amount]", Short: "Sends a user's liquid tokens to a validator for delegation.", Args: cobra.ExactArgs(4), RunE: func(cmd *cobra.Command, args []string) error { @@ -296,22 +275,11 @@ var delegateCmd = &cobra.Command{ return fmt.Errorf("error parsing an amount: %w", err) } - fee, err := strconv.ParseInt(args[2], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a fee: %w", err) - } - - gasLimit, err := strconv.ParseUint(args[3], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a gas limit: %w", err) - } - txResponse, err := client.State.Delegate( cmd.Context(), addr.Address.(state.ValAddress), math.NewInt(amount), - math.NewInt(fee), - gasLimit, + GetTxConfig(), ) return cmdnode.PrintOutput(txResponse, err, nil) }, @@ -390,10 +358,10 @@ var queryRedelegationCmd = &cobra.Command{ } var grantFeeCmd = &cobra.Command{ - Use: "grant-fee [granteeAddress] [fee] [gasLimit]", + Use: "grant-fee [granteeAddress]", Short: "Grant an allowance to a specified grantee account to pay the fees for their transactions.\n" + "Grantee can spend any amount of tokens in case the spend limit is not set.", - Args: cobra.ExactArgs(3), + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := cmdnode.ParseClientFromCtx(cmd.Context()) if err != nil { @@ -406,28 +374,19 @@ var grantFeeCmd = &cobra.Command{ return fmt.Errorf("error parsing an address: %w", err) } - fee, err := strconv.ParseInt(args[1], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a fee: %w", err) - } - gasLimit, err := strconv.ParseUint(args[2], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a gas limit: %w", err) - } - txResponse, err := client.State.GrantFee( cmd.Context(), granteeAddr.Address.(state.AccAddress), - math.NewInt(int64(amount)), math.NewInt(fee), gasLimit, + math.NewInt(int64(amount)), GetTxConfig(), ) return cmdnode.PrintOutput(txResponse, err, nil) }, } var revokeGrantFeeCmd = &cobra.Command{ - Use: "revoke-grant-fee [granteeAddress] [fee] [gasLimit]", + Use: "revoke-grant-fee [granteeAddress]", Short: "Removes permission for grantee to submit PFB transactions which will be paid by granter.", - Args: cobra.ExactArgs(3), + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := cmdnode.ParseClientFromCtx(cmd.Context()) if err != nil { @@ -440,19 +399,10 @@ var revokeGrantFeeCmd = &cobra.Command{ return fmt.Errorf("error parsing an address: %w", err) } - fee, err := strconv.ParseInt(args[1], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a fee: %w", err) - } - gasLimit, err := strconv.ParseUint(args[2], 10, 64) - if err != nil { - return fmt.Errorf("error parsing a gas limit: %w", err) - } - txResponse, err := client.State.RevokeGrantFee( cmd.Context(), granteeAddr.Address.(state.AccAddress), - math.NewInt(fee), gasLimit, + GetTxConfig(), ) return cmdnode.PrintOutput(txResponse, err, nil) }, @@ -466,3 +416,59 @@ func parseAddressFromString(addrStr string) (state.Address, error) { } return address, nil } + +func ApplyFlags(cmds ...*cobra.Command) { + for _, cmd := range cmds { + cmd.PersistentFlags().StringVar( + &signer, + "signer", + "", + "Specifies the signer address from the keystore.\n"+ + "If both the address and the key are specified, the address field will take priority.\n"+ + "Note: The account address should be provided as a Bech32 address.", + ) + + cmd.PersistentFlags().StringVar( + &keyName, + "key.name", + "", + "Specifies the signer name from the keystore.", + ) + + cmd.PersistentFlags().Float64Var( + &gasPrice, + "gas.price", + state.DefaultGasPrice, + "Specifies gas price for the fee calculation", + ) + + cmd.PersistentFlags().Uint64Var( + &gas, + "gas", + 0, + "Specifies gas limit (in utia) for tx submission. "+ + "(default 0)", + ) + + cmd.PersistentFlags().StringVar( + &feeGranterAddress, + "granter.address", + "", + "Specifies the address that can pay fees on behalf of the signer.\n"+ + "The granter must submit the transaction to pay for the grantee's (signer's) transactions.\n"+ + "By default, this will be set to an empty string, meaning the signer will pay the fees.\n"+ + "Note: The granter should be provided as a Bech32 address.\n"+ + "Example: celestiaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + ) + } +} + +func GetTxConfig() *state.TxConfig { + return state.NewTxConfig( + state.WithGasPrice(gasPrice), + state.WithGas(gas), + state.WithKeyName(keyName), + state.WithSignerAddress(signer), + state.WithFeeGranterAddress(feeGranterAddress), + ) +} diff --git a/nodebuilder/state/config.go b/nodebuilder/state/config.go index e3589c7a68..2488fa256d 100644 --- a/nodebuilder/state/config.go +++ b/nodebuilder/state/config.go @@ -2,25 +2,21 @@ package state import ( "github.com/cosmos/cosmos-sdk/crypto/keyring" - - "github.com/celestiaorg/celestia-node/state" ) -var defaultKeyringBackend = keyring.BackendTest +var defaultBackendName = keyring.BackendTest // Config contains configuration parameters for constructing // the node's keyring signer. type Config struct { - KeyringAccName string - KeyringBackend string - GranterAddress state.AccAddress + DefaultKeyName string + DefaultBackendName string } func DefaultConfig() Config { return Config{ - KeyringAccName: "", - KeyringBackend: defaultKeyringBackend, - GranterAddress: state.AccAddress{}, + DefaultKeyName: DefaultKeyName, + DefaultBackendName: defaultBackendName, } } diff --git a/nodebuilder/state/flags.go b/nodebuilder/state/flags.go index 75d5f0fb28..4424852448 100644 --- a/nodebuilder/state/flags.go +++ b/nodebuilder/state/flags.go @@ -3,46 +3,30 @@ package state import ( "fmt" - sdktypes "github.com/cosmos/cosmos-sdk/types" "github.com/spf13/cobra" flag "github.com/spf13/pflag" ) var ( - keyringAccNameFlag = "keyring.accname" + keyringKeyNameFlag = "keyring.keyname" keyringBackendFlag = "keyring.backend" - - granterAddressFlag = "granter.address" ) // Flags gives a set of hardcoded State flags. func Flags() *flag.FlagSet { flags := &flag.FlagSet{} - flags.String(keyringAccNameFlag, "", "Directs node's keyring signer to use the key prefixed with the "+ - "given string.") - flags.String(keyringBackendFlag, defaultKeyringBackend, fmt.Sprintf("Directs node's keyring signer to use the given "+ - "backend. Default is %s.", defaultKeyringBackend)) - - flags.String(granterAddressFlag, "", "Account address that will pay for all transactions submitted from the node.") + flags.String(keyringKeyNameFlag, DefaultKeyName, + fmt.Sprintf("Directs node's keyring signer to use the key prefixed with the "+ + "given string. Default is %s", DefaultKeyName)) + flags.String(keyringBackendFlag, defaultBackendName, + fmt.Sprintf("Directs node's keyring signer to use the given "+ + "backend. Default is %s.", defaultBackendName)) return flags } // ParseFlags parses State flags from the given cmd and saves them to the passed config. -func ParseFlags(cmd *cobra.Command, cfg *Config) error { - keyringAccName := cmd.Flag(keyringAccNameFlag).Value.String() - if keyringAccName != "" { - cfg.KeyringAccName = keyringAccName - } - - cfg.KeyringBackend = cmd.Flag(keyringBackendFlag).Value.String() - - addr := cmd.Flag(granterAddressFlag).Value.String() - if addr == "" { - return nil - } - - sdkAddress, err := sdktypes.AccAddressFromBech32(addr) - cfg.GranterAddress = sdkAddress - return err +func ParseFlags(cmd *cobra.Command, cfg *Config) { + cfg.DefaultKeyName = cmd.Flag(keyringKeyNameFlag).Value.String() + cfg.DefaultBackendName = cmd.Flag(keyringBackendFlag).Value.String() } diff --git a/nodebuilder/state/keyring.go b/nodebuilder/state/keyring.go index 1b65d27efe..0f9ab59cfa 100644 --- a/nodebuilder/state/keyring.go +++ b/nodebuilder/state/keyring.go @@ -1,12 +1,14 @@ package state import ( + "fmt" + kr "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/celestiaorg/celestia-node/libs/keystore" ) -const DefaultAccountName = "my_celes_key" +const DefaultKeyName = "my_celes_key" type AccountName string @@ -15,24 +17,11 @@ type AccountName string // as having keyring-backend set to `file` prompts user for password. func Keyring(cfg Config, ks keystore.Keystore) (kr.Keyring, AccountName, error) { ring := ks.Keyring() - var info *kr.Record - // if custom keyringAccName provided, find key for that name - if cfg.KeyringAccName != "" { - keyInfo, err := ring.Key(cfg.KeyringAccName) - if err != nil { - log.Errorw("failed to find key by given name", "keyring.accname", cfg.KeyringAccName) - return nil, "", err - } - info = keyInfo - } else { - // use default key - keyInfo, err := ring.Key(DefaultAccountName) - if err != nil { - log.Errorw("could not access key in keyring", "name", DefaultAccountName) - return nil, "", err - } - info = keyInfo + keyInfo, err := ring.Key(cfg.DefaultKeyName) + if err != nil { + err = fmt.Errorf("can't get key: `%s` from the keystore: %w", cfg.DefaultKeyName, err) + log.Error(err) + return nil, "", err } - - return ring, AccountName(info.Name), nil + return ring, AccountName(keyInfo.Name), nil } diff --git a/nodebuilder/state/mocks/api.go b/nodebuilder/state/mocks/api.go index 5e132f6e21..a4fb8d8cec 100644 --- a/nodebuilder/state/mocks/api.go +++ b/nodebuilder/state/mocks/api.go @@ -9,12 +9,11 @@ import ( reflect "reflect" math "cosmossdk.io/math" - blob "github.com/celestiaorg/celestia-node/blob" state "github.com/celestiaorg/celestia-node/state" types "github.com/cosmos/cosmos-sdk/types" types0 "github.com/cosmos/cosmos-sdk/x/staking/types" gomock "github.com/golang/mock/gomock" - types1 "github.com/tendermint/tendermint/types" + types1 "github.com/tendermint/tendermint/proto/tendermint/types" ) // MockModule is a mock of Module interface. @@ -86,63 +85,63 @@ func (mr *MockModuleMockRecorder) BalanceForAddress(arg0, arg1 interface{}) *gom } // BeginRedelegate mocks base method. -func (m *MockModule) BeginRedelegate(arg0 context.Context, arg1, arg2 types.ValAddress, arg3, arg4 math.Int, arg5 uint64) (*types.TxResponse, error) { +func (m *MockModule) BeginRedelegate(arg0 context.Context, arg1, arg2 types.ValAddress, arg3 math.Int, arg4 *state.TxConfig) (*types.TxResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BeginRedelegate", arg0, arg1, arg2, arg3, arg4, arg5) + ret := m.ctrl.Call(m, "BeginRedelegate", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(*types.TxResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // BeginRedelegate indicates an expected call of BeginRedelegate. -func (mr *MockModuleMockRecorder) BeginRedelegate(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { +func (mr *MockModuleMockRecorder) BeginRedelegate(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginRedelegate", reflect.TypeOf((*MockModule)(nil).BeginRedelegate), arg0, arg1, arg2, arg3, arg4, arg5) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginRedelegate", reflect.TypeOf((*MockModule)(nil).BeginRedelegate), arg0, arg1, arg2, arg3, arg4) } // CancelUnbondingDelegation mocks base method. -func (m *MockModule) CancelUnbondingDelegation(arg0 context.Context, arg1 types.ValAddress, arg2, arg3, arg4 math.Int, arg5 uint64) (*types.TxResponse, error) { +func (m *MockModule) CancelUnbondingDelegation(arg0 context.Context, arg1 types.ValAddress, arg2, arg3 math.Int, arg4 *state.TxConfig) (*types.TxResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CancelUnbondingDelegation", arg0, arg1, arg2, arg3, arg4, arg5) + ret := m.ctrl.Call(m, "CancelUnbondingDelegation", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(*types.TxResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // CancelUnbondingDelegation indicates an expected call of CancelUnbondingDelegation. -func (mr *MockModuleMockRecorder) CancelUnbondingDelegation(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { +func (mr *MockModuleMockRecorder) CancelUnbondingDelegation(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelUnbondingDelegation", reflect.TypeOf((*MockModule)(nil).CancelUnbondingDelegation), arg0, arg1, arg2, arg3, arg4, arg5) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelUnbondingDelegation", reflect.TypeOf((*MockModule)(nil).CancelUnbondingDelegation), arg0, arg1, arg2, arg3, arg4) } // Delegate mocks base method. -func (m *MockModule) Delegate(arg0 context.Context, arg1 types.ValAddress, arg2, arg3 math.Int, arg4 uint64) (*types.TxResponse, error) { +func (m *MockModule) Delegate(arg0 context.Context, arg1 types.ValAddress, arg2 math.Int, arg3 *state.TxConfig) (*types.TxResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delegate", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "Delegate", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(*types.TxResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // Delegate indicates an expected call of Delegate. -func (mr *MockModuleMockRecorder) Delegate(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { +func (mr *MockModuleMockRecorder) Delegate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delegate", reflect.TypeOf((*MockModule)(nil).Delegate), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delegate", reflect.TypeOf((*MockModule)(nil).Delegate), arg0, arg1, arg2, arg3) } // GrantFee mocks base method. -func (m *MockModule) GrantFee(arg0 context.Context, arg1 types.AccAddress, arg2, arg3 math.Int, arg4 uint64) (*types.TxResponse, error) { +func (m *MockModule) GrantFee(arg0 context.Context, arg1 types.AccAddress, arg2 math.Int, arg3 *state.TxConfig) (*types.TxResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GrantFee", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "GrantFee", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(*types.TxResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // GrantFee indicates an expected call of GrantFee. -func (mr *MockModuleMockRecorder) GrantFee(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { +func (mr *MockModuleMockRecorder) GrantFee(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantFee", reflect.TypeOf((*MockModule)(nil).GrantFee), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantFee", reflect.TypeOf((*MockModule)(nil).GrantFee), arg0, arg1, arg2, arg3) } // QueryDelegation mocks base method. @@ -191,76 +190,61 @@ func (mr *MockModuleMockRecorder) QueryUnbonding(arg0, arg1 interface{}) *gomock } // RevokeGrantFee mocks base method. -func (m *MockModule) RevokeGrantFee(arg0 context.Context, arg1 types.AccAddress, arg2 math.Int, arg3 uint64) (*types.TxResponse, error) { +func (m *MockModule) RevokeGrantFee(arg0 context.Context, arg1 types.AccAddress, arg2 *state.TxConfig) (*types.TxResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RevokeGrantFee", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "RevokeGrantFee", arg0, arg1, arg2) ret0, _ := ret[0].(*types.TxResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // RevokeGrantFee indicates an expected call of RevokeGrantFee. -func (mr *MockModuleMockRecorder) RevokeGrantFee(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +func (mr *MockModuleMockRecorder) RevokeGrantFee(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeGrantFee", reflect.TypeOf((*MockModule)(nil).RevokeGrantFee), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeGrantFee", reflect.TypeOf((*MockModule)(nil).RevokeGrantFee), arg0, arg1, arg2) } // SubmitPayForBlob mocks base method. -func (m *MockModule) SubmitPayForBlob(arg0 context.Context, arg1 math.Int, arg2 uint64, arg3 []*blob.Blob) (*types.TxResponse, error) { +func (m *MockModule) SubmitPayForBlob(arg0 context.Context, arg1 []*types1.Blob, arg2 *state.TxConfig) (*types.TxResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SubmitPayForBlob", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "SubmitPayForBlob", arg0, arg1, arg2) ret0, _ := ret[0].(*types.TxResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // SubmitPayForBlob indicates an expected call of SubmitPayForBlob. -func (mr *MockModuleMockRecorder) SubmitPayForBlob(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +func (mr *MockModuleMockRecorder) SubmitPayForBlob(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitPayForBlob", reflect.TypeOf((*MockModule)(nil).SubmitPayForBlob), arg0, arg1, arg2, arg3) -} - -// SubmitTx mocks base method. -func (m *MockModule) SubmitTx(arg0 context.Context, arg1 types1.Tx) (*types.TxResponse, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SubmitTx", arg0, arg1) - ret0, _ := ret[0].(*types.TxResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SubmitTx indicates an expected call of SubmitTx. -func (mr *MockModuleMockRecorder) SubmitTx(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitTx", reflect.TypeOf((*MockModule)(nil).SubmitTx), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitPayForBlob", reflect.TypeOf((*MockModule)(nil).SubmitPayForBlob), arg0, arg1, arg2) } // Transfer mocks base method. -func (m *MockModule) Transfer(arg0 context.Context, arg1 types.AccAddress, arg2, arg3 math.Int, arg4 uint64) (*types.TxResponse, error) { +func (m *MockModule) Transfer(arg0 context.Context, arg1 types.AccAddress, arg2 math.Int, arg3 *state.TxConfig) (*types.TxResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Transfer", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "Transfer", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(*types.TxResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // Transfer indicates an expected call of Transfer. -func (mr *MockModuleMockRecorder) Transfer(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { +func (mr *MockModuleMockRecorder) Transfer(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transfer", reflect.TypeOf((*MockModule)(nil).Transfer), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transfer", reflect.TypeOf((*MockModule)(nil).Transfer), arg0, arg1, arg2, arg3) } // Undelegate mocks base method. -func (m *MockModule) Undelegate(arg0 context.Context, arg1 types.ValAddress, arg2, arg3 math.Int, arg4 uint64) (*types.TxResponse, error) { +func (m *MockModule) Undelegate(arg0 context.Context, arg1 types.ValAddress, arg2 math.Int, arg3 *state.TxConfig) (*types.TxResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Undelegate", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "Undelegate", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(*types.TxResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // Undelegate indicates an expected call of Undelegate. -func (mr *MockModuleMockRecorder) Undelegate(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { +func (mr *MockModuleMockRecorder) Undelegate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Undelegate", reflect.TypeOf((*MockModule)(nil).Undelegate), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Undelegate", reflect.TypeOf((*MockModule)(nil).Undelegate), arg0, arg1, arg2, arg3) } diff --git a/nodebuilder/state/module.go b/nodebuilder/state/module.go index 0856884b98..bd6f2081d3 100644 --- a/nodebuilder/state/module.go +++ b/nodebuilder/state/module.go @@ -24,9 +24,6 @@ func ConstructModule(tp node.Type, cfg *Config, coreCfg *core.Config) fx.Option // sanitize config values before constructing module cfgErr := cfg.Validate() opts := make([]state.Option, 0) - if !cfg.GranterAddress.Empty() { - opts = append(opts, state.WithGranter(cfg.GranterAddress)) - } baseComponents := fx.Options( fx.Supply(*cfg), fx.Error(cfgErr), diff --git a/nodebuilder/state/state.go b/nodebuilder/state/state.go index c13aef4086..5b63b69450 100644 --- a/nodebuilder/state/state.go +++ b/nodebuilder/state/state.go @@ -5,7 +5,6 @@ import ( "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/celestiaorg/celestia-node/blob" "github.com/celestiaorg/celestia-node/state" ) @@ -30,55 +29,46 @@ type Module interface { // the node's current head (head-1). This is due to the fact that for block N, the block's // `AppHash` is the result of applying the previous block's transaction list. BalanceForAddress(ctx context.Context, addr state.Address) (*state.Balance, error) - // Transfer sends the given amount of coins from default wallet of the node to the given account // address. Transfer( - ctx context.Context, to state.AccAddress, amount, fee state.Int, gasLimit uint64, + ctx context.Context, to state.AccAddress, amount state.Int, config *state.TxConfig, ) (*state.TxResponse, error) - // SubmitTx submits the given transaction/message to the - // Celestia network and blocks until the tx is included in - // a block. - SubmitTx(ctx context.Context, tx state.Tx) (*state.TxResponse, error) // SubmitPayForBlob builds, signs and submits a PayForBlob transaction. SubmitPayForBlob( ctx context.Context, - fee state.Int, - gasLim uint64, - blobs []*blob.Blob, + blobs []*state.Blob, + config *state.TxConfig, ) (*state.TxResponse, error) - // CancelUnbondingDelegation cancels a user's pending undelegation from a validator. CancelUnbondingDelegation( ctx context.Context, valAddr state.ValAddress, amount, - height, - fee state.Int, - gasLim uint64, + height state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) // BeginRedelegate sends a user's delegated tokens to a new validator for redelegation. BeginRedelegate( ctx context.Context, srcValAddr, dstValAddr state.ValAddress, - amount, - fee state.Int, - gasLim uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) // Undelegate undelegates a user's delegated tokens, unbonding them from the current validator. Undelegate( ctx context.Context, delAddr state.ValAddress, - amount, fee state.Int, - gasLim uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) // Delegate sends a user's liquid tokens to a validator for delegation. Delegate( ctx context.Context, delAddr state.ValAddress, - amount, fee state.Int, - gasLim uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) // QueryDelegation retrieves the delegation information between a delegator and a validator. @@ -95,16 +85,14 @@ type Module interface { GrantFee( ctx context.Context, grantee state.AccAddress, - amount, - fee state.Int, - gasLim uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) RevokeGrantFee( ctx context.Context, grantee state.AccAddress, - fee state.Int, - gasLim uint64, + config *state.TxConfig, ) (*state.TxResponse, error) } @@ -120,46 +108,38 @@ type API struct { Transfer func( ctx context.Context, to state.AccAddress, - amount, - fee state.Int, - gasLimit uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) `perm:"write"` - SubmitTx func(ctx context.Context, tx state.Tx) (*state.TxResponse, error) `perm:"read"` SubmitPayForBlob func( ctx context.Context, - fee state.Int, - gasLim uint64, - blobs []*blob.Blob, + blobs []*state.Blob, + config *state.TxConfig, ) (*state.TxResponse, error) `perm:"write"` CancelUnbondingDelegation func( ctx context.Context, valAddr state.ValAddress, - amount, - height, - fee state.Int, - gasLim uint64, + amount, height state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) `perm:"write"` BeginRedelegate func( ctx context.Context, srcValAddr, dstValAddr state.ValAddress, - amount, - fee state.Int, - gasLim uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) `perm:"write"` Undelegate func( ctx context.Context, delAddr state.ValAddress, - amount, - fee state.Int, - gasLim uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) `perm:"write"` Delegate func( ctx context.Context, delAddr state.ValAddress, - amount, - fee state.Int, - gasLim uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) `perm:"write"` QueryDelegation func( ctx context.Context, @@ -177,16 +157,13 @@ type API struct { GrantFee func( ctx context.Context, grantee state.AccAddress, - amount, - fee state.Int, - gasLim uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) `perm:"write"` - RevokeGrantFee func( ctx context.Context, grantee state.AccAddress, - fee state.Int, - gasLim uint64, + config *state.TxConfig, ) (*state.TxResponse, error) `perm:"write"` } } @@ -202,65 +179,54 @@ func (api *API) BalanceForAddress(ctx context.Context, addr state.Address) (*sta func (api *API) Transfer( ctx context.Context, to state.AccAddress, - amount, - fee state.Int, - gasLimit uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) { - return api.Internal.Transfer(ctx, to, amount, fee, gasLimit) -} - -func (api *API) SubmitTx(ctx context.Context, tx state.Tx) (*state.TxResponse, error) { - return api.Internal.SubmitTx(ctx, tx) + return api.Internal.Transfer(ctx, to, amount, config) } func (api *API) SubmitPayForBlob( ctx context.Context, - fee state.Int, - gasLim uint64, - blobs []*blob.Blob, + blobs []*state.Blob, + config *state.TxConfig, ) (*state.TxResponse, error) { - return api.Internal.SubmitPayForBlob(ctx, fee, gasLim, blobs) + return api.Internal.SubmitPayForBlob(ctx, blobs, config) } func (api *API) CancelUnbondingDelegation( ctx context.Context, valAddr state.ValAddress, - amount, - height, - fee state.Int, - gasLim uint64, + amount, height state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) { - return api.Internal.CancelUnbondingDelegation(ctx, valAddr, amount, height, fee, gasLim) + return api.Internal.CancelUnbondingDelegation(ctx, valAddr, amount, height, config) } func (api *API) BeginRedelegate( ctx context.Context, srcValAddr, dstValAddr state.ValAddress, - amount, - fee state.Int, - gasLim uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) { - return api.Internal.BeginRedelegate(ctx, srcValAddr, dstValAddr, amount, fee, gasLim) + return api.Internal.BeginRedelegate(ctx, srcValAddr, dstValAddr, amount, config) } func (api *API) Undelegate( ctx context.Context, delAddr state.ValAddress, - amount, - fee state.Int, - gasLim uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) { - return api.Internal.Undelegate(ctx, delAddr, amount, fee, gasLim) + return api.Internal.Undelegate(ctx, delAddr, amount, config) } func (api *API) Delegate( ctx context.Context, delAddr state.ValAddress, - amount, - fee state.Int, - gasLim uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) { - return api.Internal.Delegate(ctx, delAddr, amount, fee, gasLim) + return api.Internal.Delegate(ctx, delAddr, amount, config) } func (api *API) QueryDelegation(ctx context.Context, valAddr state.ValAddress) (*types.QueryDelegationResponse, error) { @@ -288,18 +254,16 @@ func (api *API) Balance(ctx context.Context) (*state.Balance, error) { func (api *API) GrantFee( ctx context.Context, grantee state.AccAddress, - amount, - fee state.Int, - gasLim uint64, + amount state.Int, + config *state.TxConfig, ) (*state.TxResponse, error) { - return api.Internal.GrantFee(ctx, grantee, amount, fee, gasLim) + return api.Internal.GrantFee(ctx, grantee, amount, config) } func (api *API) RevokeGrantFee( ctx context.Context, grantee state.AccAddress, - fee state.Int, - gasLim uint64, + config *state.TxConfig, ) (*state.TxResponse, error) { - return api.Internal.RevokeGrantFee(ctx, grantee, fee, gasLim) + return api.Internal.RevokeGrantFee(ctx, grantee, config) } diff --git a/nodebuilder/state/stub.go b/nodebuilder/state/stub.go index 58f39dd426..c08919fd28 100644 --- a/nodebuilder/state/stub.go +++ b/nodebuilder/state/stub.go @@ -6,7 +6,6 @@ import ( "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/celestiaorg/celestia-node/blob" "github.com/celestiaorg/celestia-node/state" ) @@ -35,21 +34,16 @@ func (s stubbedStateModule) BalanceForAddress( func (s stubbedStateModule) Transfer( _ context.Context, _ state.AccAddress, - _, _ state.Int, - _ uint64, + _ state.Int, + _ *state.TxConfig, ) (*state.TxResponse, error) { return nil, ErrNoStateAccess } -func (s stubbedStateModule) SubmitTx(context.Context, state.Tx) (*state.TxResponse, error) { - return nil, ErrNoStateAccess -} - func (s stubbedStateModule) SubmitPayForBlob( context.Context, - state.Int, - uint64, - []*blob.Blob, + []*state.Blob, + *state.TxConfig, ) (*state.TxResponse, error) { return nil, ErrNoStateAccess } @@ -57,8 +51,8 @@ func (s stubbedStateModule) SubmitPayForBlob( func (s stubbedStateModule) CancelUnbondingDelegation( _ context.Context, _ state.ValAddress, - _, _, _ state.Int, - _ uint64, + _, _ state.Int, + _ *state.TxConfig, ) (*state.TxResponse, error) { return nil, ErrNoStateAccess } @@ -66,8 +60,8 @@ func (s stubbedStateModule) CancelUnbondingDelegation( func (s stubbedStateModule) BeginRedelegate( _ context.Context, _, _ state.ValAddress, - _, _ state.Int, - _ uint64, + _ state.Int, + _ *state.TxConfig, ) (*state.TxResponse, error) { return nil, ErrNoStateAccess } @@ -75,8 +69,8 @@ func (s stubbedStateModule) BeginRedelegate( func (s stubbedStateModule) Undelegate( _ context.Context, _ state.ValAddress, - _, _ state.Int, - _ uint64, + _ state.Int, + _ *state.TxConfig, ) (*state.TxResponse, error) { return nil, ErrNoStateAccess } @@ -84,8 +78,8 @@ func (s stubbedStateModule) Undelegate( func (s stubbedStateModule) Delegate( _ context.Context, _ state.ValAddress, - _, _ state.Int, - _ uint64, + _ state.Int, + _ *state.TxConfig, ) (*state.TxResponse, error) { return nil, ErrNoStateAccess } @@ -114,9 +108,8 @@ func (s stubbedStateModule) QueryRedelegations( func (s stubbedStateModule) GrantFee( _ context.Context, _ state.AccAddress, - _, _ state.Int, - _ uint64, + _ *state.TxConfig, ) (*state.TxResponse, error) { return nil, ErrNoStateAccess } @@ -124,8 +117,7 @@ func (s stubbedStateModule) GrantFee( func (s stubbedStateModule) RevokeGrantFee( _ context.Context, _ state.AccAddress, - _ state.Int, - _ uint64, + _ *state.TxConfig, ) (*state.TxResponse, error) { return nil, ErrNoStateAccess } diff --git a/nodebuilder/testing.go b/nodebuilder/testing.go index a4cfc94e32..0f2e046882 100644 --- a/nodebuilder/testing.go +++ b/nodebuilder/testing.go @@ -16,9 +16,13 @@ import ( "github.com/celestiaorg/celestia-node/libs/fxutil" "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/nodebuilder/p2p" + "github.com/celestiaorg/celestia-node/nodebuilder/state" ) -const TestKeyringName = "test_celes" +const ( + TestKeyringName = "test_celes" + TestKeyringName1 = "test_celes1" +) // MockStore provides mock in memory Store for testing purposes. func MockStore(t *testing.T, cfg *Config) Store { @@ -49,10 +53,14 @@ func TestNodeWithConfig(t *testing.T, tp node.Type, cfg *Config, opts ...fx.Opti ks, err := store.Keystore() require.NoError(t, err) kr := ks.Keyring() + // create a key in the keystore to be used by the core accessor _, _, err = kr.NewMnemonic(TestKeyringName, keyring.English, "", "", hd.Secp256k1) require.NoError(t, err) - cfg.State.KeyringAccName = TestKeyringName + cfg.State.DefaultKeyName = TestKeyringName + _, accName, err := state.Keyring(cfg.State, ks) + require.NoError(t, err) + require.Equal(t, TestKeyringName, string(accName)) opts = append(opts, // temp dir for the eds store FIXME: Should be in mem diff --git a/nodebuilder/tests/api_test.go b/nodebuilder/tests/api_test.go index 56c26382ff..f9e10c5ea6 100644 --- a/nodebuilder/tests/api_test.go +++ b/nodebuilder/tests/api_test.go @@ -18,6 +18,7 @@ import ( "github.com/celestiaorg/celestia-node/nodebuilder" "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/nodebuilder/tests/swamp" + "github.com/celestiaorg/celestia-node/state" ) const ( @@ -116,7 +117,7 @@ func TestBlobRPC(t *testing.T) { ) require.NoError(t, err) - height, err := rpcClient.Blob.Submit(ctx, []*blob.Blob{newBlob}, blob.DefaultGasPrice()) + height, err := rpcClient.Blob.Submit(ctx, []*blob.Blob{newBlob}, state.NewTxConfig()) require.NoError(t, err) require.True(t, height != 0) } diff --git a/nodebuilder/tests/blob_test.go b/nodebuilder/tests/blob_test.go index 60af146f67..a1478108a2 100644 --- a/nodebuilder/tests/blob_test.go +++ b/nodebuilder/tests/blob_test.go @@ -18,6 +18,7 @@ import ( "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/nodebuilder/tests/swamp" "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/state" ) func TestBlobModule(t *testing.T) { @@ -58,7 +59,7 @@ func TestBlobModule(t *testing.T) { fullClient := getAdminClient(ctx, fullNode, t) lightClient := getAdminClient(ctx, lightNode, t) - height, err := fullClient.Blob.Submit(ctx, blobs, blob.DefaultGasPrice()) + height, err := fullClient.Blob.Submit(ctx, blobs, state.NewTxConfig()) require.NoError(t, err) _, err = fullClient.Header.WaitForHeight(ctx, height) @@ -127,6 +128,10 @@ func TestBlobModule(t *testing.T) { assert.Nil(t, b) require.Error(t, err) require.ErrorContains(t, err, blob.ErrBlobNotFound.Error()) + + blobs, err := fullClient.Blob.GetAll(ctx, height, []share.Namespace{newBlob.Namespace()}) + require.NoError(t, err) + assert.Empty(t, blobs) }, }, { @@ -141,7 +146,7 @@ func TestBlobModule(t *testing.T) { ) require.NoError(t, err) - height, err := fullClient.Blob.Submit(ctx, []*blob.Blob{b, b}, blob.DefaultGasPrice()) + height, err := fullClient.Blob.Submit(ctx, []*blob.Blob{b, b}, state.NewTxConfig()) require.NoError(t, err) _, err = fullClient.Header.WaitForHeight(ctx, height) @@ -170,7 +175,7 @@ func TestBlobModule(t *testing.T) { // different pfbs. name: "Submit the same blob in different pfb", doFn: func(t *testing.T) { - h, err := fullClient.Blob.Submit(ctx, []*blob.Blob{blobs[0]}, blob.DefaultGasPrice()) + h, err := fullClient.Blob.Submit(ctx, []*blob.Blob{blobs[0]}, state.NewTxConfig()) require.NoError(t, err) _, err = fullClient.Header.WaitForHeight(ctx, h) diff --git a/state/core_access.go b/state/core_access.go index 7ed1c62708..dca866b35c 100644 --- a/state/core_access.go +++ b/state/core_access.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "math" "sync" "time" @@ -14,9 +13,6 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keyring" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdktypes "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - sdktx "github.com/cosmos/cosmos-sdk/types/tx" - auth "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/feegrant" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -30,19 +26,14 @@ import ( "github.com/celestiaorg/celestia-app/app" "github.com/celestiaorg/celestia-app/app/encoding" apperrors "github.com/celestiaorg/celestia-app/app/errors" - "github.com/celestiaorg/celestia-app/pkg/appconsts" "github.com/celestiaorg/celestia-app/pkg/user" - apptypes "github.com/celestiaorg/celestia-app/x/blob/types" libhead "github.com/celestiaorg/go-header" - "github.com/celestiaorg/celestia-node/blob" "github.com/celestiaorg/celestia-node/header" ) const ( - // gasMultiplier is used to increase gas limit in case if tx has additional options. - gasMultiplier = 1.1 - maxRetries = 5 + maxRetries = 5 ) var ( @@ -55,13 +46,6 @@ var ( // to configure parameters. type Option func(ca *CoreAccessor) -// WithGranter is a functional option to configure the granter address parameter. -func WithGranter(addr AccAddress) Option { - return func(ca *CoreAccessor) { - ca.granter = addr - } -} - // CoreAccessor implements service over a gRPC connection // with a celestia-core node. type CoreAccessor struct { @@ -69,12 +53,11 @@ type CoreAccessor struct { cancel context.CancelFunc keyring keyring.Keyring - addr sdktypes.AccAddress - // TODO: (@cmwaters) - once multiple keys within a signer is supported, - // this will no longer be necessary. - // ref: https://github.com/celestiaorg/celestia-app/issues/3259 - signerMu sync.Mutex - signer *user.Signer + client *user.TxClient + + // TODO: remove in scope of https://github.com/celestiaorg/celestia-node/issues/3515 + defaultSignerAccount string + defaultSignerAddress AccAddress getter libhead.Head[*header.ExtendedHeader] @@ -97,11 +80,6 @@ type CoreAccessor struct { // will find a proposer that does accept the transaction. Better would be // to set a global min gas price that correct processes conform to. minGasPrice float64 - - // granter stores the address of the external node that will pay for all transactions, submitted - // by the local node. - // empty granter means that the local node will pay for the transactions. - granter AccAddress } // NewCoreAccessor dials the given celestia-core endpoint and @@ -120,22 +98,13 @@ func NewCoreAccessor( prt.RegisterOpDecoder(storetypes.ProofOpIAVLCommitment, storetypes.CommitmentOpDecoder) prt.RegisterOpDecoder(storetypes.ProofOpSimpleMerkleCommitment, storetypes.CommitmentOpDecoder) - record, err := keyring.Key(keyname) - if err != nil { - return nil, fmt.Errorf("getting key %s: %w", keyname, err) - } - addr, err := record.GetAddress() - if err != nil { - return nil, fmt.Errorf("getting address for key %s: %w", keyname, err) - } - ca := &CoreAccessor{ - keyring: keyring, - addr: addr, - getter: getter, - coreIP: coreIP, - grpcPort: grpcPort, - prt: prt, + keyring: keyring, + defaultSignerAccount: keyname, + getter: getter, + coreIP: coreIP, + grpcPort: grpcPort, + prt: prt, } for _, opt := range options { @@ -176,7 +145,7 @@ func (ca *CoreAccessor) Start(ctx context.Context) error { ca.abciQueryCli = tmservice.NewServiceClient(ca.coreConn) // set up signer to handle tx submission - ca.signer, err = ca.setupSigner(ctx) + ca.client, err = ca.setupTxClient(ctx, ca.defaultSignerAccount) if err != nil { log.Warnw("failed to set up signer, check if node's account is funded", "err", err) } @@ -215,86 +184,84 @@ func (ca *CoreAccessor) cancelCtx() { ca.cancel = nil } -// SubmitPayForBlob builds, signs, and synchronously submits a MsgPayForBlob. It blocks until the -// transaction is committed and returns the TxResponse. If gasLim is set to 0, the method will -// automatically estimate the gas limit. If the fee is negative, the method will use the nodes min -// gas price multiplied by the gas limit. +// SubmitPayForBlob builds, signs, and synchronously submits a MsgPayForBlob with additional options defined +// in `TxConfig`. It blocks until the transaction is committed and returns the TxResponse. +// The user can specify additional options that can bee applied to the Tx. func (ca *CoreAccessor) SubmitPayForBlob( ctx context.Context, - fee Int, - gasLim uint64, - blobs []*blob.Blob, + appblobs []*Blob, + cfg *TxConfig, ) (*TxResponse, error) { - signer, err := ca.getSigner(ctx) - if err != nil { - return nil, err - } - - if len(blobs) == 0 { + if len(appblobs) == 0 { return nil, errors.New("state: no blobs provided") } - appblobs := make([]*apptypes.Blob, len(blobs)) - for i := range blobs { - if err := blobs[i].Namespace().ValidateForBlob(); err != nil { + var feeGrant user.TxOption + if cfg.FeeGranterAddress() != "" { + granter, err := parseAccAddressFromString(cfg.FeeGranterAddress()) + if err != nil { return nil, err } - appblobs[i] = &blobs[i].Blob + feeGrant = user.SetFeeGranter(granter) } - // we only estimate gas if the user wants us to (by setting the gasLim to 0). In the future we may - // want to make these arguments optional. - if gasLim == 0 { - blobSizes := make([]uint32, len(blobs)) - for i, blob := range blobs { + gas := cfg.GasLimit() + if gas == 0 { + blobSizes := make([]uint32, len(appblobs)) + for i, blob := range appblobs { blobSizes[i] = uint32(len(blob.Data)) } - - // TODO (@cmwaters): the default gas per byte and the default tx size cost per byte could be changed - // through governance. This section could be more robust by tracking these values and adjusting the - // gas limit accordingly (as is done for the gas price) - gasLim = apptypes.EstimateGas(blobSizes, appconsts.DefaultGasPerBlobByte, auth.DefaultTxSizeCostPerByte) + gas = estimateGasForBlobs(blobSizes) } - minGasPrice := ca.getMinGasPrice() + gasPrice := cfg.GasPrice() + if cfg.GasPrice() == DefaultGasPrice { + gasPrice = ca.getMinGasPrice() + } - var feeGrant user.TxOption - // set granter and update gasLimit in case node run in a grantee mode - if !ca.granter.Empty() { - feeGrant = user.SetFeeGranter(ca.granter) - gasLim = uint64(float64(gasLim) * gasMultiplier) + signer, err := ca.getSigner(cfg) + if err != nil { + return nil, err } - // set the fee for the user as the minimum gas price multiplied by the gas limit - estimatedFee := false - if fee.IsNegative() { - estimatedFee = true - fee = sdktypes.NewInt(int64(math.Ceil(minGasPrice * float64(gasLim)))) + + accName := ca.defaultSignerAccount + if !signer.Equals(ca.defaultSignerAddress) { + account := ca.client.AccountByAddress(signer) + if account == nil { + return nil, fmt.Errorf("account for signer %s not found", signer) + } + accName = account.Name() } var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { - options := []user.TxOption{user.SetGasLimit(gasLim), withFee(fee)} + opts := []user.TxOption{user.SetGasLimitAndFee(gas, gasPrice)} if feeGrant != nil { - options = append(options, feeGrant) + opts = append(opts, feeGrant) } - response, err := signer.SubmitPayForBlob( + response, err := ca.client.BroadcastPayForBlobWithAccount( ctx, + accName, appblobs, - options..., + opts..., ) + if err != nil { + return nil, err + } + // TODO @vgonkivs: remove me to achieve async blob submission + response, err = ca.client.ConfirmTx(ctx, response.TxHash) // the node is capable of changing the min gas price at any time so we must be able to detect it and // update our version accordingly - if apperrors.IsInsufficientMinGasPrice(err) && estimatedFee { + if apperrors.IsInsufficientMinGasPrice(err) { // The error message contains enough information to parse the new min gas price - minGasPrice, err = apperrors.ParseInsufficientMinGasPrice(err, minGasPrice, gasLim) + gasPrice, err = apperrors.ParseInsufficientMinGasPrice(err, gasPrice, gas) if err != nil { return nil, fmt.Errorf("parsing insufficient min gas price error: %w", err) } - ca.setMinGasPrice(minGasPrice) + + ca.setMinGasPrice(gasPrice) lastErr = err - // update the fee to retry again - fee = sdktypes.NewInt(int64(math.Ceil(minGasPrice * float64(gasLim)))) continue } @@ -306,21 +273,17 @@ func (ca *CoreAccessor) SubmitPayForBlob( if response != nil && response.Code != 0 { err = errors.Join(err, sdkErrors.ABCIError(response.Codespace, response.Code, response.Logs.String())) } - - if err != nil && errors.Is(err, sdkerrors.ErrNotFound) && !ca.granter.Empty() { - return unsetTx(response), errors.New("granter has revoked the grant") - } return unsetTx(response), err } return nil, fmt.Errorf("failed to submit blobs after %d attempts: %w", maxRetries, lastErr) } func (ca *CoreAccessor) AccountAddress(context.Context) (Address, error) { - return Address{ca.addr}, nil + return Address{ca.defaultSignerAddress}, nil } func (ca *CoreAccessor) Balance(ctx context.Context) (*Balance, error) { - return ca.BalanceForAddress(ctx, Address{ca.addr}) + return ca.BalanceForAddress(ctx, Address{ca.defaultSignerAddress}) } func (ca *CoreAccessor) BalanceForAddress(ctx context.Context, addr Address) (*Balance, error) { @@ -396,180 +359,113 @@ func (ca *CoreAccessor) BalanceForAddress(ctx context.Context, addr Address) (*B }, nil } -func (ca *CoreAccessor) SubmitTx(ctx context.Context, tx Tx) (*TxResponse, error) { - txResp, err := apptypes.BroadcastTx(ctx, ca.coreConn, sdktx.BroadcastMode_BROADCAST_MODE_BLOCK, tx) - if err != nil { - return nil, err - } - return unsetTx(txResp.TxResponse), nil -} - -func (ca *CoreAccessor) SubmitTxWithBroadcastMode( - ctx context.Context, - tx Tx, - mode sdktx.BroadcastMode, -) (*TxResponse, error) { - txResp, err := apptypes.BroadcastTx(ctx, ca.coreConn, mode, tx) - if err != nil { - return nil, err - } - return unsetTx(txResp.TxResponse), nil -} - func (ca *CoreAccessor) Transfer( ctx context.Context, addr AccAddress, - amount, - fee Int, - gasLim uint64, + amount Int, + cfg *TxConfig, ) (*TxResponse, error) { - signer, err := ca.getSigner(ctx) - if err != nil { - return nil, err - } - if amount.IsNil() || amount.Int64() <= 0 { return nil, ErrInvalidAmount } - coins := sdktypes.NewCoins(sdktypes.NewCoin(app.BondDenom, amount)) - msg := banktypes.NewMsgSend(signer.Address(), addr, coins) - if gasLim == 0 { - var err error - gasLim, err = signer.EstimateGas(ctx, []sdktypes.Msg{msg}) - if err != nil { - return nil, fmt.Errorf("estimating gas: %w", err) - } + signer, err := ca.getSigner(cfg) + if err != nil { + return nil, err } - resp, err := signer.SubmitTx(ctx, []sdktypes.Msg{msg}, user.SetGasLimit(gasLim), user.SetFee(fee.Uint64())) - return unsetTx(resp), err + + coins := sdktypes.NewCoins(sdktypes.NewCoin(app.BondDenom, amount)) + msg := banktypes.NewMsgSend(signer, addr, coins) + return ca.submitMsg(ctx, msg, cfg) } func (ca *CoreAccessor) CancelUnbondingDelegation( ctx context.Context, valAddr ValAddress, amount, - height, - fee Int, - gasLim uint64, + height Int, + cfg *TxConfig, ) (*TxResponse, error) { - signer, err := ca.getSigner(ctx) - if err != nil { - return nil, err - } - if amount.IsNil() || amount.Int64() <= 0 { return nil, ErrInvalidAmount } - coins := sdktypes.NewCoin(app.BondDenom, amount) - msg := stakingtypes.NewMsgCancelUnbondingDelegation(signer.Address(), valAddr, height.Int64(), coins) - if gasLim == 0 { - var err error - gasLim, err = signer.EstimateGas(ctx, []sdktypes.Msg{msg}) - if err != nil { - return nil, fmt.Errorf("estimating gas: %w", err) - } + signer, err := ca.getSigner(cfg) + if err != nil { + return nil, err } - resp, err := signer.SubmitTx(ctx, []sdktypes.Msg{msg}, user.SetGasLimit(gasLim), user.SetFee(fee.Uint64())) - return unsetTx(resp), err + coins := sdktypes.NewCoin(app.BondDenom, amount) + msg := stakingtypes.NewMsgCancelUnbondingDelegation(signer, valAddr, height.Int64(), coins) + return ca.submitMsg(ctx, msg, cfg) } func (ca *CoreAccessor) BeginRedelegate( ctx context.Context, srcValAddr, dstValAddr ValAddress, - amount, - fee Int, - gasLim uint64, + amount Int, + cfg *TxConfig, ) (*TxResponse, error) { - signer, err := ca.getSigner(ctx) - if err != nil { - return nil, err - } - if amount.IsNil() || amount.Int64() <= 0 { return nil, ErrInvalidAmount } - coins := sdktypes.NewCoin(app.BondDenom, amount) - msg := stakingtypes.NewMsgBeginRedelegate(signer.Address(), srcValAddr, dstValAddr, coins) - if gasLim == 0 { - var err error - gasLim, err = signer.EstimateGas(ctx, []sdktypes.Msg{msg}) - if err != nil { - return nil, fmt.Errorf("estimating gas: %w", err) - } + signer, err := ca.getSigner(cfg) + if err != nil { + return nil, err } - resp, err := signer.SubmitTx(ctx, []sdktypes.Msg{msg}, user.SetGasLimit(gasLim), user.SetFee(fee.Uint64())) - return unsetTx(resp), err + coins := sdktypes.NewCoin(app.BondDenom, amount) + msg := stakingtypes.NewMsgBeginRedelegate(signer, srcValAddr, dstValAddr, coins) + return ca.submitMsg(ctx, msg, cfg) } func (ca *CoreAccessor) Undelegate( ctx context.Context, delAddr ValAddress, - amount, - fee Int, - gasLim uint64, + amount Int, + cfg *TxConfig, ) (*TxResponse, error) { - signer, err := ca.getSigner(ctx) - if err != nil { - return nil, err - } - if amount.IsNil() || amount.Int64() <= 0 { return nil, ErrInvalidAmount } - coins := sdktypes.NewCoin(app.BondDenom, amount) - msg := stakingtypes.NewMsgUndelegate(signer.Address(), delAddr, coins) - if gasLim == 0 { - var err error - gasLim, err = signer.EstimateGas(ctx, []sdktypes.Msg{msg}) - if err != nil { - return nil, fmt.Errorf("estimating gas: %w", err) - } + signer, err := ca.getSigner(cfg) + if err != nil { + return nil, err } - resp, err := signer.SubmitTx(ctx, []sdktypes.Msg{msg}, user.SetGasLimit(gasLim), user.SetFee(fee.Uint64())) - return unsetTx(resp), err + + coins := sdktypes.NewCoin(app.BondDenom, amount) + msg := stakingtypes.NewMsgUndelegate(signer, delAddr, coins) + return ca.submitMsg(ctx, msg, cfg) } func (ca *CoreAccessor) Delegate( ctx context.Context, delAddr ValAddress, amount Int, - fee Int, - gasLim uint64, + cfg *TxConfig, ) (*TxResponse, error) { - signer, err := ca.getSigner(ctx) - if err != nil { - return nil, err - } - if amount.IsNil() || amount.Int64() <= 0 { return nil, ErrInvalidAmount } - coins := sdktypes.NewCoin(app.BondDenom, amount) - msg := stakingtypes.NewMsgDelegate(signer.Address(), delAddr, coins) - if gasLim == 0 { - var err error - gasLim, err = signer.EstimateGas(ctx, []sdktypes.Msg{msg}) - if err != nil { - return nil, fmt.Errorf("estimating gas: %w", err) - } + signer, err := ca.getSigner(cfg) + if err != nil { + return nil, err } - resp, err := signer.SubmitTx(ctx, []sdktypes.Msg{msg}, user.SetGasLimit(gasLim), user.SetFee(fee.Uint64())) - return unsetTx(resp), err + + coins := sdktypes.NewCoin(app.BondDenom, amount) + msg := stakingtypes.NewMsgDelegate(signer, delAddr, coins) + return ca.submitMsg(ctx, msg, cfg) } func (ca *CoreAccessor) QueryDelegation( ctx context.Context, valAddr ValAddress, ) (*stakingtypes.QueryDelegationResponse, error) { - delAddr := ca.addr + delAddr := ca.defaultSignerAddress return ca.stakingCli.Delegation(ctx, &stakingtypes.QueryDelegationRequest{ DelegatorAddr: delAddr.String(), ValidatorAddr: valAddr.String(), @@ -580,7 +476,7 @@ func (ca *CoreAccessor) QueryUnbonding( ctx context.Context, valAddr ValAddress, ) (*stakingtypes.QueryUnbondingDelegationResponse, error) { - delAddr := ca.addr + delAddr := ca.defaultSignerAddress return ca.stakingCli.UnbondingDelegation(ctx, &stakingtypes.QueryUnbondingDelegationRequest{ DelegatorAddr: delAddr.String(), ValidatorAddr: valAddr.String(), @@ -592,7 +488,7 @@ func (ca *CoreAccessor) QueryRedelegations( srcValAddr, dstValAddr ValAddress, ) (*stakingtypes.QueryRedelegationsResponse, error) { - delAddr := ca.addr + delAddr := ca.defaultSignerAddress return ca.stakingCli.Redelegations(ctx, &stakingtypes.QueryRedelegationsRequest{ DelegatorAddr: delAddr.String(), SrcValidatorAddr: srcValAddr.String(), @@ -603,17 +499,14 @@ func (ca *CoreAccessor) QueryRedelegations( func (ca *CoreAccessor) GrantFee( ctx context.Context, grantee AccAddress, - amount, - fee Int, - gasLim uint64, + amount Int, + cfg *TxConfig, ) (*TxResponse, error) { - signer, err := ca.getSigner(ctx) + granter, err := ca.getSigner(cfg) if err != nil { return nil, err } - granter := signer.Address() - allowance := &feegrant.BasicAllowance{} if !amount.IsZero() { // set spend limit @@ -624,27 +517,21 @@ func (ca *CoreAccessor) GrantFee( if err != nil { return nil, err } - - resp, err := signer.SubmitTx(ctx, []sdktypes.Msg{msg}, user.SetGasLimit(gasLim), withFee(fee)) - return unsetTx(resp), err + return ca.submitMsg(ctx, msg, cfg) } func (ca *CoreAccessor) RevokeGrantFee( ctx context.Context, grantee AccAddress, - fee Int, - gasLim uint64, + cfg *TxConfig, ) (*TxResponse, error) { - signer, err := ca.getSigner(ctx) + granter, err := ca.getSigner(cfg) if err != nil { return nil, err } - granter := signer.Address() - msg := feegrant.NewMsgRevokeAllowance(granter, grantee) - resp, err := signer.SubmitTx(ctx, []sdktypes.Msg{&msg}, user.SetGasLimit(gasLim), withFee(fee)) - return unsetTx(resp), err + return ca.submitMsg(ctx, &msg, cfg) } func (ca *CoreAccessor) LastPayForBlob() int64 { @@ -694,30 +581,68 @@ func (ca *CoreAccessor) queryMinimumGasPrice( return coins.AmountOf(app.BondDenom).MustFloat64(), nil } -// getSigner returns the signer if it has already been constructed, otherwise -// it will attempt to set it up. The signer can only be constructed if the account -// exists / is funded. -func (ca *CoreAccessor) getSigner(ctx context.Context) (*user.Signer, error) { - ca.signerMu.Lock() - defer ca.signerMu.Unlock() +func (ca *CoreAccessor) setupTxClient(ctx context.Context, keyName string) (*user.TxClient, error) { + encCfg := encoding.MakeConfig(app.ModuleEncodingRegisters...) + // explicitly set default address. Otherwise, there could be a mismatch between defaultKey and defaultAddress. + rec, err := ca.keyring.Key(keyName) + if err != nil { + return nil, err + } + addr, err := rec.GetAddress() + if err != nil { + return nil, err + } + ca.defaultSignerAddress = addr + return user.SetupTxClient(ctx, ca.keyring, ca.coreConn, encCfg, + user.WithDefaultAccount(keyName), user.WithDefaultAddress(addr), + ) +} - if ca.signer != nil { - return ca.signer, nil +func (ca *CoreAccessor) submitMsg( + ctx context.Context, + msg sdktypes.Msg, + cfg *TxConfig, +) (*TxResponse, error) { + txConfig := make([]user.TxOption, 0) + var ( + gas = cfg.GasLimit() + err error + ) + if gas == 0 { + gas, err = estimateGas(ctx, ca.client, msg) + if err != nil { + return nil, fmt.Errorf("estimating gas: %w", err) + } } - var err error - ca.signer, err = ca.setupSigner(ctx) - return ca.signer, err -} + gasPrice := cfg.GasPrice() + if gasPrice == DefaultGasPrice { + gasPrice = ca.minGasPrice + } -func (ca *CoreAccessor) setupSigner(ctx context.Context) (*user.Signer, error) { - encCfg := encoding.MakeConfig(app.ModuleEncodingRegisters...) - return user.SetupSigner(ctx, ca.keyring, ca.coreConn, ca.addr, encCfg) + txConfig = append(txConfig, user.SetGasLimitAndFee(gas, gasPrice)) + + if cfg.FeeGranterAddress() != "" { + granter, err := parseAccAddressFromString(cfg.FeeGranterAddress()) + if err != nil { + return nil, fmt.Errorf("getting granter: %w", err) + } + txConfig = append(txConfig, user.SetFeeGranter(granter)) + } + + resp, err := ca.client.SubmitTx(ctx, []sdktypes.Msg{msg}, txConfig...) + return unsetTx(resp), err } -func withFee(fee Int) user.TxOption { - gasFee := sdktypes.NewCoins(sdktypes.NewCoin(app.BondDenom, fee)) - return user.SetFeeAmount(gasFee) +func (ca *CoreAccessor) getSigner(cfg *TxConfig) (AccAddress, error) { + switch { + case cfg.SignerAddress() != "": + return parseAccAddressFromString(cfg.SignerAddress()) + case cfg.KeyName() != "" && cfg.KeyName() != ca.defaultSignerAccount: + return parseAccountKey(ca.keyring, cfg.KeyName()) + default: + return ca.defaultSignerAddress, nil + } } // THIS IS A TEMPORARY SOLUTION!!! diff --git a/state/core_access_test.go b/state/core_access_test.go index c9bb60f26a..ce8f77b9e9 100644 --- a/state/core_access_test.go +++ b/state/core_access_test.go @@ -10,36 +10,22 @@ import ( "testing" "time" - "cosmossdk.io/math" sdktypes "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" "github.com/celestiaorg/celestia-app/app" "github.com/celestiaorg/celestia-app/pkg/appconsts" "github.com/celestiaorg/celestia-app/test/util/testnode" - blobtypes "github.com/celestiaorg/celestia-app/x/blob/types" + apptypes "github.com/celestiaorg/celestia-app/x/blob/types" - "github.com/celestiaorg/celestia-node/blob" "github.com/celestiaorg/celestia-node/share" ) func TestSubmitPayForBlob(t *testing.T) { - accounts := []string{"jimy", "rob"} - tmCfg := testnode.DefaultTendermintConfig() - tmCfg.Consensus.TimeoutCommit = time.Millisecond * 1 - appConf := testnode.DefaultAppConfig() - appConf.API.Enable = true - appConf.MinGasPrices = fmt.Sprintf("0.002%s", app.BondDenom) - - config := testnode.DefaultConfig().WithTendermintConfig(tmCfg).WithAppConfig(appConf).WithAccounts(accounts) - cctx, _, grpcAddr := testnode.NewNetwork(t, config) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - ca, err := NewCoreAccessor(cctx.Keyring, accounts[0], nil, "127.0.0.1", extractPort(grpcAddr)) - require.NoError(t, err) + ctx := context.Background() + ca, accounts := buildAccessor(t) // start the accessor - err = ca.Start(ctx) + err := ca.Start(ctx) require.NoError(t, err) t.Cleanup(func() { _ = ca.Stop(ctx) @@ -47,7 +33,7 @@ func TestSubmitPayForBlob(t *testing.T) { ns, err := share.NewBlobNamespaceV0([]byte("namespace")) require.NoError(t, err) - blobbyTheBlob, err := blob.NewBlobV0(ns, []byte("data")) + blobbyTheBlob, err := apptypes.NewBlob(ns.ToAppNamespace(), []byte("data"), 0) require.NoError(t, err) minGas, err := ca.queryMinimumGasPrice(ctx) @@ -55,25 +41,25 @@ func TestSubmitPayForBlob(t *testing.T) { require.Equal(t, appconsts.DefaultMinGasPrice, minGas) testcases := []struct { - name string - blobs []*blob.Blob - fee math.Int - gasLim uint64 - expErr error + name string + blobs []*apptypes.Blob + gasPrice float64 + gasLim uint64 + expErr error }{ { - name: "empty blobs", - blobs: []*blob.Blob{}, - fee: sdktypes.ZeroInt(), - gasLim: 0, - expErr: errors.New("state: no blobs provided"), + name: "empty blobs", + blobs: []*apptypes.Blob{}, + gasPrice: DefaultGasPrice, + gasLim: 0, + expErr: errors.New("state: no blobs provided"), }, { - name: "good blob with user provided gas and fees", - blobs: []*blob.Blob{blobbyTheBlob}, - fee: sdktypes.NewInt(10_000), // roughly 0.12 utia per gas (should be good) - gasLim: blobtypes.DefaultEstimateGas([]uint32{uint32(len(blobbyTheBlob.Data))}), - expErr: nil, + name: "good blob with user provided gas and fees", + blobs: []*apptypes.Blob{blobbyTheBlob}, + gasPrice: 0.005, + gasLim: apptypes.DefaultEstimateGas([]uint32{uint32(len(blobbyTheBlob.Data))}), + expErr: nil, }, // TODO: add more test cases. The problem right now is that the celestia-app doesn't // correctly construct the node (doesn't pass the min gas price) hence the price on @@ -82,7 +68,22 @@ func TestSubmitPayForBlob(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - resp, err := ca.SubmitPayForBlob(ctx, tc.fee, tc.gasLim, tc.blobs) + resp, err := ca.SubmitPayForBlob(ctx, tc.blobs, NewTxConfig()) + require.Equal(t, tc.expErr, err) + if err == nil { + require.EqualValues(t, 0, resp.Code) + } + }) + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + opts := NewTxConfig( + WithGas(tc.gasLim), + WithGasPrice(tc.gasPrice), + WithKeyName(accounts[2]), + ) + resp, err := ca.SubmitPayForBlob(ctx, tc.blobs, opts) require.Equal(t, tc.expErr, err) if err == nil { require.EqualValues(t, 0, resp.Code) @@ -91,7 +92,140 @@ func TestSubmitPayForBlob(t *testing.T) { } } +func TestTransfer(t *testing.T) { + ctx := context.Background() + ca, accounts := buildAccessor(t) + // start the accessor + err := ca.Start(ctx) + require.NoError(t, err) + t.Cleanup(func() { + _ = ca.Stop(ctx) + }) + + minGas, err := ca.queryMinimumGasPrice(ctx) + require.NoError(t, err) + require.Equal(t, appconsts.DefaultMinGasPrice, minGas) + + testcases := []struct { + name string + gasPrice float64 + gasLim uint64 + account string + expErr error + }{ + { + name: "transfer without options", + gasPrice: DefaultGasPrice, + gasLim: 0, + account: "", + expErr: nil, + }, + { + name: "transfer with options", + gasPrice: 0.005, + gasLim: 0, + account: accounts[2], + expErr: nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + opts := NewTxConfig( + WithGas(tc.gasLim), + WithGasPrice(tc.gasPrice), + WithKeyName(accounts[2]), + ) + key, err := ca.keyring.Key(accounts[1]) + require.NoError(t, err) + addr, err := key.GetAddress() + require.NoError(t, err) + + resp, err := ca.Transfer(ctx, addr, sdktypes.NewInt(10_000), opts) + require.Equal(t, tc.expErr, err) + if err == nil { + require.EqualValues(t, 0, resp.Code) + } + }) + } +} + +func TestDelegate(t *testing.T) { + ctx := context.Background() + ca, accounts := buildAccessor(t) + // start the accessor + err := ca.Start(ctx) + require.NoError(t, err) + t.Cleanup(func() { + _ = ca.Stop(ctx) + }) + + minGas, err := ca.queryMinimumGasPrice(ctx) + require.NoError(t, err) + require.Equal(t, appconsts.DefaultMinGasPrice, minGas) + + valRec, err := ca.keyring.Key("validator") + require.NoError(t, err) + valAddr, err := valRec.GetAddress() + require.NoError(t, err) + + testcases := []struct { + name string + gasPrice float64 + gasLim uint64 + account string + }{ + { + name: "delegate/undelegate without options", + gasPrice: DefaultGasPrice, + gasLim: 0, + account: "", + }, + { + name: "delegate/undelegate with options", + gasPrice: 0.005, + gasLim: 0, + account: accounts[2], + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + opts := NewTxConfig( + WithGas(tc.gasLim), + WithGasPrice(tc.gasPrice), + WithKeyName(accounts[2]), + ) + resp, err := ca.Delegate(ctx, ValAddress(valAddr), sdktypes.NewInt(100_000), opts) + require.NoError(t, err) + require.EqualValues(t, 0, resp.Code) + + resp, err = ca.Undelegate(ctx, ValAddress(valAddr), sdktypes.NewInt(100_000), opts) + require.NoError(t, err) + require.EqualValues(t, 0, resp.Code) + }) + } +} + func extractPort(addr string) string { splitStr := strings.Split(addr, ":") return splitStr[len(splitStr)-1] } + +func buildAccessor(t *testing.T) (*CoreAccessor, []string) { + t.Helper() + accounts := []string{"jimmy", "carl", "sheen", "cindy"} + tmCfg := testnode.DefaultTendermintConfig() + tmCfg.Consensus.TimeoutCommit = time.Millisecond * 1 + + appConf := testnode.DefaultAppConfig() + appConf.API.Enable = true + appConf.MinGasPrices = fmt.Sprintf("0.002%s", app.BondDenom) + + config := testnode.DefaultConfig().WithTendermintConfig(tmCfg).WithAppConfig(appConf).WithAccounts(accounts) + cctx, _, grpcAddr := testnode.NewNetwork(t, config) + + ca, err := NewCoreAccessor(cctx.Keyring, accounts[0], nil, "127.0.0.1", extractPort(grpcAddr)) + require.NoError(t, err) + return ca, accounts +} diff --git a/state/state.go b/state/state.go index d55bb6901c..e5ed1c557a 100644 --- a/state/state.go +++ b/state/state.go @@ -7,6 +7,8 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" coretypes "github.com/tendermint/tendermint/types" + + apptypes "github.com/celestiaorg/celestia-app/x/blob/types" ) // Balance is an alias to the Coin type from Cosmos-SDK. @@ -24,6 +26,9 @@ type Address struct { sdk.Address } +// Blob is an alias of Blob from celestia-app. +type Blob = apptypes.Blob + // ValAddress is an alias to the ValAddress type from Cosmos-SDK. type ValAddress = sdk.ValAddress diff --git a/state/tx_config.go b/state/tx_config.go new file mode 100644 index 0000000000..1a7480e607 --- /dev/null +++ b/state/tx_config.go @@ -0,0 +1,187 @@ +package state + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdktypes "github.com/cosmos/cosmos-sdk/types" + + "github.com/celestiaorg/celestia-app/pkg/user" + apptypes "github.com/celestiaorg/celestia-app/x/blob/types" +) + +const ( + // DefaultGasPrice specifies the default gas price value to be used when the user + // wants to use the global minimal gas price, which is fetched from the celestia-app. + DefaultGasPrice float64 = -1.0 + // gasMultiplier is used to increase gas limit in case if tx has additional cfg. + gasMultiplier = 1.1 +) + +// NewTxConfig constructs a new TxConfig with the provided options. +// It starts with a DefaultGasPrice and then applies any additional +// options provided through the variadic parameter. +func NewTxConfig(opts ...ConfigOption) *TxConfig { + options := &TxConfig{gasPrice: DefaultGasPrice} + for _, opt := range opts { + opt(options) + } + return options +} + +// TxConfig specifies additional options that will be applied to the Tx. +type TxConfig struct { + // Specifies the address from the keystore that will sign transactions. + // NOTE: Only `signerAddress` or `KeyName` should be passed. + // signerAddress is a primary cfg. This means If both the address and the key are specified, + // the address field will take priority. + signerAddress string + // Specifies the key from the keystore associated with an account that + // will be used to sign transactions. + // NOTE: This `Account` must be available in the `Keystore`. + keyName string + // gasPrice represents the amount to be paid per gas unit. + // Negative gasPrice means user want us to use the minGasPrice + // defined in the node. + gasPrice float64 + // since gasPrice can be 0, it is necessary to understand that user explicitly set it. + isGasPriceSet bool + // 0 gas means users want us to calculate it for them. + gas uint64 + // Specifies the account that will pay for the transaction. + // Input format Bech32. + feeGranterAddress string +} + +func (cfg *TxConfig) GasPrice() float64 { + if !cfg.isGasPriceSet { + return DefaultGasPrice + } + return cfg.gasPrice +} + +func (cfg *TxConfig) GasLimit() uint64 { return cfg.gas } + +func (cfg *TxConfig) KeyName() string { return cfg.keyName } + +func (cfg *TxConfig) SignerAddress() string { return cfg.signerAddress } + +func (cfg *TxConfig) FeeGranterAddress() string { return cfg.feeGranterAddress } + +type jsonTxConfig struct { + GasPrice float64 `json:"gas_price,omitempty"` + IsGasPriceSet bool `json:"is_gas_price_set,omitempty"` + Gas uint64 `json:"gas,omitempty"` + KeyName string `json:"key_name,omitempty"` + SignerAddress string `json:"signer_address,omitempty"` + FeeGranterAddress string `json:"fee_granter_address,omitempty"` +} + +func (cfg *TxConfig) MarshalJSON() ([]byte, error) { + jsonOpts := &jsonTxConfig{ + SignerAddress: cfg.signerAddress, + KeyName: cfg.keyName, + GasPrice: cfg.gasPrice, + IsGasPriceSet: cfg.isGasPriceSet, + Gas: cfg.gas, + FeeGranterAddress: cfg.feeGranterAddress, + } + return json.Marshal(jsonOpts) +} + +func (cfg *TxConfig) UnmarshalJSON(data []byte) error { + var jsonOpts jsonTxConfig + err := json.Unmarshal(data, &jsonOpts) + if err != nil { + return fmt.Errorf("unmarshalling TxConfig: %w", err) + } + + cfg.keyName = jsonOpts.KeyName + cfg.signerAddress = jsonOpts.SignerAddress + cfg.gasPrice = jsonOpts.GasPrice + cfg.isGasPriceSet = jsonOpts.IsGasPriceSet + cfg.gas = jsonOpts.Gas + cfg.feeGranterAddress = jsonOpts.FeeGranterAddress + return nil +} + +// estimateGas estimates gas in case it has not been set. +// NOTE: final result of the estimation will be multiplied by the `gasMultiplier`(1.1) to cover additional costs. +func estimateGas(ctx context.Context, client *user.TxClient, msg sdktypes.Msg) (uint64, error) { + // set fee as 1utia helps to simulate the tx more reliably. + gas, err := client.EstimateGas(ctx, []sdktypes.Msg{msg}, user.SetFee(1)) + if err != nil { + return 0, fmt.Errorf("estimating gas: %w", err) + } + return uint64(float64(gas) * gasMultiplier), nil +} + +// estimateGasForBlobs returns a gas limit that can be applied to the `MsgPayForBlob` transactions. +// NOTE: final result of the estimation will be multiplied by the `gasMultiplier`(1.1) +// to cover additional options of the Tx. +func estimateGasForBlobs(blobSizes []uint32) uint64 { + gas := apptypes.DefaultEstimateGas(blobSizes) + return uint64(float64(gas) * gasMultiplier) +} + +func parseAccountKey(kr keyring.Keyring, accountKey string) (sdktypes.AccAddress, error) { + rec, err := kr.Key(accountKey) + if err != nil { + return nil, fmt.Errorf("getting account key: %w", err) + } + return rec.GetAddress() +} + +func parseAccAddressFromString(addrStr string) (sdktypes.AccAddress, error) { + return sdktypes.AccAddressFromBech32(addrStr) +} + +// ConfigOption is the functional option that is applied to the TxConfig instance +// to configure parameters. +type ConfigOption func(cfg *TxConfig) + +// WithGasPrice is an option that allows to specify a GasPrice, which is needed +// to calculate the fee. In case GasPrice is not specified, the global GasPrice fetched from +// celestia-app will be used. +func WithGasPrice(gasPrice float64) ConfigOption { + return func(cfg *TxConfig) { + if gasPrice >= 0 { + cfg.gasPrice = gasPrice + cfg.isGasPriceSet = true + } + } +} + +// WithGas is an option that allows to specify Gas. +// Gas will be calculated in case it wasn't specified. +func WithGas(gas uint64) ConfigOption { + return func(cfg *TxConfig) { + cfg.gas = gas + } +} + +// WithKeyName is an option that allows you to specify an KeyName, which is needed to +// sign the transaction. This key should be associated with the address and stored +// locally in the key store. Default Account will be used in case it wasn't specified. +func WithKeyName(key string) ConfigOption { + return func(cfg *TxConfig) { + cfg.keyName = key + } +} + +// WithSignerAddress is an option that allows you to specify an address, that will sign the transaction. +// This address must be stored locally in the key store. Default signerAddress will be used in case it wasn't specified. +func WithSignerAddress(address string) ConfigOption { + return func(cfg *TxConfig) { + cfg.signerAddress = address + } +} + +// WithFeeGranterAddress is an option that allows you to specify a GranterAddress to pay the fees. +func WithFeeGranterAddress(granter string) ConfigOption { + return func(cfg *TxConfig) { + cfg.feeGranterAddress = granter + } +} diff --git a/state/tx_config_test.go b/state/tx_config_test.go new file mode 100644 index 0000000000..69ade8f24d --- /dev/null +++ b/state/tx_config_test.go @@ -0,0 +1,26 @@ +package state + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMarshallingOptions(t *testing.T) { + opts := NewTxConfig( + WithGas(10_000), + WithGasPrice(0.002), + WithKeyName("test"), + WithSignerAddress("celestia1eucs6ax66ypjcmwj81hak531w6hyr2c4g8cfsgc"), + WithFeeGranterAddress("celestia1hakc56ax66ypjcmwj8w6hyr2c4g8cfs3wesguc"), + ) + + data, err := json.Marshal(opts) + require.NoError(t, err) + + newOpts := &TxConfig{} + err = json.Unmarshal(data, newOpts) + require.NoError(t, err) + require.Equal(t, opts, newOpts) +}