Skip to content

Commit 499bfdc

Browse files
rootulpclaude
andcommitted
fix(post_dispatch): register types.proto before tx.proto to fix Amino JSON signing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5d8016b commit 499bfdc

File tree

2 files changed

+119
-0
lines changed

2 files changed

+119
-0
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package types_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1"
10+
"cosmossdk.io/math"
11+
"cosmossdk.io/x/tx/signing"
12+
"cosmossdk.io/x/tx/signing/aminojson"
13+
14+
"github.com/bcp-innovations/hyperlane-cosmos/util"
15+
"github.com/bcp-innovations/hyperlane-cosmos/x/core/02_post_dispatch/types"
16+
17+
gogoproto "github.com/cosmos/gogoproto/proto"
18+
"google.golang.org/protobuf/proto"
19+
"google.golang.org/protobuf/types/known/anypb"
20+
)
21+
22+
// TestAminoJSONSigningMsgSetDestinationGasConfig reproduces the bug where Amino
23+
// JSON signing fails for MsgSetDestinationGasConfig because the nested
24+
// DestinationGasConfig message's field descriptors are not resolvable via
25+
// gogoproto.HybridResolver.
26+
//
27+
// See: https://github.com/bcp-innovations/hyperlane-cosmos/issues/156
28+
func TestAminoJSONSigningMsgSetDestinationGasConfig(t *testing.T) {
29+
// 1. Construct a MsgSetDestinationGasConfig with a populated DestinationGasConfig
30+
igpId := util.CreateMockHexAddress("igp", 1)
31+
32+
msg := &types.MsgSetDestinationGasConfig{
33+
Owner: "cosmos1testowner",
34+
IgpId: igpId,
35+
DestinationGasConfig: &types.DestinationGasConfig{
36+
RemoteDomain: 42,
37+
GasOracle: &types.GasOracle{
38+
TokenExchangeRate: math.NewInt(1000000000),
39+
GasPrice: math.NewInt(100),
40+
},
41+
GasOverhead: math.NewInt(50000),
42+
},
43+
}
44+
45+
// 2. Marshal with gogo proto
46+
msgBytes, err := gogoproto.Marshal(msg)
47+
require.NoError(t, err)
48+
49+
// 3. Wrap as an anypb.Any
50+
anyMsg := &anypb.Any{
51+
TypeUrl: "/" + gogoproto.MessageName(msg),
52+
Value: msgBytes,
53+
}
54+
55+
// 4. Build a TxBody and marshal to bodyBytes
56+
body := &txv1beta1.TxBody{
57+
Messages: []*anypb.Any{anyMsg},
58+
}
59+
bodyBytes, err := proto.Marshal(body)
60+
require.NoError(t, err)
61+
62+
// 5. Build a minimal AuthInfo with a Fee and marshal to authInfoBytes
63+
authInfo := &txv1beta1.AuthInfo{
64+
Fee: &txv1beta1.Fee{
65+
GasLimit: 200000,
66+
},
67+
}
68+
authInfoBytes, err := proto.Marshal(authInfo)
69+
require.NoError(t, err)
70+
71+
// 6. Create an amino JSON sign mode handler with default options
72+
// (uses gogoproto.HybridResolver, same as the real app)
73+
handler := aminojson.NewSignModeHandler(aminojson.SignModeHandlerOptions{})
74+
75+
signerData := signing.SignerData{
76+
Address: "cosmos1testowner",
77+
ChainID: "test-chain",
78+
AccountNumber: 1,
79+
Sequence: 0,
80+
}
81+
82+
txData := signing.TxData{
83+
Body: body,
84+
AuthInfo: authInfo,
85+
BodyBytes: bodyBytes,
86+
AuthInfoBytes: authInfoBytes,
87+
}
88+
89+
// 7. Call GetSignBytes — this is where the bug manifests.
90+
// The handler internally calls RejectUnknownFields which descends into the
91+
// nested DestinationGasConfig message and fails because its field descriptors
92+
// are not properly registered in the google.golang.org/protobuf registry.
93+
signBytes, err := handler.GetSignBytes(context.Background(), signerData, txData)
94+
require.NoError(t, err, "GetSignBytes should succeed for MsgSetDestinationGasConfig")
95+
require.NotEmpty(t, signBytes)
96+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package types
2+
3+
import proto "github.com/cosmos/gogoproto/proto"
4+
5+
// This file ensures types.proto is registered before tx.proto.
6+
//
7+
// Go runs init() functions in alphabetical file order. Since "tx" sorts before
8+
// "types", tx.pb.go's init registers tx.proto first. tx.proto references the
9+
// DestinationGasConfig message from types.proto, but types.proto isn't in the
10+
// registry yet, so DestinationGasConfig becomes a placeholder with 0 fields.
11+
// This breaks Amino JSON signing because RejectUnknownFields sees the 0-field
12+
// placeholder and rejects all DestinationGasConfig fields as unknown.
13+
//
14+
// This file ("proto_init.go") sorts before "tx.pb.go" ('p' < 't'), so its
15+
// init() runs first and registers types.proto. When tx.pb.go's init() runs
16+
// next, DestinationGasConfig resolves correctly from the registry.
17+
//
18+
// fileDescriptor_d8f5bab7d9705187 is a package-level []byte var in types.pb.go.
19+
// Go initializes all package-level variables before any init() functions, so it
20+
// is available here.
21+
func init() {
22+
proto.RegisterFile("hyperlane/core/post_dispatch/v1/types.proto", fileDescriptor_d8f5bab7d9705187)
23+
}

0 commit comments

Comments
 (0)