-
-
Save 3docSec/2ce73d7321dd957a0dc8ee5c379cbc45 to your computer and use it in GitHub Desktop.
Amino JSON test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
POC_ROOT=$(pwd) | |
# start from a clean env | |
rm -rf 2024-05-canto cosmos-sdk | |
# clone the scope code + the canto SDK | |
git clone git@github.com:code-423n4/2024-05-canto.git | |
git clone git@github.com:cosmos/cosmos-sdk.git | |
# hack the cosmos SDK tests to be importable | |
pushd cosmos-sdk | |
git checkout v0.50.6 | |
mv tests/integration/tx/internal tests/integration/tx/intern | |
popd | |
# modify the canto-main go.mod to point to the local checkout | |
pushd 2024-05-canto/canto-main | |
mv go.mod go.mod.old | |
sed '7i\ | |
replace github.com/cosmos/cosmos-sdk/tests => ../../cosmos-sdk/tests | |
' go.mod.old > go.mod | |
# prepare the test | |
go get github.com/cosmos/cosmos-sdk/tests@v0.50.6 | |
go get ./... | |
cat <<EOT >> app/amino_test.go | |
package app | |
import ( | |
"context" | |
"fmt" | |
csrapi "github.com/Canto-Network/Canto/v7/api/canto/csr/v1" | |
erc20api "github.com/Canto-Network/Canto/v7/api/canto/erc20/v1" | |
inflationapi "github.com/Canto-Network/Canto/v7/api/canto/inflation/v1" | |
"github.com/Canto-Network/Canto/v7/x/coinswap" | |
coinswaptypes "github.com/Canto-Network/Canto/v7/x/coinswap/types" | |
"github.com/Canto-Network/Canto/v7/x/csr" | |
csrtypes "github.com/Canto-Network/Canto/v7/x/csr/types" | |
"github.com/Canto-Network/Canto/v7/x/epochs" | |
"github.com/Canto-Network/Canto/v7/x/erc20" | |
erc20types "github.com/Canto-Network/Canto/v7/x/erc20/types" | |
"github.com/Canto-Network/Canto/v7/x/govshuttle" | |
govshuttletypes "github.com/Canto-Network/Canto/v7/x/govshuttle/types" | |
"github.com/Canto-Network/Canto/v7/x/inflation" | |
inflationtypes "github.com/Canto-Network/Canto/v7/x/inflation/types" | |
"github.com/Canto-Network/Canto/v7/x/onboarding" | |
onboardingtypes "github.com/Canto-Network/Canto/v7/x/onboarding/types" | |
"github.com/cosmos/cosmos-proto/rapidproto" | |
"github.com/stretchr/testify/require" | |
"google.golang.org/protobuf/proto" | |
"google.golang.org/protobuf/reflect/protoregistry" | |
"google.golang.org/protobuf/types/known/anypb" | |
"pgregory.net/rapid" | |
"testing" | |
authapi "cosmossdk.io/api/cosmos/auth/v1beta1" | |
v1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" | |
msgv1 "cosmossdk.io/api/cosmos/msg/v1" | |
txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1" | |
"cosmossdk.io/x/tx/signing/aminojson" | |
signing_testutil "cosmossdk.io/x/tx/signing/testutil" | |
coinswapapi "github.com/Canto-Network/Canto/v7/api/canto/coinswap/v1" | |
govshuttleapi "github.com/Canto-Network/Canto/v7/api/canto/govshuttle/v1" | |
onboardingapi "github.com/Canto-Network/Canto/v7/api/canto/onboarding/v1" | |
"github.com/cosmos/cosmos-sdk/tests/integration/rapidgen" | |
"github.com/cosmos/cosmos-sdk/testutil/testdata" | |
"github.com/cosmos/cosmos-sdk/types" | |
"github.com/cosmos/cosmos-sdk/types/bech32" | |
"github.com/cosmos/cosmos-sdk/types/module/testutil" | |
signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" | |
"github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" | |
"github.com/cosmos/cosmos-sdk/x/auth/signing" | |
"github.com/cosmos/cosmos-sdk/x/auth/tx" | |
) | |
// TestAminoJSON_Equivalence tests that x/tx/Encoder encoding is equivalent to the legacy Encoder encoding. | |
// A custom generator is used to generate random messages that are then encoded using both encoders. The custom | |
// generator only supports proto.Message (which implement the protoreflect API) so in order to test legacy gogo types | |
// we end up with a workflow as follows: | |
// | |
// 1. Generate a random protobuf proto.Message using the custom generator | |
// 2. Marshal the proto.Message to protobuf binary bytes | |
// 3. Unmarshal the protobuf bytes to a gogoproto.Message | |
// 4. Marshal the gogoproto.Message to amino JSON bytes | |
// 5. Marshal the proto.Message to amino JSON bytes | |
// 6. Compare the amino JSON bytes from steps 4 and 5 | |
// | |
// In order for step 3 to work certain restrictions on the data generated in step 1 must be enforced and are described | |
// by the mutation of genOpts passed to the generator. | |
func TestAminoJSON_Equivalence(t *testing.T) { | |
encCfg := testutil.MakeTestEncodingConfig( | |
coinswap.AppModuleBasic{}, | |
csr.AppModuleBasic{}, | |
epochs.AppModuleBasic{}, | |
erc20.AppModuleBasic{}, | |
govshuttle.AppModuleBasic{}, | |
inflation.AppModuleBasic{}, | |
onboarding.AppModuleBasic{}) | |
legacytx.RegressionTestingAminoCodec = encCfg.Amino | |
aj := aminojson.NewEncoder(aminojson.EncoderOptions{DoNotSortFields: true}) | |
GenOpts := rapidproto.GeneratorOptions{ | |
Resolver: protoregistry.GlobalTypes, | |
FieldMaps: []rapidproto.FieldMapper{rapidgen.GeneratorFieldMapper}, | |
} | |
testedMsgs := []rapidgen.GeneratedType{ | |
// coinswap | |
rapidgen.GenType(&coinswaptypes.MsgAddLiquidity{}, &coinswapapi.MsgAddLiquidity{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&coinswaptypes.MsgRemoveLiquidity{}, &coinswapapi.MsgRemoveLiquidity{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&coinswaptypes.MsgSwapOrder{}, &coinswapapi.MsgSwapOrder{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&coinswaptypes.MsgUpdateParams{}, &coinswapapi.MsgUpdateParams{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&coinswaptypes.Params{}, &coinswapapi.Params{}, GenOpts.WithDisallowNil()), | |
// csr | |
rapidgen.GenType(&csrtypes.MsgUpdateParams{}, &csrapi.MsgUpdateParams{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&csrtypes.Params{}, &csrapi.Params{}, GenOpts.WithDisallowNil()), | |
// erc20 | |
rapidgen.GenType(&erc20types.MsgRegisterCoin{}, &erc20api.MsgRegisterCoin{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&erc20types.MsgRegisterERC20{}, &erc20api.MsgRegisterERC20{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&erc20types.MsgConvertERC20{}, &erc20api.MsgConvertERC20{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&erc20types.MsgConvertCoin{}, &erc20api.MsgConvertCoin{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&erc20types.MsgUpdateParams{}, &erc20api.MsgUpdateParams{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&erc20types.Params{}, &erc20api.Params{}, GenOpts.WithDisallowNil()), | |
// govshuttle | |
rapidgen.GenType(&govshuttletypes.LendingMarketProposal{}, &govshuttleapi.LendingMarketProposal{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&govshuttletypes.TreasuryProposal{}, &govshuttleapi.TreasuryProposal{}, GenOpts.WithDisallowNil()), | |
// inflation | |
rapidgen.GenType(&inflationtypes.MsgUpdateParams{}, &inflationapi.MsgUpdateParams{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&inflationtypes.Params{}, &inflationapi.Params{}, GenOpts.WithDisallowNil()), | |
// onboarding | |
rapidgen.GenType(&onboardingtypes.MsgUpdateParams{}, &onboardingapi.MsgUpdateParams{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&onboardingtypes.Params{}, &onboardingapi.Params{}, GenOpts.WithDisallowNil()), | |
} | |
for _, tt := range testedMsgs { | |
desc := tt.Pulsar.ProtoReflect().Descriptor() | |
name := string(desc.FullName()) | |
t.Run(name, func(t *testing.T) { | |
gen := rapidproto.MessageGenerator(tt.Pulsar, tt.Opts) | |
fmt.Printf("testing %s\n", tt.Pulsar.ProtoReflect().Descriptor().FullName()) | |
rapid.Check(t, func(t *rapid.T) { | |
// uncomment to debug; catch a panic and inspect application state | |
// defer func() { | |
// if r := recover(); r != nil { | |
// //fmt.Printf("Panic: %+v\n", r) | |
// t.FailNow() | |
// } | |
// }() | |
msg := gen.Draw(t, "msg") | |
postFixPulsarMessage(msg) | |
gogo := tt.Gogo | |
sanity := tt.Pulsar | |
protoBz, err := proto.Marshal(msg) | |
require.NoError(t, err) | |
err = proto.Unmarshal(protoBz, sanity) | |
require.NoError(t, err) | |
err = encCfg.Codec.Unmarshal(protoBz, gogo) | |
require.NoError(t, err) | |
legacyAminoJSON, err := encCfg.Amino.MarshalJSON(gogo) | |
require.NoError(t, err) | |
aminoJSON, err := aj.Marshal(msg) | |
require.NoError(t, err) | |
require.Equal(t, string(legacyAminoJSON), string(aminoJSON)) | |
// test amino json signer handler equivalence | |
if !proto.HasExtension(desc.Options(), msgv1.E_Signer) { | |
// not signable | |
return | |
} | |
handlerOptions := signing_testutil.HandlerArgumentOptions{ | |
ChainID: "test-chain", | |
Memo: "sometestmemo", | |
Msg: tt.Pulsar, | |
AccNum: 1, | |
AccSeq: 2, | |
SignerAddress: "signerAddress", | |
Fee: &txv1beta1.Fee{ | |
Amount: []*v1beta1.Coin{{Denom: "uatom", Amount: "1000"}}, | |
}, | |
} | |
signerData, txData, err := signing_testutil.MakeHandlerArguments(handlerOptions) | |
require.NoError(t, err) | |
handler := aminojson.NewSignModeHandler(aminojson.SignModeHandlerOptions{}) | |
signBz, err := handler.GetSignBytes(context.Background(), signerData, txData) | |
require.NoError(t, err) | |
legacyHandler := tx.NewSignModeLegacyAminoJSONHandler() | |
txBuilder := encCfg.TxConfig.NewTxBuilder() | |
require.NoError(t, txBuilder.SetMsgs([]types.Msg{tt.Gogo}...)) | |
txBuilder.SetMemo(handlerOptions.Memo) | |
txBuilder.SetFeeAmount(types.Coins{types.NewInt64Coin("uatom", 1000)}) | |
theTx := txBuilder.GetTx() | |
legacySigningData := signing.SignerData{ | |
ChainID: handlerOptions.ChainID, | |
Address: handlerOptions.SignerAddress, | |
AccountNumber: handlerOptions.AccNum, | |
Sequence: handlerOptions.AccSeq, | |
} | |
legacySignBz, err := legacyHandler.GetSignBytes(signingtypes.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, | |
legacySigningData, theTx) | |
require.NoError(t, err) | |
require.Equal(t, string(legacySignBz), string(signBz)) | |
}) | |
}) | |
} | |
} | |
func newAny(t *testing.T, msg proto.Message) *anypb.Any { | |
bz, err := proto.Marshal(msg) | |
require.NoError(t, err) | |
typeName := fmt.Sprintf("/%s", msg.ProtoReflect().Descriptor().FullName()) | |
return &anypb.Any{ | |
TypeUrl: typeName, | |
Value: bz, | |
} | |
} | |
func postFixPulsarMessage(msg proto.Message) { | |
if m, ok := msg.(*authapi.ModuleAccount); ok { | |
if m.BaseAccount == nil { | |
m.BaseAccount = &authapi.BaseAccount{} | |
} | |
_, _, bz := testdata.KeyTestPubAddr() | |
// always set address to a valid bech32 address | |
text, _ := bech32.ConvertAndEncode("cosmos", bz) | |
m.BaseAccount.Address = text | |
// see negative test | |
if len(m.Permissions) == 0 { | |
m.Permissions = nil | |
} | |
} | |
if m, ok := msg.(*coinswapapi.MsgUpdateParams); ok { | |
m.Params.MaxStandardCoinPerPool = "10" | |
} | |
if m, ok := msg.(*coinswapapi.Params); ok { | |
m.MaxStandardCoinPerPool = "10" | |
} | |
} | |
EOT | |
# run the test | |
go test app/amino_test.go || true | |
# modify the ethermint-main go.mod to point to the local checkout | |
popd | |
pushd 2024-05-canto/ethermint-main | |
mv go.mod go.mod.old | |
sed '7i\ | |
replace github.com/cosmos/cosmos-sdk/tests => ../../cosmos-sdk/tests | |
' go.mod.old > go.mod | |
# prepare the test | |
go get github.com/cosmos/cosmos-sdk/tests@v0.50.6 | |
go get ./... | |
cat <<EOT >> app/amino_test.go | |
package app | |
import ( | |
"context" | |
"fmt" | |
evmv1 "github.com/evmos/ethermint/api/ethermint/evm/v1" | |
feemarketv1 "github.com/evmos/ethermint/api/ethermint/feemarket/v1" | |
"github.com/evmos/ethermint/x/evm" | |
evmtypes "github.com/evmos/ethermint/x/evm/types" | |
"github.com/evmos/ethermint/x/feemarket" | |
feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" | |
"github.com/cosmos/cosmos-proto/rapidproto" | |
"github.com/stretchr/testify/require" | |
"google.golang.org/protobuf/proto" | |
"google.golang.org/protobuf/reflect/protoregistry" | |
"google.golang.org/protobuf/types/known/anypb" | |
"pgregory.net/rapid" | |
"testing" | |
authapi "cosmossdk.io/api/cosmos/auth/v1beta1" | |
v1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" | |
msgv1 "cosmossdk.io/api/cosmos/msg/v1" | |
txv1beta1 "cosmossdk.io/api/cosmos/tx/v1beta1" | |
"cosmossdk.io/x/tx/signing/aminojson" | |
signing_testutil "cosmossdk.io/x/tx/signing/testutil" | |
"github.com/cosmos/cosmos-sdk/tests/integration/rapidgen" | |
"github.com/cosmos/cosmos-sdk/testutil/testdata" | |
"github.com/cosmos/cosmos-sdk/types" | |
"github.com/cosmos/cosmos-sdk/types/bech32" | |
"github.com/cosmos/cosmos-sdk/types/module/testutil" | |
signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" | |
"github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" | |
"github.com/cosmos/cosmos-sdk/x/auth/signing" | |
"github.com/cosmos/cosmos-sdk/x/auth/tx" | |
) | |
// TestAminoJSON_Equivalence tests that x/tx/Encoder encoding is equivalent to the legacy Encoder encoding. | |
// A custom generator is used to generate random messages that are then encoded using both encoders. The custom | |
// generator only supports proto.Message (which implement the protoreflect API) so in order to test legacy gogo types | |
// we end up with a workflow as follows: | |
// | |
// 1. Generate a random protobuf proto.Message using the custom generator | |
// 2. Marshal the proto.Message to protobuf binary bytes | |
// 3. Unmarshal the protobuf bytes to a gogoproto.Message | |
// 4. Marshal the gogoproto.Message to amino JSON bytes | |
// 5. Marshal the proto.Message to amino JSON bytes | |
// 6. Compare the amino JSON bytes from steps 4 and 5 | |
// | |
// In order for step 3 to work certain restrictions on the data generated in step 1 must be enforced and are described | |
// by the mutation of genOpts passed to the generator. | |
func TestAminoJSON_Equivalence(t *testing.T) { | |
encCfg := testutil.MakeTestEncodingConfig( | |
evm.AppModuleBasic{}, | |
feemarket.AppModuleBasic{}) | |
legacytx.RegressionTestingAminoCodec = encCfg.Amino | |
aj := aminojson.NewEncoder(aminojson.EncoderOptions{DoNotSortFields: true}) | |
GenOpts := rapidproto.GeneratorOptions{ | |
Resolver: protoregistry.GlobalTypes, | |
FieldMaps: []rapidproto.FieldMapper{rapidgen.GeneratorFieldMapper}, | |
} | |
testedMsgs := []rapidgen.GeneratedType{ | |
// evm | |
rapidgen.GenType(&evmtypes.MsgEthereumTx{}, &evmv1.MsgEthereumTx{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&evmtypes.MsgUpdateParams{}, &evmv1.MsgUpdateParams{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&evmtypes.Params{}, &evmv1.Params{}, GenOpts.WithDisallowNil()), | |
// feemarket | |
rapidgen.GenType(&feemarkettypes.MsgUpdateParams{}, &feemarketv1.MsgUpdateParams{}, GenOpts.WithDisallowNil()), | |
rapidgen.GenType(&feemarkettypes.Params{}, &feemarketv1.Params{}, GenOpts.WithDisallowNil()), | |
} | |
for _, tt := range testedMsgs { | |
desc := tt.Pulsar.ProtoReflect().Descriptor() | |
name := string(desc.FullName()) | |
t.Run(name, func(t *testing.T) { | |
gen := rapidproto.MessageGenerator(tt.Pulsar, tt.Opts) | |
fmt.Printf("testing %s\n", tt.Pulsar.ProtoReflect().Descriptor().FullName()) | |
rapid.Check(t, func(t *rapid.T) { | |
// uncomment to debug; catch a panic and inspect application state | |
// defer func() { | |
// if r := recover(); r != nil { | |
// //fmt.Printf("Panic: %+v\n", r) | |
// t.FailNow() | |
// } | |
// }() | |
msg := gen.Draw(t, "msg") | |
postFixPulsarMessage(msg) | |
gogo := tt.Gogo | |
sanity := tt.Pulsar | |
protoBz, err := proto.Marshal(msg) | |
require.NoError(t, err) | |
err = proto.Unmarshal(protoBz, sanity) | |
require.NoError(t, err) | |
err = encCfg.Codec.Unmarshal(protoBz, gogo) | |
require.NoError(t, err) | |
legacyAminoJSON, err := encCfg.Amino.MarshalJSON(gogo) | |
require.NoError(t, err) | |
aminoJSON, err := aj.Marshal(msg) | |
require.NoError(t, err) | |
require.Equal(t, string(legacyAminoJSON), string(aminoJSON)) | |
// test amino json signer handler equivalence | |
if !proto.HasExtension(desc.Options(), msgv1.E_Signer) { | |
// not signable | |
return | |
} | |
handlerOptions := signing_testutil.HandlerArgumentOptions{ | |
ChainID: "test-chain", | |
Memo: "sometestmemo", | |
Msg: tt.Pulsar, | |
AccNum: 1, | |
AccSeq: 2, | |
SignerAddress: "signerAddress", | |
Fee: &txv1beta1.Fee{ | |
Amount: []*v1beta1.Coin{{Denom: "uatom", Amount: "1000"}}, | |
}, | |
} | |
signerData, txData, err := signing_testutil.MakeHandlerArguments(handlerOptions) | |
require.NoError(t, err) | |
handler := aminojson.NewSignModeHandler(aminojson.SignModeHandlerOptions{}) | |
signBz, err := handler.GetSignBytes(context.Background(), signerData, txData) | |
require.NoError(t, err) | |
legacyHandler := tx.NewSignModeLegacyAminoJSONHandler() | |
txBuilder := encCfg.TxConfig.NewTxBuilder() | |
require.NoError(t, txBuilder.SetMsgs([]types.Msg{tt.Gogo}...)) | |
txBuilder.SetMemo(handlerOptions.Memo) | |
txBuilder.SetFeeAmount(types.Coins{types.NewInt64Coin("uatom", 1000)}) | |
theTx := txBuilder.GetTx() | |
legacySigningData := signing.SignerData{ | |
ChainID: handlerOptions.ChainID, | |
Address: handlerOptions.SignerAddress, | |
AccountNumber: handlerOptions.AccNum, | |
Sequence: handlerOptions.AccSeq, | |
} | |
legacySignBz, err := legacyHandler.GetSignBytes(signingtypes.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, | |
legacySigningData, theTx) | |
require.NoError(t, err) | |
require.Equal(t, string(legacySignBz), string(signBz)) | |
}) | |
}) | |
} | |
} | |
func newAny(t *testing.T, msg proto.Message) *anypb.Any { | |
bz, err := proto.Marshal(msg) | |
require.NoError(t, err) | |
typeName := fmt.Sprintf("/%s", msg.ProtoReflect().Descriptor().FullName()) | |
return &anypb.Any{ | |
TypeUrl: typeName, | |
Value: bz, | |
} | |
} | |
func postFixPulsarMessage(msg proto.Message) { | |
if m, ok := msg.(*authapi.ModuleAccount); ok { | |
if m.BaseAccount == nil { | |
m.BaseAccount = &authapi.BaseAccount{} | |
} | |
_, _, bz := testdata.KeyTestPubAddr() | |
// always set address to a valid bech32 address | |
text, _ := bech32.ConvertAndEncode("cosmos", bz) | |
m.BaseAccount.Address = text | |
// see negative test | |
if len(m.Permissions) == 0 { | |
m.Permissions = nil | |
} | |
} | |
if m, ok := msg.(*evmv1.MsgUpdateParams); ok { | |
m.Params.ChainConfig.CancunBlock = "10" | |
} | |
if m, ok := msg.(*evmv1.Params); ok { | |
m.ChainConfig.CancunBlock = "10" | |
} | |
if m, ok := msg.(*feemarketv1.MsgUpdateParams); ok { | |
m.Params.BaseFee = "10" | |
} | |
if m, ok := msg.(*feemarketv1.Params); ok { | |
m.BaseFee = "10" | |
} | |
} | |
EOT | |
# run the test | |
go test app/amino_test.go || true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment