diff --git a/UPGRADE_GUIDE.md b/UPGRADE_GUIDE.md index b3c522346261..c1244598952f 100644 --- a/UPGRADE_GUIDE.md +++ b/UPGRADE_GUIDE.md @@ -406,36 +406,51 @@ Lastly, add an entry for epochs in the ModuleConfig: ## Enable Unordered Transactions **OPTIONAL** -To enable unordered transaction support on an application, the ante handler options must be updated. +To enable unordered transaction support on an application, the `x/auth` keeper must be supplied with the `WithUnorderedTransactions` option. + +```go + app.AccountKeeper = authkeeper.NewAccountKeeper( + appCodec, + runtime.NewKVStoreService(keys[authtypes.StoreKey]), + authtypes.ProtoBaseAccount, + maccPerms, + authcodec.NewBech32Codec(sdk.Bech32MainPrefix), + sdk.Bech32MainPrefix, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + authkeeper.WithUnorderedTransactions(true), // new option! + ) +``` + +If using dependency injection, update the auth module config. + +```go + { + Name: authtypes.ModuleName, + Config: appconfig.WrapAny(&authmodulev1.Module{ + Bech32Prefix: "cosmos", + ModuleAccountPermissions: moduleAccPerms, + EnableUnorderedTransactions: true, // remove this line if you do not want unordered transactions. + }), + }, +``` + +By default, unordered transactions use a transaction timeout duration of 10 minutes and a default gas charge of 2240 gas units. +To modify these default values, pass in the corresponding options to the new `SigVerifyOptions` field in `x/auth's` `ante.HandlerOptions`. ```go options := ante.HandlerOptions{ - // ... - UnorderedNonceManager: app.AccountKeeper, - // The following options are set by default. - // If you do not want to change these, you may remove the UnorderedTxOptions field entirely. - UnorderedTxOptions: []ante.UnorderedTxDecoratorOptions{ - ante.WithUnorderedTxGasCost(2240), - ante.WithTimeoutDuration(10 * time.Minute), + SigVerifyOptions: []ante.SigVerificationDecoratorOption{ + // change below as needed. + ante.WithUnorderedTxGasCost(ante.DefaultUnorderedTxGasCost), + ante.WithMaxUnorderedTxTimeoutDuration(ante.DefaultMaxTimoutDuration), }, } +``` +```go anteDecorators := []sdk.AnteDecorator{ - ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first - circuitante.NewCircuitBreakerDecorator(options.CircuitKeeper), - ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker), - ante.NewValidateBasicDecorator(), - ante.NewTxTimeoutHeightDecorator(), - ante.NewValidateMemoDecorator(options.AccountKeeper), - ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), - ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker), - ante.NewSetPubKeyDecorator(options.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators - ante.NewValidateSigCountDecorator(options.AccountKeeper), - ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer), - ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), - ante.NewIncrementSequenceDecorator(options.AccountKeeper), - // NEW !! NEW !! NEW !! - ante.NewUnorderedTxDecorator(options.UnorderedNonceManager, options.UnorderedTxOptions...) + // ... other decorators ... + ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler, options.SigVerifyOptions...), // supply new options } ``` diff --git a/UPGRADING.md b/UPGRADING.md index 95ca63204f13..6f697058b453 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -27,40 +27,51 @@ unique to the account; however, the difference may be as small as a nanosecond. #### Enabling Unordered Transactions -To enable unordered transactions, set the new `UnorderedNonceManager` field in the `x/auth` `ante.HandlerOptions`. +To enable unordered transactions, supply the `WithUnorderedTransactions` option to the `x/auth` keeper: -By default, unordered transactions use a transaction timeout duration of 10 minutes and a default gas charge of 2240 gas. -To modify these default values, pass in the corresponding options to the new `UnorderedTxOptions` field in `x/auth's` `ante.HandlerOptions`. +```go + app.AccountKeeper = authkeeper.NewAccountKeeper( + appCodec, + runtime.NewKVStoreService(keys[authtypes.StoreKey]), + authtypes.ProtoBaseAccount, + maccPerms, + authcodec.NewBech32Codec(sdk.Bech32MainPrefix), + sdk.Bech32MainPrefix, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + authkeeper.WithUnorderedTransactions(true), // new option! + ) +``` + +If using dependency injection, update the auth module config. + +```go + { + Name: authtypes.ModuleName, + Config: appconfig.WrapAny(&authmodulev1.Module{ + Bech32Prefix: "cosmos", + ModuleAccountPermissions: moduleAccPerms, + EnableUnorderedTransactions: true, // remove this line if you do not want unordered transactions. + }), + }, +``` + +By default, unordered transactions use a transaction timeout duration of 10 minutes and a default gas charge of 2240 gas units. +To modify these default values, pass in the corresponding options to the new `SigVerifyOptions` field in `x/auth's` `ante.HandlerOptions`. ```go options := ante.HandlerOptions{ - UnorderedNonceManager: app.AccountKeeper, - // The following options are set by default. - // If you do not want to change these, you may remove the UnorderedTxOptions field entirely. - UnorderedTxOptions: []ante.UnorderedTxDecoratorOptions{ - ante.WithUnorderedTxGasCost(2240), - ante.WithTimeoutDuration(10 * time.Minute), + SigVerifyOptions: []ante.SigVerificationDecoratorOption{ + // change below as needed. + ante.WithUnorderedTxGasCost(ante.DefaultUnorderedTxGasCost), + ante.WithMaxUnorderedTxTimeoutDuration(ante.DefaultMaxTimoutDuration), }, } ``` ```go anteDecorators := []sdk.AnteDecorator{ - ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first - circuitante.NewCircuitBreakerDecorator(options.CircuitKeeper), - ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker), - ante.NewValidateBasicDecorator(), - ante.NewTxTimeoutHeightDecorator(), - ante.NewValidateMemoDecorator(options.AccountKeeper), - ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), - ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker), - ante.NewSetPubKeyDecorator(options.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators - ante.NewValidateSigCountDecorator(options.AccountKeeper), - ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer), - ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), - ante.NewIncrementSequenceDecorator(options.AccountKeeper), - // NEW !! NEW !! NEW !! - ante.NewUnorderedTxDecorator(options.UnorderedNonceManager, options.UnorderedTxOptions...) + // ... other decorators ... + ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler, options.SigVerifyOptions...), // supply new options } ``` diff --git a/api/cosmos/auth/module/v1/module.pulsar.go b/api/cosmos/auth/module/v1/module.pulsar.go index 2ba0bff52d41..fca43b4d8cbc 100644 --- a/api/cosmos/auth/module/v1/module.pulsar.go +++ b/api/cosmos/auth/module/v1/module.pulsar.go @@ -65,10 +65,11 @@ func (x *_Module_2_list) IsValid() bool { } var ( - md_Module protoreflect.MessageDescriptor - fd_Module_bech32_prefix protoreflect.FieldDescriptor - fd_Module_module_account_permissions protoreflect.FieldDescriptor - fd_Module_authority protoreflect.FieldDescriptor + md_Module protoreflect.MessageDescriptor + fd_Module_bech32_prefix protoreflect.FieldDescriptor + fd_Module_module_account_permissions protoreflect.FieldDescriptor + fd_Module_authority protoreflect.FieldDescriptor + fd_Module_enable_unordered_transactions protoreflect.FieldDescriptor ) func init() { @@ -77,6 +78,7 @@ func init() { fd_Module_bech32_prefix = md_Module.Fields().ByName("bech32_prefix") fd_Module_module_account_permissions = md_Module.Fields().ByName("module_account_permissions") fd_Module_authority = md_Module.Fields().ByName("authority") + fd_Module_enable_unordered_transactions = md_Module.Fields().ByName("enable_unordered_transactions") } var _ protoreflect.Message = (*fastReflection_Module)(nil) @@ -162,6 +164,12 @@ func (x *fastReflection_Module) Range(f func(protoreflect.FieldDescriptor, proto return } } + if x.EnableUnorderedTransactions != false { + value := protoreflect.ValueOfBool(x.EnableUnorderedTransactions) + if !f(fd_Module_enable_unordered_transactions, value) { + return + } + } } // Has reports whether a field is populated. @@ -183,6 +191,8 @@ func (x *fastReflection_Module) Has(fd protoreflect.FieldDescriptor) bool { return len(x.ModuleAccountPermissions) != 0 case "cosmos.auth.module.v1.Module.authority": return x.Authority != "" + case "cosmos.auth.module.v1.Module.enable_unordered_transactions": + return x.EnableUnorderedTransactions != false default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.auth.module.v1.Module")) @@ -205,6 +215,8 @@ func (x *fastReflection_Module) Clear(fd protoreflect.FieldDescriptor) { x.ModuleAccountPermissions = nil case "cosmos.auth.module.v1.Module.authority": x.Authority = "" + case "cosmos.auth.module.v1.Module.enable_unordered_transactions": + x.EnableUnorderedTransactions = false default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.auth.module.v1.Module")) @@ -233,6 +245,9 @@ func (x *fastReflection_Module) Get(descriptor protoreflect.FieldDescriptor) pro case "cosmos.auth.module.v1.Module.authority": value := x.Authority return protoreflect.ValueOfString(value) + case "cosmos.auth.module.v1.Module.enable_unordered_transactions": + value := x.EnableUnorderedTransactions + return protoreflect.ValueOfBool(value) default: if descriptor.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.auth.module.v1.Module")) @@ -261,6 +276,8 @@ func (x *fastReflection_Module) Set(fd protoreflect.FieldDescriptor, value proto x.ModuleAccountPermissions = *clv.list case "cosmos.auth.module.v1.Module.authority": x.Authority = value.Interface().(string) + case "cosmos.auth.module.v1.Module.enable_unordered_transactions": + x.EnableUnorderedTransactions = value.Bool() default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.auth.module.v1.Module")) @@ -291,6 +308,8 @@ func (x *fastReflection_Module) Mutable(fd protoreflect.FieldDescriptor) protore panic(fmt.Errorf("field bech32_prefix of message cosmos.auth.module.v1.Module is not mutable")) case "cosmos.auth.module.v1.Module.authority": panic(fmt.Errorf("field authority of message cosmos.auth.module.v1.Module is not mutable")) + case "cosmos.auth.module.v1.Module.enable_unordered_transactions": + panic(fmt.Errorf("field enable_unordered_transactions of message cosmos.auth.module.v1.Module is not mutable")) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.auth.module.v1.Module")) @@ -311,6 +330,8 @@ func (x *fastReflection_Module) NewField(fd protoreflect.FieldDescriptor) protor return protoreflect.ValueOfList(&_Module_2_list{list: &list}) case "cosmos.auth.module.v1.Module.authority": return protoreflect.ValueOfString("") + case "cosmos.auth.module.v1.Module.enable_unordered_transactions": + return protoreflect.ValueOfBool(false) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.auth.module.v1.Module")) @@ -394,6 +415,9 @@ func (x *fastReflection_Module) ProtoMethods() *protoiface.Methods { if l > 0 { n += 1 + l + runtime.Sov(uint64(l)) } + if x.EnableUnorderedTransactions { + n += 2 + } if x.unknownFields != nil { n += len(x.unknownFields) } @@ -423,6 +447,16 @@ func (x *fastReflection_Module) ProtoMethods() *protoiface.Methods { i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } + if x.EnableUnorderedTransactions { + i-- + if x.EnableUnorderedTransactions { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x20 + } if len(x.Authority) > 0 { i -= len(x.Authority) copy(dAtA[i:], x.Authority) @@ -600,6 +634,26 @@ func (x *fastReflection_Module) ProtoMethods() *protoiface.Methods { } x.Authority = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 4: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field EnableUnorderedTransactions", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + x.EnableUnorderedTransactions = bool(v != 0) default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -1204,6 +1258,10 @@ type Module struct { ModuleAccountPermissions []*ModuleAccountPermission `protobuf:"bytes,2,rep,name=module_account_permissions,json=moduleAccountPermissions,proto3" json:"module_account_permissions,omitempty"` // authority defines the custom module authority. If not set, defaults to the governance module. Authority string `protobuf:"bytes,3,opt,name=authority,proto3" json:"authority,omitempty"` + // enable_unordered_transactions determines whether unordered transactions should be processed or not. + // When true, unordered transactions will be validated and processed. + // When false, unordered transactions will be rejected. + EnableUnorderedTransactions bool `protobuf:"varint,4,opt,name=enable_unordered_transactions,json=enableUnorderedTransactions,proto3" json:"enable_unordered_transactions,omitempty"` } func (x *Module) Reset() { @@ -1247,6 +1305,13 @@ func (x *Module) GetAuthority() string { return "" } +func (x *Module) GetEnableUnorderedTransactions() bool { + if x != nil { + return x.EnableUnorderedTransactions + } + return false +} + // ModuleAccountPermission represents permissions for a module account. type ModuleAccountPermission struct { state protoimpl.MessageState @@ -1302,7 +1367,7 @@ var file_cosmos_auth_module_v1_module_proto_rawDesc = []byte{ 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x15, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x20, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, - 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe6, 0x01, + 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xaa, 0x02, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x62, 0x65, 0x63, 0x68, 0x33, 0x32, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x62, 0x65, 0x63, 0x68, 0x33, 0x32, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x6c, 0x0a, @@ -1314,29 +1379,33 @@ var file_cosmos_auth_module_v1_module_proto_rawDesc = []byte{ 0x6e, 0x52, 0x18, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x3a, 0x2b, 0xba, 0xc0, 0x96, 0xda, 0x01, - 0x25, 0x0a, 0x23, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, - 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2d, 0x73, 0x64, 0x6b, 0x2f, - 0x78, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x22, 0x55, 0x0a, 0x17, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, - 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x70, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0xd0, 0x01, - 0x0a, 0x19, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x2e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0b, 0x4d, 0x6f, 0x64, - 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2f, 0x63, 0x6f, 0x73, 0x6d, - 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x73, - 0x6d, 0x6f, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2f, - 0x76, 0x31, 0x3b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x41, - 0x4d, 0xaa, 0x02, 0x15, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x2e, - 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x15, 0x43, 0x6f, 0x73, 0x6d, - 0x6f, 0x73, 0x5c, 0x41, 0x75, 0x74, 0x68, 0x5c, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5c, 0x56, - 0x31, 0xe2, 0x02, 0x21, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x75, 0x74, 0x68, 0x5c, - 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x18, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, - 0x41, 0x75, 0x74, 0x68, 0x3a, 0x3a, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x3a, 0x3a, 0x56, 0x31, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x1d, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x75, 0x6e, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x1b, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x6e, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x65, + 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x2b, 0xba, + 0xc0, 0x96, 0xda, 0x01, 0x25, 0x0a, 0x23, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2d, + 0x73, 0x64, 0x6b, 0x2f, 0x78, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x22, 0x55, 0x0a, 0x17, 0x4d, 0x6f, + 0x64, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x42, 0xd0, 0x01, 0x0a, 0x19, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x42, + 0x0b, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2f, + 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6d, 0x6f, 0x64, + 0x75, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x76, 0x31, 0xa2, + 0x02, 0x03, 0x43, 0x41, 0x4d, 0xaa, 0x02, 0x15, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, + 0x75, 0x74, 0x68, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x15, + 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x75, 0x74, 0x68, 0x5c, 0x4d, 0x6f, 0x64, 0x75, + 0x6c, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x21, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x41, + 0x75, 0x74, 0x68, 0x5c, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, + 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x18, 0x43, 0x6f, 0x73, 0x6d, + 0x6f, 0x73, 0x3a, 0x3a, 0x41, 0x75, 0x74, 0x68, 0x3a, 0x3a, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, + 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/go.mod b/go.mod index 4585244f5d9b..9fa2b9d5c8c8 100644 --- a/go.mod +++ b/go.mod @@ -174,6 +174,7 @@ require ( // replace ( // // ) +replace cosmossdk.io/api => ./api // Below are the long-lived replace of the Cosmos SDK replace ( diff --git a/go.sum b/go.sum index 7abbe79ca3f9..bd625dbd03a1 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cosmossdk.io/api v0.9.0 h1:QYs9APeSlDNGbsBOBFjp3jXgGd4hnEPnnku3+W3tT4Y= -cosmossdk.io/api v0.9.0/go.mod h1:pLkU/NSqYHWxyN7XftVt8iD7oldKJzqMZgzeiOmT2nk= cosmossdk.io/collections v1.2.0 h1:IesfVG8G/+FYCMVMP01frS/Cw99Omk5vBh3cHbO01Gg= cosmossdk.io/collections v1.2.0/go.mod h1:4NkMoYw6qRA8fnSH/yn1D/MOutr8qyQnwsO50Mz9ItU= cosmossdk.io/core v0.11.3 h1:mei+MVDJOwIjIniaKelE3jPDqShCc/F4LkNNHh+4yfo= diff --git a/proto/cosmos/auth/module/v1/module.proto b/proto/cosmos/auth/module/v1/module.proto index dbe46a157c72..69b4fad6a677 100644 --- a/proto/cosmos/auth/module/v1/module.proto +++ b/proto/cosmos/auth/module/v1/module.proto @@ -18,6 +18,11 @@ message Module { // authority defines the custom module authority. If not set, defaults to the governance module. string authority = 3; + + // enable_unordered_transactions determines whether unordered transactions should be supported or not. + // When true, unordered transactions will be validated and processed. + // When false, unordered transactions will be rejected. + bool enable_unordered_transactions = 4; } // ModuleAccountPermission represents permissions for a module account. diff --git a/simapp/ante.go b/simapp/ante.go index c33e88ef6943..eab46c4e1669 100644 --- a/simapp/ante.go +++ b/simapp/ante.go @@ -43,13 +43,9 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { ante.NewSetPubKeyDecorator(options.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators ante.NewValidateSigCountDecorator(options.AccountKeeper), ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer), - ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), + ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler, options.SigVerifyOptions...), ante.NewIncrementSequenceDecorator(options.AccountKeeper), } - if options.UnorderedNonceManager != nil { - anteDecorators = append(anteDecorators, ante.NewUnorderedTxDecorator(options.UnorderedNonceManager, options.UnorderedTxOptions...)) - } - return sdk.ChainAnteDecorators(anteDecorators...), nil } diff --git a/simapp/app.go b/simapp/app.go index 277b01daf9b0..0bebafc655fb 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -5,12 +5,13 @@ package simapp import ( "encoding/json" "fmt" + "io" + "maps" + abci "github.com/cometbft/cometbft/abci/types" dbm "github.com/cosmos/cosmos-db" "github.com/cosmos/gogoproto/proto" "github.com/spf13/cast" - "io" - "maps" autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1" @@ -306,6 +307,7 @@ func NewSimApp( authcodec.NewBech32Codec(sdk.Bech32MainPrefix), sdk.Bech32MainPrefix, authtypes.NewModuleAddress(govtypes.ModuleName).String(), + authkeeper.WithUnorderedTransactions(true), ) app.BankKeeper = bankkeeper.NewBaseKeeper( @@ -467,7 +469,7 @@ func NewSimApp( app.GovKeeper = *govKeeper.SetHooks( govtypes.NewMultiGovHooks( - // register the governance hooks + // register the governance hooks ), ) @@ -497,7 +499,7 @@ func NewSimApp( app.EpochsKeeper.SetHooks( epochstypes.NewMultiEpochHooks( - // insert epoch hooks receivers here + // insert epoch hooks receivers here ), ) @@ -697,12 +699,16 @@ func (app *SimApp) setAnteHandler(txConfig client.TxConfig) { anteHandler, err := NewAnteHandler( HandlerOptions{ ante.HandlerOptions{ - UnorderedNonceManager: app.AccountKeeper, - AccountKeeper: app.AccountKeeper, - BankKeeper: app.BankKeeper, - SignModeHandler: txConfig.SignModeHandler(), - FeegrantKeeper: app.FeeGrantKeeper, - SigGasConsumer: ante.DefaultSigVerificationGasConsumer, + AccountKeeper: app.AccountKeeper, + BankKeeper: app.BankKeeper, + SignModeHandler: txConfig.SignModeHandler(), + FeegrantKeeper: app.FeeGrantKeeper, + SigGasConsumer: ante.DefaultSigVerificationGasConsumer, + SigVerifyOptions: []ante.SigVerificationDecoratorOption{ + // change below as needed. + ante.WithUnorderedTxGasCost(ante.DefaultUnorderedTxGasCost), + ante.WithMaxUnorderedTxTimeoutDuration(ante.DefaultMaxTimeoutDuration), + }, }, &app.CircuitKeeper, }, diff --git a/simapp/app_config.go b/simapp/app_config.go index 72a87f916cf1..f072296c3156 100644 --- a/simapp/app_config.go +++ b/simapp/app_config.go @@ -197,6 +197,7 @@ var ( // By default modules authority is the governance module. This is configurable with the following: // Authority: "group", // A custom module authority can be set using a module name // Authority: "cosmos1cwwv22j5ca08ggdv9c2uky355k908694z577tv", // or a specific address + EnableUnorderedTransactions: true, }), }, { diff --git a/simapp/app_di.go b/simapp/app_di.go index 70f666dc4f47..a70a43ff2c3d 100644 --- a/simapp/app_di.go +++ b/simapp/app_di.go @@ -139,7 +139,6 @@ func NewSimApp( // with the prefix defined in the auth module configuration. // // func() address.Codec { return <- custom address codec type -> } - // // STAKING // @@ -275,12 +274,11 @@ func (app *SimApp) setAnteHandler(txConfig client.TxConfig) { anteHandler, err := NewAnteHandler( HandlerOptions{ ante.HandlerOptions{ - UnorderedNonceManager: app.AccountKeeper, - AccountKeeper: app.AccountKeeper, - BankKeeper: app.BankKeeper, - SignModeHandler: txConfig.SignModeHandler(), - FeegrantKeeper: app.FeeGrantKeeper, - SigGasConsumer: ante.DefaultSigVerificationGasConsumer, + AccountKeeper: app.AccountKeeper, + BankKeeper: app.BankKeeper, + SignModeHandler: txConfig.SignModeHandler(), + FeegrantKeeper: app.FeeGrantKeeper, + SigGasConsumer: ante.DefaultSigVerificationGasConsumer, }, &app.CircuitKeeper, }, diff --git a/simapp/go.mod b/simapp/go.mod index b574b4f1f44b..6932511268b1 100644 --- a/simapp/go.mod +++ b/simapp/go.mod @@ -227,6 +227,7 @@ require ( // ) replace ( + cosmossdk.io/api => ../api cosmossdk.io/client/v2 => ../client/v2 cosmossdk.io/x/circuit => ../x/circuit ) diff --git a/simapp/go.sum b/simapp/go.sum index 96925f31fad7..bb08a2218775 100644 --- a/simapp/go.sum +++ b/simapp/go.sum @@ -614,8 +614,6 @@ cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoIS cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= -cosmossdk.io/api v0.9.0 h1:QYs9APeSlDNGbsBOBFjp3jXgGd4hnEPnnku3+W3tT4Y= -cosmossdk.io/api v0.9.0/go.mod h1:pLkU/NSqYHWxyN7XftVt8iD7oldKJzqMZgzeiOmT2nk= cosmossdk.io/collections v1.2.0 h1:IesfVG8G/+FYCMVMP01frS/Cw99Omk5vBh3cHbO01Gg= cosmossdk.io/collections v1.2.0/go.mod h1:4NkMoYw6qRA8fnSH/yn1D/MOutr8qyQnwsO50Mz9ItU= cosmossdk.io/core v0.11.3 h1:mei+MVDJOwIjIniaKelE3jPDqShCc/F4LkNNHh+4yfo= diff --git a/tests/go.mod b/tests/go.mod index 079a599a0f67..7dbc84daf0f7 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -221,6 +221,7 @@ require ( // replace ( // // ) +replace cosmossdk.io/api => ../api // Below are the long-lived replace for tests. replace ( diff --git a/tests/go.sum b/tests/go.sum index bb5d3d32e63d..48cafa72e22d 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -614,8 +614,6 @@ cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoIS cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= -cosmossdk.io/api v0.9.0 h1:QYs9APeSlDNGbsBOBFjp3jXgGd4hnEPnnku3+W3tT4Y= -cosmossdk.io/api v0.9.0/go.mod h1:pLkU/NSqYHWxyN7XftVt8iD7oldKJzqMZgzeiOmT2nk= cosmossdk.io/client/v2 v2.0.0-beta.5.0.20241121152743-3dad36d9a29e h1:NqQEVIjRqSdsAfTI9uDRZ1oU/cQuCoAbUuIkndQM+Bo= cosmossdk.io/client/v2 v2.0.0-beta.5.0.20241121152743-3dad36d9a29e/go.mod h1:4p0P6o0ro+FizakJUYS9SeM94RNbv0thLmkHRw5o5as= cosmossdk.io/collections v1.2.0 h1:IesfVG8G/+FYCMVMP01frS/Cw99Omk5vBh3cHbO01Gg= diff --git a/x/auth/ante/ante.go b/x/auth/ante/ante.go index 755ff8f38572..4ff59962f0cb 100644 --- a/x/auth/ante/ante.go +++ b/x/auth/ante/ante.go @@ -20,10 +20,10 @@ type HandlerOptions struct { SignModeHandler *txsigning.HandlerMap SigGasConsumer func(meter storetypes.GasMeter, sig signing.SignatureV2, params types.Params) error TxFeeChecker TxFeeChecker - // UnorderedNonceManager is an opt-in feature for x/auth. - // When set, this application will be able to receive and process unordered transactions. - UnorderedNonceManager UnorderedNonceManager - UnorderedTxOptions []UnorderedTxDecoratorOptions + // SigVerifyOptions are the options for the signature verification decorator. + // This allows for modification of signature verification behavior, such as how long an unordered transaction can + // be valid, or how much gas to charge for unordered transactions. + SigVerifyOptions []SigVerificationDecoratorOption } // NewAnteHandler returns an AnteHandler that checks and increments sequence @@ -53,13 +53,9 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { NewSetPubKeyDecorator(options.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators NewValidateSigCountDecorator(options.AccountKeeper), NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer), - NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), + NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler, options.SigVerifyOptions...), NewIncrementSequenceDecorator(options.AccountKeeper), } - if options.UnorderedNonceManager != nil { - anteDecorators = append(anteDecorators, NewUnorderedTxDecorator(options.UnorderedNonceManager, options.UnorderedTxOptions...)) - } - return sdk.ChainAnteDecorators(anteDecorators...), nil } diff --git a/x/auth/ante/ante_test.go b/x/auth/ante/ante_test.go index b9c46ab94213..e86e86abe563 100644 --- a/x/auth/ante/ante_test.go +++ b/x/auth/ante/ante_test.go @@ -1450,7 +1450,7 @@ func TestAnteHandlerReCheck(t *testing.T) { } func TestAnteHandlerUnorderedTx(t *testing.T) { - suite := SetupTestSuite(t, false) + suite := SetupTestSuiteWithUnordered(t, false, true) accs := suite.CreateTestAccounts(1) msg := testdata.NewTestMsg(accs[0].acc.GetAddress()) diff --git a/x/auth/ante/expected_keepers.go b/x/auth/ante/expected_keepers.go index 45ded21626c1..5f0990b42def 100644 --- a/x/auth/ante/expected_keepers.go +++ b/x/auth/ante/expected_keepers.go @@ -18,10 +18,7 @@ type AccountKeeper interface { SetAccount(ctx context.Context, acc sdk.AccountI) GetModuleAddress(moduleName string) sdk.AccAddress AddressCodec() address.Codec -} - -// UnorderedNonceManager defines the contract needed for UnorderedNonce management. -type UnorderedNonceManager interface { + UnorderedTransactionsEnabled() bool RemoveExpiredUnorderedNonces(ctx sdk.Context) error TryAddUnorderedNonce(ctx sdk.Context, sender []byte, timestamp time.Time) error } diff --git a/x/auth/ante/sigverify.go b/x/auth/ante/sigverify.go index 8eb087c554f5..b45846f0ab30 100644 --- a/x/auth/ante/sigverify.go +++ b/x/auth/ante/sigverify.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/hex" "fmt" + "time" "google.golang.org/protobuf/types/known/anypb" @@ -212,23 +213,65 @@ func (sgcd SigGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula return next(ctx, tx, simulate) } -// SigVerificationDecorator verifies all signatures for a tx and return an error if any are invalid. Note, -// the SigVerificationDecorator will not check signatures on ReCheck. +// SigVerificationDecorator verifies all signatures for a tx and returns an error if any are invalid. +// Note, the SigVerificationDecorator will not check signatures on ReCheck. +// +// As of Cosmos SDK v0.53.0, the SigVerificationDecorator will also verify the validity of unordered transactions. +// This involves ensuring the TTL is valid, and that the unordered nonce has not been used previously. // // CONTRACT: Pubkeys are set in context for all signers before this decorator runs // CONTRACT: Tx must implement SigVerifiableTx interface type SigVerificationDecorator struct { - ak AccountKeeper - signModeHandler *txsigning.HandlerMap + ak AccountKeeper + signModeHandler *txsigning.HandlerMap + maxTxTimeoutDuration time.Duration + unorderedTxGasCost uint64 +} + +type SigVerificationDecoratorOption func(svd *SigVerificationDecorator) + +// WithMaxUnorderedTxTimeoutDuration sets the maximum TTL a transaction can define for unordered transactions. +func WithMaxUnorderedTxTimeoutDuration(duration time.Duration) SigVerificationDecoratorOption { + return func(svd *SigVerificationDecorator) { + svd.maxTxTimeoutDuration = duration + } } -func NewSigVerificationDecorator(ak AccountKeeper, signModeHandler *txsigning.HandlerMap) SigVerificationDecorator { - return SigVerificationDecorator{ - ak: ak, - signModeHandler: signModeHandler, +// WithUnorderedTxGasCost sets the gas cost for unordered transactions. +// We must charge extra gas for unordered transactions +// as they incur extra processing time for cleaning up the expired txs in x/auth PreBlocker. +// Note: this value was chosen by 2x-ing the cost of fetching and removing an unordered nonce entry. +func WithUnorderedTxGasCost(gasCost uint64) SigVerificationDecoratorOption { + return func(svd *SigVerificationDecorator) { + svd.unorderedTxGasCost = gasCost } } +const ( + // DefaultMaxTimeoutDuration defines a default maximum TTL a transaction can define. + DefaultMaxTimeoutDuration = 10 * time.Minute + // DefaultUnorderedTxGasCost defines a default gas cost for unordered transactions. + // We must charge extra gas for unordered transactions + // as they incur extra processing time for cleaning up the expired txs in x/auth PreBlocker. + // Note: this value was chosen by 2x-ing the cost of fetching and removing an unordered nonce entry. + DefaultUnorderedTxGasCost = uint64(2240) +) + +func NewSigVerificationDecorator(ak AccountKeeper, signModeHandler *txsigning.HandlerMap, opts ...SigVerificationDecoratorOption) SigVerificationDecorator { + svd := SigVerificationDecorator{ + ak: ak, + signModeHandler: signModeHandler, + maxTxTimeoutDuration: DefaultMaxTimeoutDuration, + unorderedTxGasCost: DefaultUnorderedTxGasCost, + } + + for _, opt := range opts { + opt(&svd) + } + + return svd +} + // OnlyLegacyAminoSigners checks SignatureData to see if all // signers are using SIGN_MODE_LEGACY_AMINO_JSON. If this is the case // then the corresponding SignatureV2 struct will not have account sequence @@ -258,6 +301,11 @@ func (svd SigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simul utx, ok := tx.(sdk.TxWithUnordered) isUnordered := ok && utx.GetUnordered() + unorderedEnabled := svd.ak.UnorderedTransactionsEnabled() + + if isUnordered && !unorderedEnabled { + return ctx, errorsmod.Wrap(sdkerrors.ErrNotSupported, "unordered transactions are not enabled") + } // stdSigs contains the sequence number, account number, and signatures. // When simulating, this would just be a 0-length slice. @@ -276,6 +324,15 @@ func (svd SigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simul return ctx, errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "invalid number of signer; expected: %d, got %d", len(signers), len(sigs)) } + // In normal transactions, each signer has a sequence value. In unordered transactions, the nonce value is at the tx body level, + // so we get one nonce value for all the signers, rather than a sequence value for each signer. + // Because of this, we verify the unordered nonce outside the sigs loop, to avoid verifying the same nonce multiple times. + if isUnordered { + if err := svd.verifyUnorderedNonce(ctx, utx); err != nil { + return ctx, err + } + } + for i, sig := range sigs { acc, err := GetSignerAcc(ctx, svd.ak, signers[i]) if err != nil { @@ -344,6 +401,69 @@ func (svd SigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simul return next(ctx, tx, simulate) } +// verifyUnorderedNonce verifies the unordered nonce of an unordered transaction. +// This checks that: +// 1. The unordered transaction's timeout timestamp is set. +// 2. The unordered transaction's timeout timestamp is not in the past. +// 3. The unordered transaction's timeout timestamp is not more than the max TTL. +// 4. The unordered transaction's nonce has not been used previously. +// +// If all the checks above pass, the nonce is marked as used for each signer of the transaction. +func (svd SigVerificationDecorator) verifyUnorderedNonce(ctx sdk.Context, unorderedTx sdk.TxWithUnordered) error { + blockTime := ctx.BlockTime() + timeoutTimestamp := unorderedTx.GetTimeoutTimeStamp() + if timeoutTimestamp.IsZero() || timeoutTimestamp.Unix() == 0 { + return errorsmod.Wrap( + sdkerrors.ErrInvalidRequest, + "unordered transaction must have timeout_timestamp set", + ) + } + if timeoutTimestamp.Before(blockTime) { + return errorsmod.Wrap( + sdkerrors.ErrInvalidRequest, + "unordered transaction has a timeout_timestamp that has already passed", + ) + } + if timeoutTimestamp.After(blockTime.Add(svd.maxTxTimeoutDuration)) { + return errorsmod.Wrapf( + sdkerrors.ErrInvalidRequest, + "unordered tx ttl exceeds %s", + svd.maxTxTimeoutDuration.String(), + ) + } + + ctx.GasMeter().ConsumeGas(svd.unorderedTxGasCost, "unordered tx") + + execMode := ctx.ExecMode() + if execMode == sdk.ExecModeSimulate { + return nil + } + + signerAddrs, err := extractSignersBytes(unorderedTx) + if err != nil { + return err + } + + for _, signerAddr := range signerAddrs { + if err := svd.ak.TryAddUnorderedNonce(ctx, signerAddr, unorderedTx.GetTimeoutTimeStamp()); err != nil { + return errorsmod.Wrapf( + sdkerrors.ErrInvalidRequest, + "failed to add unordered nonce: %s", err, + ) + } + } + + return nil +} + +func extractSignersBytes(tx sdk.Tx) ([][]byte, error) { + sigTx, ok := tx.(authsigning.SigVerifiableTx) + if !ok { + return nil, errorsmod.Wrap(sdkerrors.ErrTxDecode, "invalid tx type") + } + return sigTx.GetSigners() +} + // IncrementSequenceDecorator handles incrementing sequences of all signers. // Use the IncrementSequenceDecorator decorator to prevent replay attacks. Note, // there is need to execute IncrementSequenceDecorator on RecheckTx since @@ -365,6 +485,9 @@ func NewIncrementSequenceDecorator(ak AccountKeeper) IncrementSequenceDecorator func (isd IncrementSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { if utx, ok := tx.(sdk.TxWithUnordered); ok && utx.GetUnordered() { + if !isd.ak.UnorderedTransactionsEnabled() { + return ctx, errorsmod.Wrap(sdkerrors.ErrNotSupported, "unordered transactions are disabled") + } return next(ctx, tx, simulate) } sigTx, ok := tx.(authsigning.SigVerifiableTx) diff --git a/x/auth/ante/sigverify_test.go b/x/auth/ante/sigverify_test.go index 5d56d3ca81b7..8cc816e79254 100644 --- a/x/auth/ante/sigverify_test.go +++ b/x/auth/ante/sigverify_test.go @@ -319,8 +319,21 @@ func runSigDecorators(t *testing.T, params types.Params, _ bool, privs ...crypto return after - before, err } -func TestIncrementSequenceDecorator(t *testing.T) { +func TestIncrementSequenceDecorator_ShouldFailWhenUnorderedTxsDisabled(t *testing.T) { suite := SetupTestSuite(t, true) + isd := ante.NewIncrementSequenceDecorator(suite.accountKeeper) + antehandler := sdk.ChainAnteDecorators(isd) + + priv, _, _ := testdata.KeyTestPubAddr() + tx, err := suite.CreateTestUnorderedTx(suite.ctx, []cryptotypes.PrivKey{priv}, []uint64{0}, []uint64{0}, suite.ctx.ChainID(), signing.SignMode_SIGN_MODE_DIRECT, true, time.Now()) + require.NoError(t, err) + + _, err = antehandler(suite.ctx, tx, false) + require.ErrorContains(t, err, "unordered transactions are disabled") +} + +func TestIncrementSequenceDecorator(t *testing.T) { + suite := SetupTestSuiteWithUnordered(t, true, true) suite.txBuilder = suite.clientCtx.TxConfig.NewTxBuilder() priv, _, addr := testdata.KeyTestPubAddr() @@ -382,7 +395,7 @@ func TestIncrementSequenceDecorator(t *testing.T) { true, }, { - "no inc on unordered", + "unordered tx should not inc sequence", suite.ctx.WithIsReCheckTx(true), true, func() sdk.Tx { diff --git a/x/auth/ante/testutil/expected_keepers_mocks.go b/x/auth/ante/testutil/expected_keepers_mocks.go index 2449b5995816..2942459bc273 100644 --- a/x/auth/ante/testutil/expected_keepers_mocks.go +++ b/x/auth/ante/testutil/expected_keepers_mocks.go @@ -100,6 +100,20 @@ func (mr *MockAccountKeeperMockRecorder) GetParams(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParams", reflect.TypeOf((*MockAccountKeeper)(nil).GetParams), ctx) } +// RemoveExpiredUnorderedNonces mocks base method. +func (m *MockAccountKeeper) RemoveExpiredUnorderedNonces(ctx types.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveExpiredUnorderedNonces", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveExpiredUnorderedNonces indicates an expected call of RemoveExpiredUnorderedNonces. +func (mr *MockAccountKeeperMockRecorder) RemoveExpiredUnorderedNonces(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveExpiredUnorderedNonces", reflect.TypeOf((*MockAccountKeeper)(nil).RemoveExpiredUnorderedNonces), ctx) +} + // SetAccount mocks base method. func (m *MockAccountKeeper) SetAccount(ctx context.Context, acc types.AccountI) { m.ctrl.T.Helper() @@ -112,56 +126,32 @@ func (mr *MockAccountKeeperMockRecorder) SetAccount(ctx, acc any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAccount", reflect.TypeOf((*MockAccountKeeper)(nil).SetAccount), ctx, acc) } -// MockUnorderedNonceManager is a mock of UnorderedNonceManager interface. -type MockUnorderedNonceManager struct { - ctrl *gomock.Controller - recorder *MockUnorderedNonceManagerMockRecorder - isgomock struct{} -} - -// MockUnorderedNonceManagerMockRecorder is the mock recorder for MockUnorderedNonceManager. -type MockUnorderedNonceManagerMockRecorder struct { - mock *MockUnorderedNonceManager -} - -// NewMockUnorderedNonceManager creates a new mock instance. -func NewMockUnorderedNonceManager(ctrl *gomock.Controller) *MockUnorderedNonceManager { - mock := &MockUnorderedNonceManager{ctrl: ctrl} - mock.recorder = &MockUnorderedNonceManagerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockUnorderedNonceManager) EXPECT() *MockUnorderedNonceManagerMockRecorder { - return m.recorder -} - -// RemoveExpiredUnorderedNonces mocks base method. -func (m *MockUnorderedNonceManager) RemoveExpiredUnorderedNonces(ctx types.Context) error { +// TryAddUnorderedNonce mocks base method. +func (m *MockAccountKeeper) TryAddUnorderedNonce(ctx types.Context, sender []byte, timestamp time.Time) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveExpiredUnorderedNonces", ctx) + ret := m.ctrl.Call(m, "TryAddUnorderedNonce", ctx, sender, timestamp) ret0, _ := ret[0].(error) return ret0 } -// RemoveExpiredUnorderedNonces indicates an expected call of RemoveExpiredUnorderedNonces. -func (mr *MockUnorderedNonceManagerMockRecorder) RemoveExpiredUnorderedNonces(ctx any) *gomock.Call { +// TryAddUnorderedNonce indicates an expected call of TryAddUnorderedNonce. +func (mr *MockAccountKeeperMockRecorder) TryAddUnorderedNonce(ctx, sender, timestamp any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveExpiredUnorderedNonces", reflect.TypeOf((*MockUnorderedNonceManager)(nil).RemoveExpiredUnorderedNonces), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TryAddUnorderedNonce", reflect.TypeOf((*MockAccountKeeper)(nil).TryAddUnorderedNonce), ctx, sender, timestamp) } -// TryAddUnorderedNonce mocks base method. -func (m *MockUnorderedNonceManager) TryAddUnorderedNonce(ctx types.Context, sender []byte, timestamp time.Time) error { +// UnorderedTransactionsEnabled mocks base method. +func (m *MockAccountKeeper) UnorderedTransactionsEnabled() bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "TryAddUnorderedNonce", ctx, sender, timestamp) - ret0, _ := ret[0].(error) + ret := m.ctrl.Call(m, "UnorderedTransactionsEnabled") + ret0, _ := ret[0].(bool) return ret0 } -// TryAddUnorderedNonce indicates an expected call of TryAddUnorderedNonce. -func (mr *MockUnorderedNonceManagerMockRecorder) TryAddUnorderedNonce(ctx, sender, timestamp any) *gomock.Call { +// UnorderedTransactionsEnabled indicates an expected call of UnorderedTransactionsEnabled. +func (mr *MockAccountKeeperMockRecorder) UnorderedTransactionsEnabled() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TryAddUnorderedNonce", reflect.TypeOf((*MockUnorderedNonceManager)(nil).TryAddUnorderedNonce), ctx, sender, timestamp) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnorderedTransactionsEnabled", reflect.TypeOf((*MockAccountKeeper)(nil).UnorderedTransactionsEnabled)) } // MockFeegrantKeeper is a mock of FeegrantKeeper interface. diff --git a/x/auth/ante/testutil_test.go b/x/auth/ante/testutil_test.go index 93376714d07c..b0dfbfae47ef 100644 --- a/x/auth/ante/testutil_test.go +++ b/x/auth/ante/testutil_test.go @@ -56,8 +56,7 @@ type AnteTestSuite struct { encCfg moduletestutil.TestEncodingConfig } -// SetupTest setups a new test, with new app, context, and anteHandler. -func SetupTestSuite(t *testing.T, isCheckTx bool) *AnteTestSuite { +func setupSuite(t *testing.T, isCheckTx, enableUnorderedTxs bool) *AnteTestSuite { t.Helper() suite := &AnteTestSuite{} @@ -82,7 +81,7 @@ func SetupTestSuite(t *testing.T, isCheckTx bool) *AnteTestSuite { suite.accountKeeper = keeper.NewAccountKeeper( suite.encCfg.Codec, runtime.NewKVStoreService(key), types.ProtoBaseAccount, maccPerms, authcodec.NewBech32Codec("cosmos"), - sdk.Bech32MainPrefix, types.NewModuleAddress("gov").String(), + sdk.Bech32MainPrefix, types.NewModuleAddress("gov").String(), keeper.WithUnorderedTransactions(enableUnorderedTxs), ) suite.accountKeeper.GetModuleAccount(suite.ctx, types.FeeCollectorName) err := suite.accountKeeper.Params.Set(suite.ctx, types.DefaultParams()) @@ -114,6 +113,18 @@ func SetupTestSuite(t *testing.T, isCheckTx bool) *AnteTestSuite { return suite } +// SetupTest setups a new test, with new app, context, and anteHandler. +func SetupTestSuite(t *testing.T, isCheckTx bool) *AnteTestSuite { + t.Helper() + return setupSuite(t, isCheckTx, false) +} + +// SetupTest setups a new test, with new app, context, and anteHandler. +func SetupTestSuiteWithUnordered(t *testing.T, isCheckTx, enableUnorderedTxs bool) *AnteTestSuite { + t.Helper() + return setupSuite(t, isCheckTx, enableUnorderedTxs) +} + func (suite *AnteTestSuite) CreateTestAccounts(numAccs int) []TestAccount { var accounts []TestAccount @@ -216,11 +227,14 @@ func (suite *AnteTestSuite) RunTestCase(t *testing.T, tc TestCase, args TestCase } // CreateTestTx is a helper function to create a tx given multiple inputs. -func (suite *AnteTestSuite) CreateTestTx( +func (suite *AnteTestSuite) createTx( ctx sdk.Context, privs []cryptotypes.PrivKey, accNums, accSeqs []uint64, - chainID string, signMode signing.SignMode, + chainID string, signMode signing.SignMode, unordered bool, unorderedTimeout time.Time, ) (xauthsigning.Tx, error) { + suite.txBuilder.SetUnordered(unordered) + suite.txBuilder.SetTimeoutTimestamp(unorderedTimeout) + // First round: we gather all the signer infos. We use the "set empty // signature" hack to do that. var sigsV2 []signing.SignatureV2 @@ -268,58 +282,20 @@ func (suite *AnteTestSuite) CreateTestTx( return suite.txBuilder.GetTx(), nil } +// CreateTestTx is a helper function to create a tx given multiple inputs. +func (suite *AnteTestSuite) CreateTestTx( + ctx sdk.Context, privs []cryptotypes.PrivKey, + accNums, accSeqs []uint64, + chainID string, signMode signing.SignMode, +) (xauthsigning.Tx, error) { + return suite.createTx(ctx, privs, accNums, accSeqs, chainID, signMode, false, time.Time{}) +} + func (suite *AnteTestSuite) CreateTestUnorderedTx( ctx sdk.Context, privs []cryptotypes.PrivKey, accNums, accSeqs []uint64, chainID string, signMode signing.SignMode, unordered bool, unorderedTimeout time.Time, ) (xauthsigning.Tx, error) { - suite.txBuilder.SetUnordered(unordered) - suite.txBuilder.SetTimeoutTimestamp(unorderedTimeout) - - // First round: we gather all the signer infos. We use the "set empty - // signature" hack to do that. - var sigsV2 []signing.SignatureV2 - for i, priv := range privs { - sigV2 := signing.SignatureV2{ - PubKey: priv.PubKey(), - Data: &signing.SingleSignatureData{ - SignMode: signMode, - Signature: nil, - }, - Sequence: accSeqs[i], - } - - sigsV2 = append(sigsV2, sigV2) - } - err := suite.txBuilder.SetSignatures(sigsV2...) - if err != nil { - return nil, err - } - - // Second round: all signer infos are set, so each signer can sign. - sigsV2 = []signing.SignatureV2{} - for i, priv := range privs { - signerData := xauthsigning.SignerData{ - Address: sdk.AccAddress(priv.PubKey().Address()).String(), - ChainID: chainID, - AccountNumber: accNums[i], - Sequence: accSeqs[i], - PubKey: priv.PubKey(), - } - sigV2, err := tx.SignWithPrivKey( - ctx, signMode, signerData, - suite.txBuilder, priv, suite.clientCtx.TxConfig, accSeqs[i]) - if err != nil { - return nil, err - } - - sigsV2 = append(sigsV2, sigV2) - } - err = suite.txBuilder.SetSignatures(sigsV2...) - if err != nil { - return nil, err - } - - return suite.txBuilder.GetTx(), nil + return suite.createTx(ctx, privs, accNums, accSeqs, chainID, signMode, unordered, unorderedTimeout) } diff --git a/x/auth/ante/unordered.go b/x/auth/ante/unordered.go deleted file mode 100644 index 28b2f927ce95..000000000000 --- a/x/auth/ante/unordered.go +++ /dev/null @@ -1,146 +0,0 @@ -package ante - -import ( - "time" - - errorsmod "cosmossdk.io/errors" - - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" -) - -const ( - // DefaultMaxTimoutDuration defines a default maximum TTL a transaction can define. - DefaultMaxTimoutDuration = 10 * time.Minute - // DefaultUnorderedTxGasCost defines a default gas cost for unordered transactions. - // We must charge extra gas for unordered transactions - // as they incur extra processing time for cleaning up the expired txs in x/auth PreBlocker. - // Note: this value was chosen by 2x-ing the cost of fetching and removing an unordered nonce entry. - DefaultUnorderedTxGasCost = uint64(2240) -) - -var _ sdk.AnteDecorator = (*UnorderedTxDecorator)(nil) - -type UnorderedTxDecoratorOptions func(*UnorderedTxDecorator) - -// WithTimeoutDuration allows for changing the timeout duration for unordered txs. -func WithTimeoutDuration(duration time.Duration) UnorderedTxDecoratorOptions { - return func(utx *UnorderedTxDecorator) { - utx.maxTxTimeoutDuration = duration - } -} - -func WithUnorderedTxGasCost(cost uint64) UnorderedTxDecoratorOptions { - return func(utx *UnorderedTxDecorator) { - utx.txGasCost = cost - } -} - -// UnorderedTxDecorator defines an AnteHandler decorator that is responsible for -// checking if a transaction is intended to be unordered and, if so, evaluates -// the transaction accordingly. An unordered transaction will bypass having its -// nonce incremented, which allows fire-and-forget transaction broadcasting, -// removing the necessity of ordering on the sender-side. -// -// The transaction sender must ensure that unordered=true and a timeout_height -// is appropriately set. The AnteHandler will check that the transaction is not -// a duplicate and will evict it from state when the timeout is reached. -// -// The UnorderedTxDecorator should be placed as early as possible in the AnteHandler -// chain to ensure that during DeliverTx, the transaction is added to the UnorderedNonceManager. -type UnorderedTxDecorator struct { - maxTxTimeoutDuration time.Duration - txGasCost uint64 - txManager UnorderedNonceManager -} - -func NewUnorderedTxDecorator( - utxm UnorderedNonceManager, - opts ...UnorderedTxDecoratorOptions, -) *UnorderedTxDecorator { - utx := &UnorderedTxDecorator{ - maxTxTimeoutDuration: DefaultMaxTimoutDuration, - txGasCost: DefaultUnorderedTxGasCost, - txManager: utxm, - } - for _, opt := range opts { - opt(utx) - } - - return utx -} - -func (d *UnorderedTxDecorator) AnteHandle( - ctx sdk.Context, - tx sdk.Tx, - _ bool, - next sdk.AnteHandler, -) (sdk.Context, error) { - if err := d.ValidateTx(ctx, tx); err != nil { - return ctx, err - } - return next(ctx, tx, false) -} - -func (d *UnorderedTxDecorator) ValidateTx(ctx sdk.Context, tx sdk.Tx) error { - unorderedTx, ok := tx.(sdk.TxWithUnordered) - if !ok || !unorderedTx.GetUnordered() { - // If the transaction does not implement unordered capabilities or has the - // unordered value as false, we bypass. - return nil - } - - blockTime := ctx.BlockTime() - timeoutTimestamp := unorderedTx.GetTimeoutTimeStamp() - if timeoutTimestamp.IsZero() || timeoutTimestamp.Unix() == 0 { - return errorsmod.Wrap( - sdkerrors.ErrInvalidRequest, - "unordered transaction must have timeout_timestamp set", - ) - } - if timeoutTimestamp.Before(blockTime) { - return errorsmod.Wrap( - sdkerrors.ErrInvalidRequest, - "unordered transaction has a timeout_timestamp that has already passed", - ) - } - if timeoutTimestamp.After(blockTime.Add(d.maxTxTimeoutDuration)) { - return errorsmod.Wrapf( - sdkerrors.ErrInvalidRequest, - "unordered tx ttl exceeds %s", - d.maxTxTimeoutDuration.String(), - ) - } - - ctx.GasMeter().ConsumeGas(d.txGasCost, "unordered tx") - - execMode := ctx.ExecMode() - if execMode == sdk.ExecModeSimulate { - return nil - } - - signerAddrs, err := extractSignersBytes(tx) - if err != nil { - return err - } - - for _, signerAddr := range signerAddrs { - if err := d.txManager.TryAddUnorderedNonce(ctx, signerAddr, unorderedTx.GetTimeoutTimeStamp()); err != nil { - return errorsmod.Wrapf( - sdkerrors.ErrInvalidRequest, - "failed to add unordered nonce: %s", err, - ) - } - } - - return nil -} - -func extractSignersBytes(tx sdk.Tx) ([][]byte, error) { - sigTx, ok := tx.(authsigning.SigVerifiableTx) - if !ok { - return nil, errorsmod.Wrap(sdkerrors.ErrTxDecode, "invalid tx type") - } - return sigTx.GetSigners() -} diff --git a/x/auth/ante/unordered_test.go b/x/auth/ante/unordered_test.go index f47901fe89f7..d9622fae5aef 100644 --- a/x/auth/ante/unordered_test.go +++ b/x/auth/ante/unordered_test.go @@ -5,140 +5,204 @@ import ( "time" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" - storetypes "cosmossdk.io/store/types" - + "github.com/cosmos/cosmos-sdk/codec" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - "github.com/cosmos/cosmos-sdk/runtime" - "github.com/cosmos/cosmos-sdk/testutil" "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" - moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" "github.com/cosmos/cosmos-sdk/types/tx/signing" "github.com/cosmos/cosmos-sdk/x/auth/ante" - authcodec "github.com/cosmos/cosmos-sdk/x/auth/codec" - "github.com/cosmos/cosmos-sdk/x/auth/keeper" - "github.com/cosmos/cosmos-sdk/x/auth/types" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + txmodule "github.com/cosmos/cosmos-sdk/x/auth/tx/config" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) -func TestUnorderedAnte(t *testing.T) { - testPK, _, testAddr := testdata.KeyTestPubAddr() +type UnorderedTxTestSuite struct { + anteSuite *AnteTestSuite + priv1, priv2, priv3 cryptotypes.PrivKey + antehandler sdk.AnteHandler + defaultSignMode signing.SignMode + accs []sdk.AccountI + msgs []sdk.Msg +} + +func setupUnorderedTxTestSuite(t *testing.T, isCheckTx, withUnordered bool) *UnorderedTxTestSuite { + t.Helper() + anteSuite := SetupTestSuiteWithUnordered(t, isCheckTx, withUnordered) + anteSuite.txBankKeeper.EXPECT().DenomMetadata(gomock.Any(), gomock.Any()).Return(&banktypes.QueryDenomMetadataResponse{}, nil).AnyTimes() + + enabledSignModes := []signing.SignMode{signing.SignMode_SIGN_MODE_DIRECT} + txConfigOpts := authtx.ConfigOptions{ + TextualCoinMetadataQueryFn: txmodule.NewGRPCCoinMetadataQueryFn(anteSuite.clientCtx), + EnabledSignModes: enabledSignModes, + } + var err error + anteSuite.clientCtx.TxConfig, err = authtx.NewTxConfigWithOptions( + codec.NewProtoCodec(anteSuite.encCfg.InterfaceRegistry), + txConfigOpts, + ) + require.NoError(t, err) + anteSuite.txBuilder = anteSuite.clientCtx.TxConfig.NewTxBuilder() + + // make block height non-zero to ensure account numbers part of signBytes + anteSuite.ctx = anteSuite.ctx.WithBlockHeight(1) + + // keys and addresses + pk1, _, addr1 := testdata.KeyTestPubAddr() + pk2, _, addr2 := testdata.KeyTestPubAddr() + pk3, _, addr3 := testdata.KeyTestPubAddr() + priv1, priv2, priv3 := pk1, pk2, pk3 + + addrs := []sdk.AccAddress{addr1, addr2, addr3} + + msgs := make([]sdk.Msg, len(addrs)) + accs := make([]sdk.AccountI, len(addrs)) + // set accounts and create msg for each address + for i, addr := range addrs { + acc := anteSuite.accountKeeper.NewAccountWithAddress(anteSuite.ctx, addr) + require.NoError(t, acc.SetAccountNumber(uint64(i)+1000)) + anteSuite.accountKeeper.SetAccount(anteSuite.ctx, acc) + msgs[i] = testdata.NewTestMsg(addr) + accs[i] = acc + } + + spkd := ante.NewSetPubKeyDecorator(anteSuite.accountKeeper) + txConfigOpts = authtx.ConfigOptions{ + TextualCoinMetadataQueryFn: txmodule.NewBankKeeperCoinMetadataQueryFn(anteSuite.txBankKeeper), + EnabledSignModes: enabledSignModes, + } + anteTxConfig, err := authtx.NewTxConfigWithOptions( + codec.NewProtoCodec(anteSuite.encCfg.InterfaceRegistry), + txConfigOpts, + ) + require.NoError(t, err) + svd := ante.NewSigVerificationDecorator(anteSuite.accountKeeper, anteTxConfig.SignModeHandler()) + antehandler := sdk.ChainAnteDecorators(spkd, svd) + defaultSignMode := signing.SignMode_SIGN_MODE_DIRECT + + return &UnorderedTxTestSuite{ + anteSuite: anteSuite, + priv1: priv1, + priv2: priv2, + priv3: priv3, + antehandler: antehandler, + defaultSignMode: defaultSignMode, + accs: accs, + msgs: msgs, + } +} + +func TestSigVerification_UnorderedTxs(t *testing.T) { + feeAmount := testdata.NewTestFeeAmount() + gasLimit := testdata.NewTestGasLimit() testCases := map[string]struct { - addTxs func() []sdk.Tx - runTx func() sdk.Tx - blockTime time.Time - execMode sdk.ExecMode - expectedErr string + unorderedDisabled bool + unordered bool + timeout time.Time + blockTime time.Time + duplicate bool + execMode sdk.ExecMode + expectedErr string }{ "normal/ordered tx should just skip": { - runTx: func() sdk.Tx { - return genTestTx(t, genTxOptions{}) - }, + unordered: false, blockTime: time.Unix(0, 0), execMode: sdk.ExecModeFinalize, }, - "happy case - simple pass": { - runTx: func() sdk.Tx { - return genTestTx(t, genTxOptions{unordered: true, timestamp: time.Unix(10, 0)}) - }, + "normal/ordered tx should just skip with unordered disabled too": { + unorderedDisabled: true, + unordered: false, + blockTime: time.Unix(0, 0), + execMode: sdk.ExecModeFinalize, + }, + "happy case": { + unordered: true, + timeout: time.Unix(10, 0), blockTime: time.Unix(0, 0), execMode: sdk.ExecModeFinalize, }, "zero time should fail": { - runTx: func() sdk.Tx { - return genTestTx(t, genTxOptions{unordered: true}) - }, - blockTime: time.Unix(0, 0), + unordered: true, + blockTime: time.Unix(10, 0), execMode: sdk.ExecModeFinalize, expectedErr: "unordered transaction must have timeout_timestamp set", }, + "fail if tx is unordered but unordered is disabled": { + unorderedDisabled: true, + unordered: true, + blockTime: time.Unix(10, 0), + execMode: sdk.ExecModeFinalize, + expectedErr: "unordered transactions are not enabled", + }, "timeout before current block time should fail": { - runTx: func() sdk.Tx { - return genTestTx(t, genTxOptions{unordered: true, timestamp: time.Unix(7, 0)}) - }, + unordered: true, + timeout: time.Unix(7, 0), blockTime: time.Unix(10, 1), execMode: sdk.ExecModeFinalize, expectedErr: "unordered transaction has a timeout_timestamp that has already passed", }, "timeout equal to current block time should pass": { - runTx: func() sdk.Tx { - return genTestTx(t, genTxOptions{unordered: true, timestamp: time.Unix(10, 0)}) - }, + unordered: true, + timeout: time.Unix(10, 0), blockTime: time.Unix(10, 0), execMode: sdk.ExecModeFinalize, }, "timeout after the max duration should fail": { - runTx: func() sdk.Tx { - return genTestTx(t, genTxOptions{unordered: true, timestamp: time.Unix(10, 1).Add(ante.DefaultMaxTimoutDuration)}) - }, + unordered: true, + timeout: time.Unix(10, 1).Add(ante.DefaultMaxTimeoutDuration), blockTime: time.Unix(10, 0), execMode: sdk.ExecModeFinalize, expectedErr: "unordered tx ttl exceeds", }, "fails if manager has duplicate": { - addTxs: func() []sdk.Tx { - tx := genTestTx( - t, - genTxOptions{unordered: true, timestamp: time.Unix(10, 0), pk: testPK, addr: testAddr}, - ) - return []sdk.Tx{tx} - }, - runTx: func() sdk.Tx { - return genTestTx( - t, - genTxOptions{unordered: true, timestamp: time.Unix(10, 0), pk: testPK, addr: testAddr}, - ) - }, + unordered: true, + timeout: time.Unix(10, 0), + duplicate: true, blockTime: time.Unix(5, 0), execMode: sdk.ExecModeFinalize, expectedErr: "already used timeout", }, "duplicate doesn't matter if we're in simulate mode": { - addTxs: func() []sdk.Tx { - tx := genTestTx( - t, - genTxOptions{unordered: true, timestamp: time.Unix(10, 0), pk: testPK, addr: testAddr}, - ) - return []sdk.Tx{tx} - }, - runTx: func() sdk.Tx { - return genTestTx( - t, - genTxOptions{unordered: true, timestamp: time.Unix(10, 0), pk: testPK, addr: testAddr}, - ) - }, + unordered: true, + timeout: time.Unix(10, 0), + duplicate: true, blockTime: time.Unix(5, 0), execMode: sdk.ExecModeSimulate, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { - mockStoreKey := storetypes.NewKVStoreKey("test") - storeService := runtime.NewKVStoreService(mockStoreKey) - ctx := testutil.DefaultContextWithDB( - t, - mockStoreKey, - storetypes.NewTransientStoreKey("transient_test"), - ).Ctx.WithBlockTime(tc.blockTime).WithExecMode(tc.execMode) - mgr := keeper.NewAccountKeeper( - moduletestutil.MakeTestEncodingConfig().Codec, - storeService, - types.ProtoBaseAccount, - nil, - authcodec.NewBech32Codec("cosmos"), - "cosmos", - types.NewModuleAddress("gov").String(), + suite := setupUnorderedTxTestSuite(t, tc.execMode == sdk.ExecModeCheck, !tc.unorderedDisabled) + ctx := suite.anteSuite.ctx.WithBlockTime(tc.blockTime).WithExecMode(tc.execMode).WithIsSigverifyTx(true) + + suite.anteSuite.txBuilder = suite.anteSuite.clientCtx.TxConfig.NewTxBuilder() // Create new txBuilder for each test + require.NoError(t, suite.anteSuite.txBuilder.SetMsgs(suite.msgs...)) + suite.anteSuite.txBuilder.SetFeeAmount(feeAmount) + suite.anteSuite.txBuilder.SetGasLimit(gasLimit) + tx, err := suite.anteSuite.CreateTestUnorderedTx( + suite.anteSuite.ctx, + []cryptotypes.PrivKey{suite.priv1, suite.priv2, suite.priv3}, + []uint64{suite.accs[0].GetAccountNumber(), suite.accs[1].GetAccountNumber(), suite.accs[2].GetAccountNumber()}, + []uint64{0, 0, 0}, + suite.anteSuite.ctx.ChainID(), + suite.defaultSignMode, + tc.unordered, + tc.timeout, ) - chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(mgr)) - var err error - if tc.addTxs != nil { - txs := tc.addTxs() - for _, tx := range txs { - ctx, err = chain(ctx, tx, false) - require.NoError(t, err) - } + require.NoError(t, err) + txBytes, err := suite.anteSuite.clientCtx.TxConfig.TxEncoder()(tx) + require.NoError(t, err) + ctx = ctx.WithTxBytes(txBytes) + + simulate := tc.execMode == sdk.ExecModeSimulate + + if tc.duplicate { + _, err = suite.antehandler(ctx, tx, simulate) + require.NoError(t, err) } - _, err = chain(ctx, tc.runTx(), false) + + _, err = suite.antehandler(ctx, tx, simulate) if tc.expectedErr != "" { require.ErrorContains(t, err, tc.expectedErr) } else { @@ -147,111 +211,3 @@ func TestUnorderedAnte(t *testing.T) { }) } } - -func TestMultiSignerUnorderedTx(t *testing.T) { - pk1, _, addr1 := testdata.KeyTestPubAddr() - pk2, _, addr2 := testdata.KeyTestPubAddr() - pk3, _, addr3 := testdata.KeyTestPubAddr() - - signerAddrs := []sdk.AccAddress{addr1, addr2, addr3} - - mockStoreKey := storetypes.NewKVStoreKey("test") - storeService := runtime.NewKVStoreService(mockStoreKey) - ctx := testutil.DefaultContextWithDB( - t, - mockStoreKey, - storetypes.NewTransientStoreKey("transient_test"), - ).Ctx.WithBlockTime(time.Unix(9, 0)) - mgr := keeper.NewAccountKeeper( - moduletestutil.MakeTestEncodingConfig().Codec, - storeService, - types.ProtoBaseAccount, - nil, - authcodec.NewBech32Codec("cosmos"), - "cosmos", - types.NewModuleAddress("gov").String(), - ) - chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(mgr)) - - timeout := time.Unix(10, 0) - tx := genMultiSignedUnorderedTx(t, signerAddrs, timeout, []cryptotypes.PrivKey{pk1, pk2, pk3}) - - newCtx, err := chain(ctx, tx, false) - require.NoError(t, err) - - for _, addr := range signerAddrs { - ok, err := mgr.ContainsUnorderedNonce(newCtx, addr.Bytes(), timeout) - require.NoError(t, err) - require.True(t, ok) - } -} - -type genTxOptions struct { - unordered bool - timestamp time.Time - pk cryptotypes.PrivKey - addr sdk.AccAddress -} - -func genTestTx(t *testing.T, options genTxOptions) sdk.Tx { - t.Helper() - - s := SetupTestSuite(t, true) - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - // keys and addresses - priv1 := options.pk - addr1 := options.addr - if options.pk == nil || options.addr == nil { - priv1, _, addr1 = testdata.KeyTestPubAddr() - } - - // msg and signatures - msg := testdata.NewTestMsg(addr1) - feeAmount := testdata.NewTestFeeAmount() - gasLimit := testdata.NewTestGasLimit() - require.NoError(t, s.txBuilder.SetMsgs(msg)) - - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - s.txBuilder.SetUnordered(options.unordered) - s.txBuilder.SetTimeoutTimestamp(options.timestamp) - - privKeys, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(s.ctx, privKeys, accNums, accSeqs, s.ctx.ChainID(), signing.SignMode_SIGN_MODE_DIRECT) - require.NoError(t, err) - - require.NoError(t, err) - - return tx -} - -func genMultiSignedUnorderedTx(t *testing.T, addrs []sdk.AccAddress, ts time.Time, pks []cryptotypes.PrivKey) sdk.Tx { - t.Helper() - - s := SetupTestSuite(t, true) - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - // msg and signatures - msgs := make([]sdk.Msg, 0, len(addrs)) - for _, addr := range addrs { - msgs = append(msgs, testdata.NewTestMsg(addr)) - } - feeAmount := testdata.NewTestFeeAmount() - gasLimit := testdata.NewTestGasLimit() - require.NoError(t, s.txBuilder.SetMsgs(msgs...)) - - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - s.txBuilder.SetUnordered(true) - s.txBuilder.SetTimeoutTimestamp(ts) - - accNums := make([]uint64, len(pks)) - accSeqs := make([]uint64, len(pks)) - tx, err := s.CreateTestTx(s.ctx, pks, accNums, accSeqs, s.ctx.ChainID(), signing.SignMode_SIGN_MODE_DIRECT) - require.NoError(t, err) - - require.NoError(t, err) - - return tx -} diff --git a/x/auth/keeper/keeper.go b/x/auth/keeper/keeper.go index 2fb009bb9847..cca314456f5b 100644 --- a/x/auth/keeper/keeper.go +++ b/x/auth/keeper/keeper.go @@ -93,6 +93,10 @@ type AccountKeeper struct { permAddrs map[string]types.PermissionsForAddress bech32Prefix string + // enableUnorderedTxs enables unordered transaction support. + // This boolean helps sigverify ante handlers to determine if they should process unordered transactions. + enableUnorderedTxs bool + // The prototypical AccountI constructor. proto func() sdk.AccountI @@ -108,6 +112,17 @@ type AccountKeeper struct { UnorderedNonces collections.KeySet[collections.Pair[int64, []byte]] } +type InitOption func(*AccountKeeper) + +// WithUnorderedTransactions enables unordered transaction support. +// When true, sigverify ante handlers will validate and process unordered transactions. +// When false, sigverify ante handlers will reject unordered transactions. +func WithUnorderedTransactions(enable bool) InitOption { + return func(ak *AccountKeeper) { + ak.enableUnorderedTxs = enable + } +} + var _ AccountKeeperI = &AccountKeeper{} // NewAccountKeeper returns a new AccountKeeperI that uses go-amino to @@ -118,7 +133,7 @@ var _ AccountKeeperI = &AccountKeeper{} // may use auth.Keeper to access the accounts permissions map. func NewAccountKeeper( cdc codec.BinaryCodec, storeService store.KVStoreService, proto func() sdk.AccountI, - maccPerms map[string][]string, ac address.Codec, bech32Prefix, authority string, + maccPerms map[string][]string, ac address.Codec, bech32Prefix, authority string, opts ...InitOption, ) AccountKeeper { permAddrs := make(map[string]types.PermissionsForAddress) for name, perms := range maccPerms { @@ -145,9 +160,17 @@ func NewAccountKeeper( panic(err) } ak.Schema = schema + + for _, opt := range opts { + opt(&ak) + } return ak } +func (ak AccountKeeper) UnorderedTransactionsEnabled() bool { + return ak.enableUnorderedTxs +} + // GetAuthority returns the x/auth module's authority. func (ak AccountKeeper) GetAuthority() string { return ak.authority diff --git a/x/auth/keeper/unordered_tx_test.go b/x/auth/keeper/unordered_tx_test.go index 6576fe9fa8ee..7f6524caf1ff 100644 --- a/x/auth/keeper/unordered_tx_test.go +++ b/x/auth/keeper/unordered_tx_test.go @@ -35,6 +35,7 @@ func TestManager(t *testing.T) { authcodec.NewBech32Codec("cosmos"), "cosmos", types.NewModuleAddress("gov").String(), + keeper.WithUnorderedTransactions(true), ) } @@ -312,6 +313,7 @@ func TestCannotAddDuplicate(t *testing.T) { authcodec.NewBech32Codec("cosmos"), "cosmos", types.NewModuleAddress("gov").String(), + keeper.WithUnorderedTransactions(true), ) addUser := []byte("foo") diff --git a/x/auth/module.go b/x/auth/module.go index ec8d49a10249..134bb2293d8c 100644 --- a/x/auth/module.go +++ b/x/auth/module.go @@ -102,9 +102,11 @@ type AppModule struct { func (am AppModule) PreBlock(ctx context.Context) (appmodule.ResponsePreBlock, error) { start := telemetry.Now() defer telemetry.ModuleMeasureSince(types.ModuleName, start, telemetry.MetricKeyPreBlocker) - err := am.accountKeeper.RemoveExpiredUnorderedNonces(sdk.UnwrapSDKContext(ctx)) - if err != nil { - return nil, err + if am.accountKeeper.UnorderedTransactionsEnabled() { + err := am.accountKeeper.RemoveExpiredUnorderedNonces(sdk.UnwrapSDKContext(ctx)) + if err != nil { + return nil, err + } } return &sdk.ResponsePreBlock{ConsensusParamsChanged: false}, nil } @@ -247,7 +249,9 @@ func ProvideModule(in ModuleInputs) ModuleOutputs { in.AccountI = types.ProtoBaseAccount } - k := keeper.NewAccountKeeper(in.Cdc, in.StoreService, in.AccountI, maccPerms, in.AddressCodec, in.Config.Bech32Prefix, authority.String()) + keeperOpts := []keeper.InitOption{keeper.WithUnorderedTransactions(in.Config.EnableUnorderedTransactions)} + + k := keeper.NewAccountKeeper(in.Cdc, in.StoreService, in.AccountI, maccPerms, in.AddressCodec, in.Config.Bech32Prefix, authority.String(), keeperOpts...) m := NewAppModule(in.Cdc, k, in.RandomGenesisAccountsFn, in.LegacySubspace) return ModuleOutputs{AccountKeeper: k, Module: m} diff --git a/x/evidence/go.mod b/x/evidence/go.mod index ff36bed4756a..32dedf38cd02 100644 --- a/x/evidence/go.mod +++ b/x/evidence/go.mod @@ -160,4 +160,7 @@ require ( ) // Temporary replace until the next 0.53 tag -replace github.com/cosmos/cosmos-sdk => ../.. +replace ( + cosmossdk.io/api => ../../api + github.com/cosmos/cosmos-sdk => ../.. +) diff --git a/x/evidence/go.sum b/x/evidence/go.sum index 03f26c85ee2b..80a5c2410953 100644 --- a/x/evidence/go.sum +++ b/x/evidence/go.sum @@ -1,7 +1,5 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cosmossdk.io/api v0.9.0 h1:QYs9APeSlDNGbsBOBFjp3jXgGd4hnEPnnku3+W3tT4Y= -cosmossdk.io/api v0.9.0/go.mod h1:pLkU/NSqYHWxyN7XftVt8iD7oldKJzqMZgzeiOmT2nk= cosmossdk.io/collections v1.2.0 h1:IesfVG8G/+FYCMVMP01frS/Cw99Omk5vBh3cHbO01Gg= cosmossdk.io/collections v1.2.0/go.mod h1:4NkMoYw6qRA8fnSH/yn1D/MOutr8qyQnwsO50Mz9ItU= cosmossdk.io/core v0.11.3 h1:mei+MVDJOwIjIniaKelE3jPDqShCc/F4LkNNHh+4yfo= diff --git a/x/feegrant/go.mod b/x/feegrant/go.mod index 154dfa79ef98..8bda531a1c62 100644 --- a/x/feegrant/go.mod +++ b/x/feegrant/go.mod @@ -161,4 +161,7 @@ require ( ) // Temporary replace until the next 0.53 tag -replace github.com/cosmos/cosmos-sdk => ../.. +replace ( + cosmossdk.io/api => ../../api + github.com/cosmos/cosmos-sdk => ../.. +) diff --git a/x/feegrant/go.sum b/x/feegrant/go.sum index e118473cdb04..280144d167aa 100644 --- a/x/feegrant/go.sum +++ b/x/feegrant/go.sum @@ -1,7 +1,5 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cosmossdk.io/api v0.9.0 h1:QYs9APeSlDNGbsBOBFjp3jXgGd4hnEPnnku3+W3tT4Y= -cosmossdk.io/api v0.9.0/go.mod h1:pLkU/NSqYHWxyN7XftVt8iD7oldKJzqMZgzeiOmT2nk= cosmossdk.io/collections v1.2.0 h1:IesfVG8G/+FYCMVMP01frS/Cw99Omk5vBh3cHbO01Gg= cosmossdk.io/collections v1.2.0/go.mod h1:4NkMoYw6qRA8fnSH/yn1D/MOutr8qyQnwsO50Mz9ItU= cosmossdk.io/core v0.11.3 h1:mei+MVDJOwIjIniaKelE3jPDqShCc/F4LkNNHh+4yfo= diff --git a/x/nft/go.mod b/x/nft/go.mod index a53a866d6537..71c3f68d2916 100644 --- a/x/nft/go.mod +++ b/x/nft/go.mod @@ -2,7 +2,10 @@ module cosmossdk.io/x/nft go 1.23.2 -replace github.com/cosmos/cosmos-sdk => ../.. +replace ( + cosmossdk.io/api => ../../api + github.com/cosmos/cosmos-sdk => ../.. +) require ( cosmossdk.io/api v0.9.0 diff --git a/x/nft/go.sum b/x/nft/go.sum index 03f26c85ee2b..80a5c2410953 100644 --- a/x/nft/go.sum +++ b/x/nft/go.sum @@ -1,7 +1,5 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cosmossdk.io/api v0.9.0 h1:QYs9APeSlDNGbsBOBFjp3jXgGd4hnEPnnku3+W3tT4Y= -cosmossdk.io/api v0.9.0/go.mod h1:pLkU/NSqYHWxyN7XftVt8iD7oldKJzqMZgzeiOmT2nk= cosmossdk.io/collections v1.2.0 h1:IesfVG8G/+FYCMVMP01frS/Cw99Omk5vBh3cHbO01Gg= cosmossdk.io/collections v1.2.0/go.mod h1:4NkMoYw6qRA8fnSH/yn1D/MOutr8qyQnwsO50Mz9ItU= cosmossdk.io/core v0.11.3 h1:mei+MVDJOwIjIniaKelE3jPDqShCc/F4LkNNHh+4yfo=