Skip to content

Commit b6c8d5e

Browse files
aleem1314amaury1093alexanderbez
authored
Add tx broadcast gRPC endpoint (#7852)
* WIP tx/broadcast grpc endpoint * fix lint * fix proto lint * Update service.proto * resolve conflicts * update service.proto * Update service.proto * review changes * proto lint * Switch to txraw * Add check breaking at the end * Fix broadcast * Send Msg on SetupSuite * Remove proto-check-breaking * 1 validator in test * Add grpc server tests for broadcast * Fix grpc server tests * Add some changes * Add ress comments * Add table tests for tx service * Add test for mode * Add simulate tests * Add build flag back * Revert custom stringer for enum * Remove stray logs * Use /txs/{hash} Co-authored-by: Amaury Martiny <amaury.martiny@protonmail.com> Co-authored-by: Aleksandr Bezobchuk <alexanderbez@users.noreply.github.com>
1 parent f57828c commit b6c8d5e

File tree

12 files changed

+1146
-206
lines changed

12 files changed

+1146
-206
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ devdoc-update:
357357
### Protobuf ###
358358
###############################################################################
359359

360-
proto-all: proto-format proto-lint proto-check-breaking proto-gen
360+
proto-all: proto-format proto-lint proto-gen
361361

362362
proto-gen:
363363
@echo "Generating Protobuf files"

client/broadcast.go

+36
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import (
77

88
"github.com/tendermint/tendermint/crypto/tmhash"
99
"github.com/tendermint/tendermint/mempool"
10+
"google.golang.org/grpc/codes"
11+
"google.golang.org/grpc/status"
1012

1113
"github.com/cosmos/cosmos-sdk/client/flags"
1214
sdk "github.com/cosmos/cosmos-sdk/types"
1315
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
16+
"github.com/cosmos/cosmos-sdk/types/tx"
1417
)
1518

1619
// BroadcastTx broadcasts a transactions either synchronously or asynchronously
@@ -142,3 +145,36 @@ func (ctx Context) BroadcastTxAsync(txBytes []byte) (*sdk.TxResponse, error) {
142145

143146
return sdk.NewResponseFormatBroadcastTx(res), err
144147
}
148+
149+
// TxServiceBroadcast is a helper function to broadcast a Tx with the correct gRPC types
150+
// from the tx service. Calls `clientCtx.BroadcastTx` under the hood.
151+
func TxServiceBroadcast(grpcCtx context.Context, clientCtx Context, req *tx.BroadcastTxRequest) (*tx.BroadcastTxResponse, error) {
152+
if req == nil || req.TxBytes == nil {
153+
return nil, status.Error(codes.InvalidArgument, "invalid empty tx")
154+
}
155+
156+
clientCtx = clientCtx.WithBroadcastMode(normalizeBroadcastMode(req.Mode))
157+
resp, err := clientCtx.BroadcastTx(req.TxBytes)
158+
if err != nil {
159+
return nil, err
160+
}
161+
162+
return &tx.BroadcastTxResponse{
163+
TxResponse: resp,
164+
}, nil
165+
}
166+
167+
// normalizeBroadcastMode converts a broadcast mode into a normalized string
168+
// to be passed into the clientCtx.
169+
func normalizeBroadcastMode(mode tx.BroadcastMode) string {
170+
switch mode {
171+
case tx.BroadcastMode_BROADCAST_MODE_ASYNC:
172+
return "async"
173+
case tx.BroadcastMode_BROADCAST_MODE_BLOCK:
174+
return "block"
175+
case tx.BroadcastMode_BROADCAST_MODE_SYNC:
176+
return "sync"
177+
default:
178+
return "unspecified"
179+
}
180+
}

client/grpc_query.go

+38-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package client
33
import (
44
gocontext "context"
55
"fmt"
6+
"reflect"
67
"strconv"
78

89
gogogrpc "github.com/gogo/protobuf/grpc"
@@ -12,18 +13,48 @@ import (
1213
"google.golang.org/grpc/encoding/proto"
1314
"google.golang.org/grpc/metadata"
1415

15-
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
16-
1716
"github.com/cosmos/cosmos-sdk/codec/types"
1817
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
18+
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
19+
"github.com/cosmos/cosmos-sdk/types/tx"
1920
)
2021

2122
var _ gogogrpc.ClientConn = Context{}
2223

2324
var protoCodec = encoding.GetCodec(proto.Name)
2425

2526
// Invoke implements the grpc ClientConn.Invoke method
26-
func (ctx Context) Invoke(grpcCtx gocontext.Context, method string, args, reply interface{}, opts ...grpc.CallOption) error {
27+
func (ctx Context) Invoke(grpcCtx gocontext.Context, method string, args, reply interface{}, opts ...grpc.CallOption) (err error) {
28+
// Two things can happen here:
29+
// 1. either we're broadcasting a Tx, in which call we call Tendermint's broadcast endpoint directly,
30+
// 2. or we are querying for state, in which case we call ABCI's Query.
31+
32+
// In both cases, we don't allow empty request args (it will panic unexpectedly).
33+
if reflect.ValueOf(args).IsNil() {
34+
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "request cannot be nil")
35+
}
36+
37+
// Case 1. Broadcasting a Tx.
38+
if isBroadcast(method) {
39+
req, ok := args.(*tx.BroadcastTxRequest)
40+
if !ok {
41+
return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "expected %T, got %T", (*tx.BroadcastTxRequest)(nil), args)
42+
}
43+
res, ok := reply.(*tx.BroadcastTxResponse)
44+
if !ok {
45+
return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "expected %T, got %T", (*tx.BroadcastTxResponse)(nil), args)
46+
}
47+
48+
broadcastRes, err := TxServiceBroadcast(grpcCtx, ctx, req)
49+
if err != nil {
50+
return err
51+
}
52+
*res = *broadcastRes
53+
54+
return err
55+
}
56+
57+
// Case 2. Querying state.
2758
reqBz, err := protoCodec.Marshal(args)
2859
if err != nil {
2960
return err
@@ -86,3 +117,7 @@ func (ctx Context) Invoke(grpcCtx gocontext.Context, method string, args, reply
86117
func (Context) NewStream(gocontext.Context, *grpc.StreamDesc, string, ...grpc.CallOption) (grpc.ClientStream, error) {
87118
return nil, fmt.Errorf("streaming rpc not supported")
88119
}
120+
121+
func isBroadcast(method string) bool {
122+
return method == "/cosmos.tx.v1beta1.Service/BroadcastTx"
123+
}

docs/migrations/rest.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Some modules expose legacy `POST` endpoints to generate unsigned transactions fo
3131

3232
| Legacy REST Endpoint | Description | New gGPC-gateway REST Endpoint |
3333
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
34-
| `GET /txs/{hash}` | Query tx by hash | `GET /cosmos/tx/v1beta1/tx/{hash}` |
34+
| `GET /txs/{hash}` | Query tx by hash | `GET /cosmos/tx/v1beta1/txs/{hash}` |
3535
| `GET /txs` | Query tx by events | `GET /cosmos/tx/v1beta1/txs` |
3636
| `POST /txs` | Broadcast tx | `POST /cosmos/tx/v1beta1/txs` |
3737
| `POST /txs/encode` | Encodes an Amino JSON tx to an Amino binary tx | N/A, use Protobuf directly |

proto/cosmos/tx/v1beta1/service.proto

+43-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package cosmos.tx.v1beta1;
44
import "google/api/annotations.proto";
55
import "cosmos/base/abci/v1beta1/abci.proto";
66
import "cosmos/tx/v1beta1/tx.proto";
7+
import "gogoproto/gogo.proto";
78
import "cosmos/base/query/v1beta1/pagination.proto";
89

910
option go_package = "github.com/cosmos/cosmos-sdk/types/tx";
@@ -12,13 +13,22 @@ option go_package = "github.com/cosmos/cosmos-sdk/types/tx";
1213
service Service {
1314
// Simulate simulates executing a transaction for estimating gas usage.
1415
rpc Simulate(SimulateRequest) returns (SimulateResponse) {
15-
option (google.api.http).post = "/cosmos/tx/v1beta1/simulate";
16+
option (google.api.http) = {
17+
post: "/cosmos/tx/v1beta1/simulate"
18+
body: "*"
19+
};
1620
}
1721
// GetTx fetches a tx by hash.
1822
rpc GetTx(GetTxRequest) returns (GetTxResponse) {
19-
option (google.api.http).get = "/cosmos/tx/v1beta1/tx/{hash}";
23+
option (google.api.http).get = "/cosmos/tx/v1beta1/txs/{hash}";
24+
}
25+
// BroadcastTx broadcast transaction.
26+
rpc BroadcastTx(BroadcastTxRequest) returns (BroadcastTxResponse) {
27+
option (google.api.http) = {
28+
post: "/cosmos/tx/v1beta1/txs"
29+
body: "*"
30+
};
2031
}
21-
2232
// GetTxsEvent fetches txs by event.
2333
rpc GetTxsEvent(GetTxsEventRequest) returns (GetTxsEventResponse) {
2434
option (google.api.http).get = "/cosmos/tx/v1beta1/txs";
@@ -45,6 +55,36 @@ message GetTxsEventResponse {
4555
cosmos.base.query.v1beta1.PageResponse pagination = 3;
4656
}
4757

58+
// BroadcastTxRequest is the request type for the Service.BroadcastTxRequest
59+
// RPC method.
60+
message BroadcastTxRequest {
61+
// tx_bytes is the raw transaction.
62+
bytes tx_bytes = 1;
63+
BroadcastMode mode = 2;
64+
}
65+
66+
// BroadcastMode specifies the broadcast mode for the TxService.Broadcast RPC method.
67+
enum BroadcastMode {
68+
// zero-value for mode ordering
69+
BROADCAST_MODE_UNSPECIFIED = 0;
70+
// BROADCAST_MODE_BLOCK defines a tx broadcasting mode where the client waits for
71+
// the tx to be committed in a block.
72+
BROADCAST_MODE_BLOCK = 1;
73+
// BROADCAST_MODE_SYNC defines a tx broadcasting mode where the client waits for
74+
// a CheckTx execution response only.
75+
BROADCAST_MODE_SYNC = 2;
76+
// BROADCAST_MODE_ASYNC defines a tx broadcasting mode where the client returns
77+
// immediately.
78+
BROADCAST_MODE_ASYNC = 3;
79+
}
80+
81+
// BroadcastTxResponse is the response type for the
82+
// Service.BroadcastTx method.
83+
message BroadcastTxResponse {
84+
// tx_response is the queried TxResponses.
85+
cosmos.base.abci.v1beta1.TxResponse tx_response = 1;
86+
}
87+
4888
// SimulateRequest is the request type for the Service.Simulate
4989
// RPC method.
5090
message SimulateRequest {

server/grpc/server_test.go

+73-15
Original file line numberDiff line numberDiff line change
@@ -13,54 +13,64 @@ import (
1313
"google.golang.org/grpc/metadata"
1414
rpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
1515

16+
clienttx "github.com/cosmos/cosmos-sdk/client/tx"
1617
"github.com/cosmos/cosmos-sdk/testutil/network"
1718
"github.com/cosmos/cosmos-sdk/testutil/testdata"
1819
sdk "github.com/cosmos/cosmos-sdk/types"
1920
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
2021
"github.com/cosmos/cosmos-sdk/types/tx"
2122
txtypes "github.com/cosmos/cosmos-sdk/types/tx"
23+
"github.com/cosmos/cosmos-sdk/types/tx/signing"
24+
authclient "github.com/cosmos/cosmos-sdk/x/auth/client"
2225
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
2326
)
2427

2528
type IntegrationTestSuite struct {
2629
suite.Suite
2730

31+
cfg network.Config
2832
network *network.Network
33+
conn *grpc.ClientConn
2934
}
3035

3136
func (s *IntegrationTestSuite) SetupSuite() {
3237
s.T().Log("setting up integration test suite")
3338

34-
s.network = network.New(s.T(), network.DefaultConfig())
39+
s.cfg = network.DefaultConfig()
40+
s.network = network.New(s.T(), s.cfg)
3541
s.Require().NotNil(s.network)
3642

3743
_, err := s.network.WaitForHeight(2)
3844
s.Require().NoError(err)
39-
}
40-
41-
func (s *IntegrationTestSuite) TearDownSuite() {
42-
s.T().Log("tearing down integration test suite")
43-
s.network.Cleanup()
44-
}
4545

46-
func (s *IntegrationTestSuite) TestGRPCServer() {
4746
val0 := s.network.Validators[0]
48-
conn, err := grpc.Dial(
47+
s.conn, err = grpc.Dial(
4948
val0.AppConfig.GRPC.Address,
5049
grpc.WithInsecure(), // Or else we get "no transport security set"
5150
)
5251
s.Require().NoError(err)
53-
defer conn.Close()
52+
}
5453

54+
func (s *IntegrationTestSuite) TearDownSuite() {
55+
s.T().Log("tearing down integration test suite")
56+
s.conn.Close()
57+
s.network.Cleanup()
58+
}
59+
60+
func (s *IntegrationTestSuite) TestGRPCServer_TestService() {
5561
// gRPC query to test service should work
56-
testClient := testdata.NewQueryClient(conn)
62+
testClient := testdata.NewQueryClient(s.conn)
5763
testRes, err := testClient.Echo(context.Background(), &testdata.EchoRequest{Message: "hello"})
5864
s.Require().NoError(err)
5965
s.Require().Equal("hello", testRes.Message)
66+
}
67+
68+
func (s *IntegrationTestSuite) TestGRPCServer_BankBalance() {
69+
val0 := s.network.Validators[0]
6070

6171
// gRPC query to bank service should work
6272
denom := fmt.Sprintf("%stoken", val0.Moniker)
63-
bankClient := banktypes.NewQueryClient(conn)
73+
bankClient := banktypes.NewQueryClient(s.conn)
6474
var header metadata.MD
6575
bankRes, err := bankClient.Balance(
6676
context.Background(),
@@ -83,9 +93,11 @@ func (s *IntegrationTestSuite) TestGRPCServer() {
8393
)
8494
blockHeight = header.Get(grpctypes.GRPCBlockHeightHeader)
8595
s.Require().Equal([]string{"1"}, blockHeight)
96+
}
8697

98+
func (s *IntegrationTestSuite) TestGRPCServer_Reflection() {
8799
// Test server reflection
88-
reflectClient := rpb.NewServerReflectionClient(conn)
100+
reflectClient := rpb.NewServerReflectionClient(s.conn)
89101
stream, err := reflectClient.ServerReflectionInfo(context.Background(), grpc.WaitForReady(true))
90102
s.Require().NoError(err)
91103
s.Require().NoError(stream.Send(&rpb.ServerReflectionRequest{
@@ -100,11 +112,13 @@ func (s *IntegrationTestSuite) TestGRPCServer() {
100112
}
101113
// Make sure the following services are present
102114
s.Require().True(servicesMap["cosmos.bank.v1beta1.Query"])
115+
}
103116

117+
func (s *IntegrationTestSuite) TestGRPCServer_GetTxsEvent() {
104118
// Query the tx via gRPC without pagination. This used to panic, see
105119
// https://github.com/cosmos/cosmos-sdk/issues/8038.
106-
txServiceClient := txtypes.NewServiceClient(conn)
107-
_, err = txServiceClient.GetTxsEvent(
120+
txServiceClient := txtypes.NewServiceClient(s.conn)
121+
_, err := txServiceClient.GetTxsEvent(
108122
context.Background(),
109123
&tx.GetTxsEventRequest{
110124
Events: []string{"message.action=send"},
@@ -115,6 +129,50 @@ func (s *IntegrationTestSuite) TestGRPCServer() {
115129
s.Require().NoError(err)
116130
}
117131

132+
func (s *IntegrationTestSuite) TestGRPCServer_BroadcastTx() {
133+
val0 := s.network.Validators[0]
134+
135+
// prepare txBuilder with msg
136+
txBuilder := val0.ClientCtx.TxConfig.NewTxBuilder()
137+
feeAmount := sdk.Coins{sdk.NewInt64Coin(s.cfg.BondDenom, 10)}
138+
gasLimit := testdata.NewTestGasLimit()
139+
s.Require().NoError(
140+
txBuilder.SetMsgs(&banktypes.MsgSend{
141+
FromAddress: val0.Address.String(),
142+
ToAddress: val0.Address.String(),
143+
Amount: sdk.Coins{sdk.NewInt64Coin(s.cfg.BondDenom, 10)},
144+
}),
145+
)
146+
txBuilder.SetFeeAmount(feeAmount)
147+
txBuilder.SetGasLimit(gasLimit)
148+
149+
// setup txFactory
150+
txFactory := clienttx.Factory{}.
151+
WithChainID(val0.ClientCtx.ChainID).
152+
WithKeybase(val0.ClientCtx.Keyring).
153+
WithTxConfig(val0.ClientCtx.TxConfig).
154+
WithSignMode(signing.SignMode_SIGN_MODE_DIRECT)
155+
156+
// Sign Tx.
157+
err := authclient.SignTx(txFactory, val0.ClientCtx, val0.Moniker, txBuilder, false)
158+
s.Require().NoError(err)
159+
160+
txBytes, err := val0.ClientCtx.TxConfig.TxEncoder()(txBuilder.GetTx())
161+
s.Require().NoError(err)
162+
163+
// Broadcast the tx via gRPC.
164+
queryClient := tx.NewServiceClient(s.conn)
165+
grpcRes, err := queryClient.BroadcastTx(
166+
context.Background(),
167+
&tx.BroadcastTxRequest{
168+
Mode: tx.BroadcastMode_BROADCAST_MODE_SYNC,
169+
TxBytes: txBytes,
170+
},
171+
)
172+
s.Require().NoError(err)
173+
s.Require().Equal(uint32(0), grpcRes.TxResponse.Code)
174+
}
175+
118176
// Test and enforce that we upfront reject any connections to baseapp containing
119177
// invalid initial x-cosmos-block-height that aren't positive and in the range [0, max(int64)]
120178
// See issue https://github.com/cosmos/cosmos-sdk/issues/7662.

0 commit comments

Comments
 (0)