From 22daac75f3702a75c878e8d2638524dc255e93e0 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Wed, 6 Dec 2023 10:37:06 -0700 Subject: [PATCH 01/20] init commit --- api/cosmos/tx/v1beta1/tx.pulsar.go | 296 +++++++++++------- crypto/keys/secp256k1/keys.pb.go | 8 +- .../architecture/adr-070-unordered-account.md | 2 +- proto/cosmos/tx/v1beta1/tx.proto | 24 +- types/tx/tx.pb.go | 200 +++++++----- 5 files changed, 330 insertions(+), 200 deletions(-) diff --git a/api/cosmos/tx/v1beta1/tx.pulsar.go b/api/cosmos/tx/v1beta1/tx.pulsar.go index 362609bcfc0f..2c2b63638401 100644 --- a/api/cosmos/tx/v1beta1/tx.pulsar.go +++ b/api/cosmos/tx/v1beta1/tx.pulsar.go @@ -2767,6 +2767,7 @@ var ( fd_TxBody_messages protoreflect.FieldDescriptor fd_TxBody_memo protoreflect.FieldDescriptor fd_TxBody_timeout_height protoreflect.FieldDescriptor + fd_TxBody_unordered protoreflect.FieldDescriptor fd_TxBody_extension_options protoreflect.FieldDescriptor fd_TxBody_non_critical_extension_options protoreflect.FieldDescriptor ) @@ -2777,6 +2778,7 @@ func init() { fd_TxBody_messages = md_TxBody.Fields().ByName("messages") fd_TxBody_memo = md_TxBody.Fields().ByName("memo") fd_TxBody_timeout_height = md_TxBody.Fields().ByName("timeout_height") + fd_TxBody_unordered = md_TxBody.Fields().ByName("unordered") fd_TxBody_extension_options = md_TxBody.Fields().ByName("extension_options") fd_TxBody_non_critical_extension_options = md_TxBody.Fields().ByName("non_critical_extension_options") } @@ -2864,6 +2866,12 @@ func (x *fastReflection_TxBody) Range(f func(protoreflect.FieldDescriptor, proto return } } + if x.Unordered != false { + value := protoreflect.ValueOfBool(x.Unordered) + if !f(fd_TxBody_unordered, value) { + return + } + } if len(x.ExtensionOptions) != 0 { value := protoreflect.ValueOfList(&_TxBody_1023_list{list: &x.ExtensionOptions}) if !f(fd_TxBody_extension_options, value) { @@ -2897,6 +2905,8 @@ func (x *fastReflection_TxBody) Has(fd protoreflect.FieldDescriptor) bool { return x.Memo != "" case "cosmos.tx.v1beta1.TxBody.timeout_height": return x.TimeoutHeight != uint64(0) + case "cosmos.tx.v1beta1.TxBody.unordered": + return x.Unordered != false case "cosmos.tx.v1beta1.TxBody.extension_options": return len(x.ExtensionOptions) != 0 case "cosmos.tx.v1beta1.TxBody.non_critical_extension_options": @@ -2923,6 +2933,8 @@ func (x *fastReflection_TxBody) Clear(fd protoreflect.FieldDescriptor) { x.Memo = "" case "cosmos.tx.v1beta1.TxBody.timeout_height": x.TimeoutHeight = uint64(0) + case "cosmos.tx.v1beta1.TxBody.unordered": + x.Unordered = false case "cosmos.tx.v1beta1.TxBody.extension_options": x.ExtensionOptions = nil case "cosmos.tx.v1beta1.TxBody.non_critical_extension_options": @@ -2955,6 +2967,9 @@ func (x *fastReflection_TxBody) Get(descriptor protoreflect.FieldDescriptor) pro case "cosmos.tx.v1beta1.TxBody.timeout_height": value := x.TimeoutHeight return protoreflect.ValueOfUint64(value) + case "cosmos.tx.v1beta1.TxBody.unordered": + value := x.Unordered + return protoreflect.ValueOfBool(value) case "cosmos.tx.v1beta1.TxBody.extension_options": if len(x.ExtensionOptions) == 0 { return protoreflect.ValueOfList(&_TxBody_1023_list{}) @@ -2995,6 +3010,8 @@ func (x *fastReflection_TxBody) Set(fd protoreflect.FieldDescriptor, value proto x.Memo = value.Interface().(string) case "cosmos.tx.v1beta1.TxBody.timeout_height": x.TimeoutHeight = value.Uint() + case "cosmos.tx.v1beta1.TxBody.unordered": + x.Unordered = value.Bool() case "cosmos.tx.v1beta1.TxBody.extension_options": lv := value.List() clv := lv.(*_TxBody_1023_list) @@ -3045,6 +3062,8 @@ func (x *fastReflection_TxBody) Mutable(fd protoreflect.FieldDescriptor) protore panic(fmt.Errorf("field memo of message cosmos.tx.v1beta1.TxBody is not mutable")) case "cosmos.tx.v1beta1.TxBody.timeout_height": panic(fmt.Errorf("field timeout_height of message cosmos.tx.v1beta1.TxBody is not mutable")) + case "cosmos.tx.v1beta1.TxBody.unordered": + panic(fmt.Errorf("field unordered of message cosmos.tx.v1beta1.TxBody is not mutable")) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.tx.v1beta1.TxBody")) @@ -3065,6 +3084,8 @@ func (x *fastReflection_TxBody) NewField(fd protoreflect.FieldDescriptor) protor return protoreflect.ValueOfString("") case "cosmos.tx.v1beta1.TxBody.timeout_height": return protoreflect.ValueOfUint64(uint64(0)) + case "cosmos.tx.v1beta1.TxBody.unordered": + return protoreflect.ValueOfBool(false) case "cosmos.tx.v1beta1.TxBody.extension_options": list := []*anypb.Any{} return protoreflect.ValueOfList(&_TxBody_1023_list{list: &list}) @@ -3153,6 +3174,9 @@ func (x *fastReflection_TxBody) ProtoMethods() *protoiface.Methods { if x.TimeoutHeight != 0 { n += 1 + runtime.Sov(uint64(x.TimeoutHeight)) } + if x.Unordered { + n += 2 + } if len(x.ExtensionOptions) > 0 { for _, e := range x.ExtensionOptions { l = options.Size(e) @@ -3230,6 +3254,16 @@ func (x *fastReflection_TxBody) ProtoMethods() *protoiface.Methods { dAtA[i] = 0xfa } } + if x.Unordered { + i-- + if x.Unordered { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x20 + } if x.TimeoutHeight != 0 { i = runtime.EncodeVarint(dAtA, i, uint64(x.TimeoutHeight)) i-- @@ -3392,6 +3426,26 @@ func (x *fastReflection_TxBody) ProtoMethods() *protoiface.Methods { break } } + case 4: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Unordered", 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.Unordered = bool(v != 0) case 1023: if wireType != 2 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ExtensionOptions", wireType) @@ -8414,11 +8468,18 @@ type TxBody struct { Messages []*anypb.Any `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` // memo is any arbitrary note/comment to be added to the transaction. // WARNING: in clients, any publicly exposed text should not be called memo, - // but should be called `note` instead (see https://github.com/cosmos/cosmos-sdk/issues/9122). + // but should be called `note` instead (see + // https://github.com/cosmos/cosmos-sdk/issues/9122). Memo string `protobuf:"bytes,2,opt,name=memo,proto3" json:"memo,omitempty"` // timeout is the block height after which this transaction will not // be processed by the chain TimeoutHeight uint64 `protobuf:"varint,3,opt,name=timeout_height,json=timeoutHeight,proto3" json:"timeout_height,omitempty"` + // unordered, when set to true, indicates that the transaction signer(s) + // intend for the transaction to be evaluated and executed in an un-ordered + // fashion. Specifically, the account's nonce will NOT be checked or + // incremented, which allows for fire-and-forget as well as concurrent + // transaction execution. + Unordered bool `protobuf:"varint,4,opt,name=unordered,proto3" json:"unordered,omitempty"` // extension_options are arbitrary options that can be added by chains // when the default options are not sufficient. If any of these are present // and can't be handled, the transaction will be rejected @@ -8470,6 +8531,13 @@ func (x *TxBody) GetTimeoutHeight() uint64 { return 0 } +func (x *TxBody) GetUnordered() bool { + if x != nil { + return x.Unordered + } + return false +} + func (x *TxBody) GetExtensionOptions() []*anypb.Any { if x != nil { return x.ExtensionOptions @@ -8703,13 +8771,15 @@ type Fee struct { // gas_limit is the maximum gas that can be used in transaction processing // before an out of gas error occurs GasLimit uint64 `protobuf:"varint,2,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty"` - // if unset, the first signer is responsible for paying the fees. If set, the specified account must pay the fees. - // the payer must be a tx signer (and thus have signed this field in AuthInfo). - // setting this field does *not* change the ordering of required signers for the transaction. + // if unset, the first signer is responsible for paying the fees. If set, the + // specified account must pay the fees. the payer must be a tx signer (and + // thus have signed this field in AuthInfo). setting this field does *not* + // change the ordering of required signers for the transaction. Payer string `protobuf:"bytes,3,opt,name=payer,proto3" json:"payer,omitempty"` - // if set, the fee payer (either the first signer or the value of the payer field) requests that a fee grant be used - // to pay fees instead of the fee payer's own balance. If an appropriate fee grant does not exist or the chain does - // not support fee grants, this will fail + // if set, the fee payer (either the first signer or the value of the payer + // field) requests that a fee grant be used to pay fees instead of the fee + // payer's own balance. If an appropriate fee grant does not exist or the + // chain does not support fee grants, this will fail Granter string `protobuf:"bytes,4,opt,name=granter,proto3" json:"granter,omitempty"` } @@ -9030,119 +9100,121 @@ var file_cosmos_tx_v1beta1_tx_proto_rawDesc = []byte{ 0x63, 0x65, 0x12, 0x2c, 0x0a, 0x03, 0x74, 0x69, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x54, 0x69, 0x70, 0x42, 0x02, 0x18, 0x01, 0x52, 0x03, 0x74, 0x69, 0x70, - 0x22, 0x95, 0x02, 0x0a, 0x06, 0x54, 0x78, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x30, 0x0a, 0x08, 0x6d, + 0x22, 0xb3, 0x02, 0x0a, 0x06, 0x54, 0x78, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x30, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x74, 0x69, 0x6d, 0x65, 0x6f, - 0x75, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x42, 0x0a, 0x11, 0x65, 0x78, 0x74, 0x65, - 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xff, 0x07, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x10, 0x65, 0x78, 0x74, 0x65, - 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5a, 0x0a, 0x1e, - 0x6e, 0x6f, 0x6e, 0x5f, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x65, 0x78, 0x74, - 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xff, - 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x1b, 0x6e, 0x6f, 0x6e, - 0x43, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, - 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xa4, 0x01, 0x0a, 0x08, 0x41, 0x75, 0x74, - 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x40, 0x0a, 0x0c, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x5f, - 0x69, 0x6e, 0x66, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, - 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, - 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, - 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x73, 0x12, 0x28, 0x0a, 0x03, 0x66, 0x65, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, - 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x46, 0x65, 0x65, 0x52, 0x03, 0x66, 0x65, - 0x65, 0x12, 0x2c, 0x0a, 0x03, 0x74, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, - 0x61, 0x31, 0x2e, 0x54, 0x69, 0x70, 0x42, 0x02, 0x18, 0x01, 0x52, 0x03, 0x74, 0x69, 0x70, 0x22, - 0x97, 0x01, 0x0a, 0x0a, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x33, - 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x75, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x6e, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x75, 0x6e, 0x6f, + 0x72, 0x64, 0x65, 0x72, 0x65, 0x64, 0x12, 0x42, 0x0a, 0x11, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, + 0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xff, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x4b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x6d, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, - 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x6d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, - 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x22, 0xe0, 0x02, 0x0a, 0x08, 0x4d, 0x6f, - 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, - 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49, - 0x6e, 0x66, 0x6f, 0x2e, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x48, 0x00, 0x52, 0x06, 0x73, 0x69, - 0x6e, 0x67, 0x6c, 0x65, 0x12, 0x39, 0x0a, 0x05, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, - 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, - 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x48, 0x00, 0x52, 0x05, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x1a, - 0x41, 0x0a, 0x06, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x12, 0x37, 0x0a, 0x04, 0x6d, 0x6f, 0x64, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, - 0x2e, 0x74, 0x78, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x62, 0x65, - 0x74, 0x61, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, - 0x64, 0x65, 0x1a, 0x90, 0x01, 0x0a, 0x05, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x12, 0x4b, 0x0a, 0x08, - 0x62, 0x69, 0x74, 0x61, 0x72, 0x72, 0x61, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, - 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x6d, - 0x75, 0x6c, 0x74, 0x69, 0x73, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, - 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x42, 0x69, 0x74, 0x41, 0x72, 0x72, 0x61, 0x79, 0x52, - 0x08, 0x62, 0x69, 0x74, 0x61, 0x72, 0x72, 0x61, 0x79, 0x12, 0x3a, 0x0a, 0x0a, 0x6d, 0x6f, 0x64, - 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, - 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, - 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x09, 0x6d, 0x6f, 0x64, 0x65, - 0x49, 0x6e, 0x66, 0x6f, 0x73, 0x42, 0x05, 0x0a, 0x03, 0x73, 0x75, 0x6d, 0x22, 0x81, 0x02, 0x0a, - 0x03, 0x46, 0x65, 0x65, 0x12, 0x79, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x62, 0x61, - 0x73, 0x65, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x6f, 0x69, 0x6e, 0x42, - 0x46, 0xc8, 0xde, 0x1f, 0x00, 0xaa, 0xdf, 0x1f, 0x28, 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, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x43, 0x6f, 0x69, 0x6e, - 0x73, 0x9a, 0xe7, 0xb0, 0x2a, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x63, 0x6f, 0x69, - 0x6e, 0x73, 0xa8, 0xe7, 0xb0, 0x2a, 0x01, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x1b, 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2e, 0x0a, 0x05, - 0x70, 0x61, 0x79, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x10, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, + 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5a, 0x0a, 0x1e, 0x6e, 0x6f, + 0x6e, 0x5f, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xff, 0x0f, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x1b, 0x6e, 0x6f, 0x6e, 0x43, 0x72, + 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x4f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xa4, 0x01, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x40, 0x0a, 0x0c, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x6e, + 0x66, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x73, 0x6d, + 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x69, + 0x67, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, + 0x49, 0x6e, 0x66, 0x6f, 0x73, 0x12, 0x28, 0x0a, 0x03, 0x66, 0x65, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, + 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x46, 0x65, 0x65, 0x52, 0x03, 0x66, 0x65, 0x65, 0x12, + 0x2c, 0x0a, 0x03, 0x74, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, + 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, + 0x2e, 0x54, 0x69, 0x70, 0x42, 0x02, 0x18, 0x01, 0x52, 0x03, 0x74, 0x69, 0x70, 0x22, 0x97, 0x01, + 0x0a, 0x0a, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x33, 0x0a, 0x0a, + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, + 0x79, 0x12, 0x38, 0x0a, 0x09, 0x6d, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, + 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, + 0x6f, 0x52, 0x08, 0x6d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x73, + 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, + 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x22, 0xe0, 0x02, 0x0a, 0x08, 0x4d, 0x6f, 0x64, 0x65, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, + 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, + 0x6f, 0x2e, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x48, 0x00, 0x52, 0x06, 0x73, 0x69, 0x6e, 0x67, + 0x6c, 0x65, 0x12, 0x39, 0x0a, 0x05, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, + 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x4d, + 0x75, 0x6c, 0x74, 0x69, 0x48, 0x00, 0x52, 0x05, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x1a, 0x41, 0x0a, + 0x06, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x12, 0x37, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, + 0x78, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, + 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, + 0x1a, 0x90, 0x01, 0x0a, 0x05, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x12, 0x4b, 0x0a, 0x08, 0x62, 0x69, + 0x74, 0x61, 0x72, 0x72, 0x61, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, + 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x6d, 0x75, 0x6c, + 0x74, 0x69, 0x73, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x6f, + 0x6d, 0x70, 0x61, 0x63, 0x74, 0x42, 0x69, 0x74, 0x41, 0x72, 0x72, 0x61, 0x79, 0x52, 0x08, 0x62, + 0x69, 0x74, 0x61, 0x72, 0x72, 0x61, 0x79, 0x12, 0x3a, 0x0a, 0x0a, 0x6d, 0x6f, 0x64, 0x65, 0x5f, + 0x69, 0x6e, 0x66, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, + 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, + 0x4d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x09, 0x6d, 0x6f, 0x64, 0x65, 0x49, 0x6e, + 0x66, 0x6f, 0x73, 0x42, 0x05, 0x0a, 0x03, 0x73, 0x75, 0x6d, 0x22, 0x81, 0x02, 0x0a, 0x03, 0x46, + 0x65, 0x65, 0x12, 0x79, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x62, 0x61, 0x73, 0x65, + 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x6f, 0x69, 0x6e, 0x42, 0x46, 0xc8, + 0xde, 0x1f, 0x00, 0xaa, 0xdf, 0x1f, 0x28, 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, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x9a, + 0xe7, 0xb0, 0x2a, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, + 0xa8, 0xe7, 0xb0, 0x2a, 0x01, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, + 0x09, 0x67, 0x61, 0x73, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x70, 0x61, + 0x79, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, 0x63, + 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x52, 0x05, 0x70, 0x61, 0x79, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x07, 0x67, 0x72, + 0x61, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, - 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x05, 0x70, 0x61, 0x79, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x07, - 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, - 0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x72, - 0x22, 0xb6, 0x01, 0x0a, 0x03, 0x54, 0x69, 0x70, 0x12, 0x79, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, - 0x73, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, - 0x6f, 0x69, 0x6e, 0x42, 0x46, 0xc8, 0xde, 0x1f, 0x00, 0xaa, 0xdf, 0x1f, 0x28, 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, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, - 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x9a, 0xe7, 0xb0, 0x2a, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, - 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0xa8, 0xe7, 0xb0, 0x2a, 0x01, 0x52, 0x06, 0x61, 0x6d, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x30, 0x0a, 0x06, 0x74, 0x69, 0x70, 0x70, 0x65, 0x72, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x06, 0x74, - 0x69, 0x70, 0x70, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0xce, 0x01, 0x0a, 0x0d, 0x41, 0x75, - 0x78, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x32, 0x0a, 0x07, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, - 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, - 0x3e, 0x0a, 0x08, 0x73, 0x69, 0x67, 0x6e, 0x5f, 0x64, 0x6f, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, - 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x44, 0x6f, 0x63, 0x44, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x41, 0x75, 0x78, 0x52, 0x07, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x6f, 0x63, 0x12, - 0x37, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, - 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, - 0x67, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x6f, - 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x69, 0x67, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x73, 0x69, 0x67, 0x42, 0xb4, 0x01, 0x0a, 0x15, 0x63, - 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, - 0x65, 0x74, 0x61, 0x31, 0x42, 0x07, 0x54, 0x78, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, - 0x2c, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x74, 0x78, 0x2f, 0x76, 0x31, 0x62, 0x65, - 0x74, 0x61, 0x31, 0x3b, 0x74, 0x78, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0xa2, 0x02, 0x03, - 0x43, 0x54, 0x58, 0xaa, 0x02, 0x11, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x54, 0x78, 0x2e, - 0x56, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0xca, 0x02, 0x11, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, - 0x5c, 0x54, 0x78, 0x5c, 0x56, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0xe2, 0x02, 0x1d, 0x43, 0x6f, - 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x54, 0x78, 0x5c, 0x56, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x5c, - 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x13, 0x43, 0x6f, - 0x73, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x54, 0x78, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x65, 0x74, 0x61, - 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x72, 0x22, 0xb6, + 0x01, 0x0a, 0x03, 0x54, 0x69, 0x70, 0x12, 0x79, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, + 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x6f, 0x69, + 0x6e, 0x42, 0x46, 0xc8, 0xde, 0x1f, 0x00, 0xaa, 0xdf, 0x1f, 0x28, 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, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x43, 0x6f, + 0x69, 0x6e, 0x73, 0x9a, 0xe7, 0xb0, 0x2a, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x63, + 0x6f, 0x69, 0x6e, 0x73, 0xa8, 0xe7, 0xb0, 0x2a, 0x01, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x30, 0x0a, 0x06, 0x74, 0x69, 0x70, 0x70, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x06, 0x74, 0x69, 0x70, + 0x70, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0xce, 0x01, 0x0a, 0x0d, 0x41, 0x75, 0x78, 0x53, + 0x69, 0x67, 0x6e, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x32, 0x0a, 0x07, 0x61, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, + 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3e, 0x0a, + 0x08, 0x73, 0x69, 0x67, 0x6e, 0x5f, 0x64, 0x6f, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x23, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, + 0x74, 0x61, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x44, 0x6f, 0x63, 0x44, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x41, 0x75, 0x78, 0x52, 0x07, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x6f, 0x63, 0x12, 0x37, 0x0a, + 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x63, 0x6f, + 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x2e, + 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x6f, 0x64, 0x65, + 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x69, 0x67, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x03, 0x73, 0x69, 0x67, 0x42, 0xb4, 0x01, 0x0a, 0x15, 0x63, 0x6f, 0x6d, + 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, + 0x61, 0x31, 0x42, 0x07, 0x54, 0x78, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2c, 0x63, + 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x74, 0x78, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, + 0x31, 0x3b, 0x74, 0x78, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x54, + 0x58, 0xaa, 0x02, 0x11, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x54, 0x78, 0x2e, 0x56, 0x31, + 0x62, 0x65, 0x74, 0x61, 0x31, 0xca, 0x02, 0x11, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x54, + 0x78, 0x5c, 0x56, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0xe2, 0x02, 0x1d, 0x43, 0x6f, 0x73, 0x6d, + 0x6f, 0x73, 0x5c, 0x54, 0x78, 0x5c, 0x56, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x5c, 0x47, 0x50, + 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x13, 0x43, 0x6f, 0x73, 0x6d, + 0x6f, 0x73, 0x3a, 0x3a, 0x54, 0x78, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/crypto/keys/secp256k1/keys.pb.go b/crypto/keys/secp256k1/keys.pb.go index 283455d0aa75..24ab774e36d4 100644 --- a/crypto/keys/secp256k1/keys.pb.go +++ b/crypto/keys/secp256k1/keys.pb.go @@ -5,14 +5,12 @@ package secp256k1 import ( fmt "fmt" + _ "github.com/cosmos/cosmos-sdk/types/tx/amino" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" io "io" math "math" math_bits "math/bits" - - _ "github.com/cosmos/gogoproto/gogoproto" - proto "github.com/cosmos/gogoproto/proto" - - _ "github.com/cosmos/cosmos-sdk/types/tx/amino" ) // Reference imports to suppress errors if they are not otherwise used. diff --git a/docs/architecture/adr-070-unordered-account.md b/docs/architecture/adr-070-unordered-account.md index 814193cef668..53477fe973c4 100644 --- a/docs/architecture/adr-070-unordered-account.md +++ b/docs/architecture/adr-070-unordered-account.md @@ -2,7 +2,7 @@ ## Changelog -* Dec 4, 2023: Initial Draft +* Dec 4, 2023: Initial Draft (@yihuang, @alexanderbez) ## Status diff --git a/proto/cosmos/tx/v1beta1/tx.proto b/proto/cosmos/tx/v1beta1/tx.proto index 4d8b290bddd7..8e6e375cc8c9 100644 --- a/proto/cosmos/tx/v1beta1/tx.proto +++ b/proto/cosmos/tx/v1beta1/tx.proto @@ -105,13 +105,21 @@ message TxBody { // memo is any arbitrary note/comment to be added to the transaction. // WARNING: in clients, any publicly exposed text should not be called memo, - // but should be called `note` instead (see https://github.com/cosmos/cosmos-sdk/issues/9122). + // but should be called `note` instead (see + // https://github.com/cosmos/cosmos-sdk/issues/9122). string memo = 2; // timeout is the block height after which this transaction will not // be processed by the chain uint64 timeout_height = 3; + // unordered, when set to true, indicates that the transaction signer(s) + // intend for the transaction to be evaluated and executed in an un-ordered + // fashion. Specifically, the account's nonce will NOT be checked or + // incremented, which allows for fire-and-forget as well as concurrent + // transaction execution. + bool unordered = 4; + // extension_options are arbitrary options that can be added by chains // when the default options are not sufficient. If any of these are present // and can't be handled, the transaction will be rejected @@ -211,14 +219,16 @@ message Fee { // before an out of gas error occurs uint64 gas_limit = 2; - // if unset, the first signer is responsible for paying the fees. If set, the specified account must pay the fees. - // the payer must be a tx signer (and thus have signed this field in AuthInfo). - // setting this field does *not* change the ordering of required signers for the transaction. + // if unset, the first signer is responsible for paying the fees. If set, the + // specified account must pay the fees. the payer must be a tx signer (and + // thus have signed this field in AuthInfo). setting this field does *not* + // change the ordering of required signers for the transaction. string payer = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"]; - // if set, the fee payer (either the first signer or the value of the payer field) requests that a fee grant be used - // to pay fees instead of the fee payer's own balance. If an appropriate fee grant does not exist or the chain does - // not support fee grants, this will fail + // if set, the fee payer (either the first signer or the value of the payer + // field) requests that a fee grant be used to pay fees instead of the fee + // payer's own balance. If an appropriate fee grant does not exist or the + // chain does not support fee grants, this will fail string granter = 4 [(cosmos_proto.scalar) = "cosmos.AddressString"]; } diff --git a/types/tx/tx.pb.go b/types/tx/tx.pb.go index c6fbf063309e..32b3e3fa2ee0 100644 --- a/types/tx/tx.pb.go +++ b/types/tx/tx.pb.go @@ -356,11 +356,18 @@ type TxBody struct { Messages []*types.Any `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` // memo is any arbitrary note/comment to be added to the transaction. // WARNING: in clients, any publicly exposed text should not be called memo, - // but should be called `note` instead (see https://github.com/cosmos/cosmos-sdk/issues/9122). + // but should be called `note` instead (see + // https://github.com/cosmos/cosmos-sdk/issues/9122). Memo string `protobuf:"bytes,2,opt,name=memo,proto3" json:"memo,omitempty"` // timeout is the block height after which this transaction will not // be processed by the chain TimeoutHeight uint64 `protobuf:"varint,3,opt,name=timeout_height,json=timeoutHeight,proto3" json:"timeout_height,omitempty"` + // unordered, when set to true, indicates that the transaction signer(s) + // intend for the transaction to be evaluated and executed in an un-ordered + // fashion. Specifically, the account's nonce will NOT be checked or + // incremented, which allows for fire-and-forget as well as concurrent + // transaction execution. + Unordered bool `protobuf:"varint,4,opt,name=unordered,proto3" json:"unordered,omitempty"` // extension_options are arbitrary options that can be added by chains // when the default options are not sufficient. If any of these are present // and can't be handled, the transaction will be rejected @@ -425,6 +432,13 @@ func (m *TxBody) GetTimeoutHeight() uint64 { return 0 } +func (m *TxBody) GetUnordered() bool { + if m != nil { + return m.Unordered + } + return false +} + func (m *TxBody) GetExtensionOptions() []*types.Any { if m != nil { return m.ExtensionOptions @@ -789,13 +803,15 @@ type Fee struct { // gas_limit is the maximum gas that can be used in transaction processing // before an out of gas error occurs GasLimit uint64 `protobuf:"varint,2,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty"` - // if unset, the first signer is responsible for paying the fees. If set, the specified account must pay the fees. - // the payer must be a tx signer (and thus have signed this field in AuthInfo). - // setting this field does *not* change the ordering of required signers for the transaction. + // if unset, the first signer is responsible for paying the fees. If set, the + // specified account must pay the fees. the payer must be a tx signer (and + // thus have signed this field in AuthInfo). setting this field does *not* + // change the ordering of required signers for the transaction. Payer string `protobuf:"bytes,3,opt,name=payer,proto3" json:"payer,omitempty"` - // if set, the fee payer (either the first signer or the value of the payer field) requests that a fee grant be used - // to pay fees instead of the fee payer's own balance. If an appropriate fee grant does not exist or the chain does - // not support fee grants, this will fail + // if set, the fee payer (either the first signer or the value of the payer + // field) requests that a fee grant be used to pay fees instead of the fee + // payer's own balance. If an appropriate fee grant does not exist or the + // chain does not support fee grants, this will fail Granter string `protobuf:"bytes,4,opt,name=granter,proto3" json:"granter,omitempty"` } @@ -1020,74 +1036,75 @@ func init() { func init() { proto.RegisterFile("cosmos/tx/v1beta1/tx.proto", fileDescriptor_96d1575ffde80842) } var fileDescriptor_96d1575ffde80842 = []byte{ - // 1059 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x55, 0x41, 0x6f, 0x1b, 0x45, - 0x14, 0xf6, 0x7a, 0x6d, 0xc7, 0x7e, 0x4d, 0xda, 0x64, 0x14, 0x21, 0xc7, 0x51, 0xdd, 0xe0, 0xaa, - 0x60, 0x55, 0x64, 0xb7, 0x4d, 0x0f, 0x94, 0x0a, 0x01, 0x76, 0x43, 0x94, 0xaa, 0x14, 0xa4, 0x4d, - 0x4e, 0xbd, 0xac, 0xc6, 0xeb, 0xc9, 0x7a, 0x54, 0xef, 0xcc, 0xb2, 0x33, 0x0b, 0xde, 0x23, 0x3f, - 0x00, 0xa9, 0x42, 0x42, 0x48, 0x9c, 0x39, 0x20, 0x4e, 0x3d, 0x20, 0x7e, 0x43, 0x4f, 0xa8, 0xe2, - 0xc4, 0x09, 0xaa, 0xe4, 0xd0, 0x3b, 0x7f, 0x00, 0x34, 0xb3, 0xb3, 0x9b, 0xb4, 0xa4, 0x4e, 0x11, - 0x48, 0x5c, 0xec, 0x99, 0xb7, 0xdf, 0x7b, 0xf3, 0xbd, 0x37, 0xdf, 0xbc, 0x07, 0x9d, 0x80, 0x8b, - 0x88, 0x0b, 0x57, 0xce, 0xdc, 0xcf, 0xae, 0x8f, 0x88, 0xc4, 0xd7, 0x5d, 0x39, 0x73, 0xe2, 0x84, - 0x4b, 0x8e, 0x56, 0xf2, 0x6f, 0x8e, 0x9c, 0x39, 0xe6, 0x5b, 0x67, 0x05, 0x47, 0x94, 0x71, 0x57, - 0xff, 0xe6, 0xa8, 0xce, 0x6a, 0xc8, 0x43, 0xae, 0x97, 0xae, 0x5a, 0x19, 0xeb, 0xa6, 0x89, 0x1b, - 0x24, 0x59, 0x2c, 0xb9, 0x1b, 0xa5, 0x53, 0x49, 0x05, 0x0d, 0xcb, 0x43, 0x0a, 0x83, 0x81, 0x77, - 0x0d, 0x7c, 0x84, 0x05, 0x29, 0x31, 0x01, 0xa7, 0xcc, 0x7c, 0x7f, 0xf3, 0x98, 0xa6, 0xa0, 0x21, - 0xa3, 0xec, 0x38, 0x92, 0xd9, 0x1b, 0xe0, 0x5a, 0xc8, 0x79, 0x38, 0x25, 0xae, 0xde, 0x8d, 0xd2, - 0x03, 0x17, 0xb3, 0xac, 0xf8, 0x94, 0xc7, 0xf0, 0x73, 0xae, 0x26, 0x37, 0xbd, 0xe9, 0x7d, 0x69, - 0x41, 0x75, 0x7f, 0x86, 0x36, 0xa1, 0x36, 0xe2, 0xe3, 0xac, 0x6d, 0x6d, 0x58, 0xfd, 0x73, 0x5b, - 0x6b, 0xce, 0xdf, 0xf2, 0x77, 0xf6, 0x67, 0x43, 0x3e, 0xce, 0x3c, 0x0d, 0x43, 0x37, 0xa1, 0x85, - 0x53, 0x39, 0xf1, 0x29, 0x3b, 0xe0, 0xed, 0xaa, 0xf6, 0x59, 0x3f, 0xc5, 0x67, 0x90, 0xca, 0xc9, - 0x1d, 0x76, 0xc0, 0xbd, 0x26, 0x36, 0x2b, 0xd4, 0x05, 0x50, 0xb4, 0xb1, 0x4c, 0x13, 0x22, 0xda, - 0xf6, 0x86, 0xdd, 0x5f, 0xf4, 0x4e, 0x58, 0x7a, 0x0c, 0xea, 0xfb, 0x33, 0x0f, 0x7f, 0x8e, 0x2e, - 0x02, 0xa8, 0xa3, 0xfc, 0x51, 0x26, 0x89, 0xd0, 0xbc, 0x16, 0xbd, 0x96, 0xb2, 0x0c, 0x95, 0x01, - 0xbd, 0x01, 0x17, 0x4a, 0x06, 0x06, 0x53, 0xd5, 0x98, 0xa5, 0xe2, 0xa8, 0x1c, 0x77, 0xd6, 0x79, - 0x5f, 0x59, 0xb0, 0xb0, 0x47, 0x43, 0xb6, 0xcd, 0x83, 0xff, 0xea, 0xc8, 0x35, 0x68, 0x06, 0x13, - 0x4c, 0x99, 0x4f, 0xc7, 0x6d, 0x7b, 0xc3, 0xea, 0xb7, 0xbc, 0x05, 0xbd, 0xbf, 0x33, 0x46, 0x57, - 0xe0, 0x3c, 0x0e, 0x02, 0x9e, 0x32, 0xe9, 0xb3, 0x34, 0x1a, 0x91, 0xa4, 0x5d, 0xdb, 0xb0, 0xfa, - 0x35, 0x6f, 0xc9, 0x58, 0x3f, 0xd6, 0xc6, 0xde, 0x1f, 0x16, 0x2c, 0x1b, 0x52, 0xdb, 0x34, 0x21, - 0x81, 0x1c, 0xa4, 0xb3, 0xb3, 0xd8, 0xdd, 0x00, 0x88, 0xd3, 0xd1, 0x94, 0x06, 0xfe, 0x03, 0x92, - 0x99, 0x3b, 0x59, 0x75, 0x72, 0x4d, 0x38, 0x85, 0x26, 0x9c, 0x01, 0xcb, 0xbc, 0x56, 0x8e, 0xbb, - 0x4b, 0xb2, 0x7f, 0x4f, 0x15, 0x75, 0xa0, 0x29, 0xc8, 0xa7, 0x29, 0x61, 0x01, 0x69, 0xd7, 0x35, - 0xa0, 0xdc, 0xa3, 0xb7, 0xc0, 0x96, 0x34, 0x6e, 0x37, 0x34, 0x97, 0xd7, 0x4e, 0xd3, 0x14, 0x8d, - 0x87, 0xd5, 0xb6, 0xe5, 0x29, 0x58, 0xef, 0xeb, 0x2a, 0x34, 0x72, 0x91, 0xa1, 0x6b, 0xd0, 0x8c, - 0x88, 0x10, 0x38, 0xd4, 0x89, 0xda, 0x2f, 0xcd, 0xa4, 0x44, 0x21, 0x04, 0xb5, 0x88, 0x44, 0xb9, - 0x16, 0x5b, 0x9e, 0x5e, 0xab, 0x0c, 0x24, 0x8d, 0x08, 0x4f, 0xa5, 0x3f, 0x21, 0x34, 0x9c, 0x48, - 0x9d, 0x62, 0xcd, 0x5b, 0x32, 0xd6, 0x5d, 0x6d, 0x44, 0x43, 0x58, 0x21, 0x33, 0x49, 0x98, 0xa0, - 0x9c, 0xf9, 0x3c, 0x96, 0x94, 0x33, 0xd1, 0xfe, 0x73, 0x61, 0xce, 0xb1, 0xcb, 0x25, 0xfe, 0x93, - 0x1c, 0x8e, 0xee, 0x43, 0x97, 0x71, 0xe6, 0x07, 0x09, 0x95, 0x34, 0xc0, 0x53, 0xff, 0x94, 0x80, - 0x17, 0xe6, 0x04, 0x5c, 0x67, 0x9c, 0xdd, 0x36, 0xbe, 0x1f, 0xbe, 0x10, 0xbb, 0xf7, 0x9d, 0x05, - 0xcd, 0xe2, 0x21, 0xa1, 0x0f, 0x60, 0x51, 0x89, 0x97, 0x24, 0x5a, 0x85, 0x45, 0x75, 0x2e, 0x9e, - 0x52, 0xdb, 0x3d, 0x0d, 0xd3, 0xaf, 0xef, 0x9c, 0x28, 0xd7, 0x02, 0xf5, 0xc1, 0x3e, 0x20, 0xc4, - 0x08, 0xe4, 0xb4, 0x4b, 0xd9, 0x21, 0xc4, 0x53, 0x90, 0xe2, 0xfa, 0xec, 0x57, 0xbb, 0xbe, 0x6f, - 0x2c, 0x80, 0xe3, 0x33, 0x5f, 0x90, 0xa3, 0xf5, 0x6a, 0x72, 0xbc, 0x09, 0xad, 0x88, 0x8f, 0xc9, - 0x59, 0x6d, 0xe5, 0x1e, 0x1f, 0x93, 0xbc, 0xad, 0x44, 0x66, 0xf5, 0x9c, 0x0c, 0xed, 0xe7, 0x65, - 0xd8, 0x7b, 0x5a, 0x85, 0x66, 0xe1, 0x82, 0xde, 0x85, 0x86, 0xa0, 0x2c, 0x9c, 0x12, 0xc3, 0xa9, - 0x37, 0x27, 0xbe, 0xb3, 0xa7, 0x91, 0xbb, 0x15, 0xcf, 0xf8, 0xa0, 0x77, 0xa0, 0xae, 0xdb, 0xb7, - 0x21, 0xf7, 0xfa, 0x3c, 0xe7, 0x7b, 0x0a, 0xb8, 0x5b, 0xf1, 0x72, 0x8f, 0xce, 0x00, 0x1a, 0x79, - 0x38, 0xf4, 0x36, 0xd4, 0x14, 0x6f, 0x4d, 0xe0, 0xfc, 0xd6, 0xe5, 0x13, 0x31, 0x8a, 0x86, 0x7e, - 0xf2, 0x0e, 0x55, 0x3c, 0x4f, 0x3b, 0x74, 0x1e, 0x5a, 0x50, 0xd7, 0x51, 0xd1, 0x5d, 0x68, 0x8e, - 0xa8, 0xc4, 0x49, 0x82, 0x8b, 0xda, 0xba, 0x45, 0x98, 0x7c, 0xec, 0x38, 0xe5, 0x94, 0x29, 0x62, - 0xdd, 0xe6, 0x51, 0x8c, 0x03, 0x39, 0xa4, 0x72, 0xa0, 0xdc, 0xbc, 0x32, 0x00, 0xba, 0x05, 0x50, - 0x56, 0x5d, 0xb5, 0x34, 0xfb, 0xac, 0xb2, 0xb7, 0x8a, 0xb2, 0x8b, 0x61, 0x1d, 0x6c, 0x91, 0x46, - 0xbd, 0x2f, 0xaa, 0x60, 0xef, 0x10, 0x82, 0x32, 0x68, 0xe0, 0x48, 0x75, 0x07, 0x23, 0xcc, 0x72, - 0x90, 0xa8, 0xe9, 0x76, 0x82, 0x0a, 0x65, 0xc3, 0x9d, 0xc7, 0xbf, 0x5d, 0xaa, 0xfc, 0xf0, 0xfb, - 0xa5, 0x7e, 0x48, 0xe5, 0x24, 0x1d, 0x39, 0x01, 0x8f, 0xdc, 0x62, 0x72, 0xea, 0xbf, 0x4d, 0x31, - 0x7e, 0xe0, 0xca, 0x2c, 0x26, 0x42, 0x3b, 0x88, 0x6f, 0x9f, 0x3d, 0xba, 0xba, 0x38, 0x25, 0x21, - 0x0e, 0x32, 0x5f, 0xcd, 0x47, 0xf1, 0xfd, 0xb3, 0x47, 0x57, 0x2d, 0xcf, 0x1c, 0x88, 0xd6, 0xa1, - 0x15, 0x62, 0xe1, 0x4f, 0x69, 0x44, 0xa5, 0xbe, 0x9e, 0x9a, 0xd7, 0x0c, 0xb1, 0xf8, 0x48, 0xed, - 0x91, 0x03, 0xf5, 0x18, 0x67, 0x24, 0xc9, 0x9b, 0xdc, 0xb0, 0xfd, 0xcb, 0x8f, 0x9b, 0xab, 0x86, - 0xd9, 0x60, 0x3c, 0x4e, 0x88, 0x10, 0x7b, 0x32, 0xa1, 0x2c, 0xf4, 0x72, 0x18, 0xda, 0x82, 0x85, - 0x30, 0xc1, 0x4c, 0x9a, 0xae, 0x37, 0xcf, 0xa3, 0x00, 0xf6, 0x7e, 0xb2, 0xc0, 0xde, 0xa7, 0xf1, - 0xff, 0x59, 0x83, 0x6b, 0xd0, 0x90, 0x34, 0x8e, 0x49, 0x92, 0xf7, 0xc1, 0x39, 0xac, 0x0d, 0xee, - 0x56, 0xb5, 0x6d, 0xf5, 0x7e, 0xb6, 0x60, 0x69, 0x90, 0xce, 0xf2, 0xc7, 0xbb, 0x8d, 0x25, 0x56, - 0xe9, 0xe3, 0x1c, 0xae, 0xd5, 0x35, 0x37, 0x7d, 0x03, 0x44, 0xef, 0x41, 0x53, 0xc9, 0xd7, 0x1f, - 0xf3, 0xc0, 0xbc, 0x8e, 0xcb, 0x2f, 0xe9, 0x4a, 0x27, 0xa7, 0x9a, 0xb7, 0x20, 0xcc, 0xf0, 0x2d, - 0x5e, 0x85, 0xfd, 0x0f, 0x5f, 0x05, 0x5a, 0x06, 0x5b, 0xd0, 0x50, 0xdf, 0xd3, 0xa2, 0xa7, 0x96, - 0xc3, 0xf7, 0x1f, 0x1f, 0x76, 0xad, 0x27, 0x87, 0x5d, 0xeb, 0xe9, 0x61, 0xd7, 0x7a, 0x78, 0xd4, - 0xad, 0x3c, 0x39, 0xea, 0x56, 0x7e, 0x3d, 0xea, 0x56, 0xee, 0x5f, 0x39, 0xbb, 0xd0, 0xae, 0x9c, - 0x8d, 0x1a, 0xba, 0x41, 0xdd, 0xf8, 0x2b, 0x00, 0x00, 0xff, 0xff, 0x24, 0x52, 0x64, 0xe6, 0x23, - 0x0a, 0x00, 0x00, + // 1076 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x55, 0x41, 0x6f, 0x1b, 0xc5, + 0x17, 0xf7, 0x7a, 0x6d, 0xc7, 0x7e, 0x4d, 0xda, 0x64, 0x14, 0xfd, 0xe5, 0x38, 0xff, 0xba, 0xc1, + 0x55, 0xc1, 0xaa, 0xc8, 0x6e, 0x9b, 0x1e, 0x28, 0x15, 0x02, 0xec, 0x86, 0x28, 0x55, 0x29, 0x48, + 0x93, 0x9c, 0x7a, 0x59, 0x8d, 0x77, 0x27, 0xeb, 0x51, 0xbd, 0x33, 0xcb, 0xce, 0x2c, 0x78, 0x8f, + 0x7c, 0x00, 0xa4, 0x8a, 0x0b, 0x12, 0x67, 0x0e, 0x88, 0x53, 0x25, 0x10, 0x9f, 0xa1, 0x27, 0x54, + 0x71, 0xe2, 0x04, 0x55, 0x72, 0xe8, 0x9d, 0x2f, 0x00, 0xda, 0xd9, 0x59, 0x27, 0x2d, 0xa9, 0x53, + 0x04, 0x12, 0x17, 0x7b, 0xe6, 0xed, 0xef, 0xbd, 0xf9, 0xbd, 0x37, 0xbf, 0x79, 0x0f, 0x3a, 0xbe, + 0x90, 0x91, 0x90, 0xae, 0x9a, 0xba, 0x9f, 0x5e, 0x1f, 0x51, 0x45, 0xae, 0xbb, 0x6a, 0xea, 0xc4, + 0x89, 0x50, 0x02, 0xad, 0x14, 0xdf, 0x1c, 0x35, 0x75, 0xcc, 0xb7, 0xce, 0x0a, 0x89, 0x18, 0x17, + 0xae, 0xfe, 0x2d, 0x50, 0x9d, 0xd5, 0x50, 0x84, 0x42, 0x2f, 0xdd, 0x7c, 0x65, 0xac, 0x9b, 0x26, + 0xae, 0x9f, 0x64, 0xb1, 0x12, 0x6e, 0x94, 0x4e, 0x14, 0x93, 0x2c, 0x9c, 0x1d, 0x52, 0x1a, 0x0c, + 0xbc, 0x6b, 0xe0, 0x23, 0x22, 0xe9, 0x0c, 0xe3, 0x0b, 0xc6, 0xcd, 0xf7, 0x37, 0x8e, 0x69, 0x4a, + 0x16, 0x72, 0xc6, 0x8f, 0x23, 0x99, 0xbd, 0x01, 0xae, 0x85, 0x42, 0x84, 0x13, 0xea, 0xea, 0xdd, + 0x28, 0x3d, 0x70, 0x09, 0xcf, 0xca, 0x4f, 0x45, 0x0c, 0xaf, 0xe0, 0x6a, 0x72, 0xd3, 0x9b, 0xde, + 0x17, 0x16, 0x54, 0xf7, 0xa7, 0x68, 0x13, 0x6a, 0x23, 0x11, 0x64, 0x6d, 0x6b, 0xc3, 0xea, 0x9f, + 0xdb, 0x5a, 0x73, 0xfe, 0x92, 0xbf, 0xb3, 0x3f, 0x1d, 0x8a, 0x20, 0xc3, 0x1a, 0x86, 0x6e, 0x42, + 0x8b, 0xa4, 0x6a, 0xec, 0x31, 0x7e, 0x20, 0xda, 0x55, 0xed, 0xb3, 0x7e, 0x8a, 0xcf, 0x20, 0x55, + 0xe3, 0x3b, 0xfc, 0x40, 0xe0, 0x26, 0x31, 0x2b, 0xd4, 0x05, 0xc8, 0x69, 0x13, 0x95, 0x26, 0x54, + 0xb6, 0xed, 0x0d, 0xbb, 0xbf, 0x88, 0x4f, 0x58, 0x7a, 0x1c, 0xea, 0xfb, 0x53, 0x4c, 0x3e, 0x43, + 0x17, 0x01, 0xf2, 0xa3, 0xbc, 0x51, 0xa6, 0xa8, 0xd4, 0xbc, 0x16, 0x71, 0x2b, 0xb7, 0x0c, 0x73, + 0x03, 0x7a, 0x1d, 0x2e, 0xcc, 0x18, 0x18, 0x4c, 0x55, 0x63, 0x96, 0xca, 0xa3, 0x0a, 0xdc, 0x59, + 0xe7, 0x7d, 0x69, 0xc1, 0xc2, 0x1e, 0x0b, 0xf9, 0xb6, 0xf0, 0xff, 0xad, 0x23, 0xd7, 0xa0, 0xe9, + 0x8f, 0x09, 0xe3, 0x1e, 0x0b, 0xda, 0xf6, 0x86, 0xd5, 0x6f, 0xe1, 0x05, 0xbd, 0xbf, 0x13, 0xa0, + 0x2b, 0x70, 0x9e, 0xf8, 0xbe, 0x48, 0xb9, 0xf2, 0x78, 0x1a, 0x8d, 0x68, 0xd2, 0xae, 0x6d, 0x58, + 0xfd, 0x1a, 0x5e, 0x32, 0xd6, 0x8f, 0xb4, 0xb1, 0xf7, 0xbb, 0x05, 0xcb, 0x86, 0xd4, 0x36, 0x4b, + 0xa8, 0xaf, 0x06, 0xe9, 0xf4, 0x2c, 0x76, 0x37, 0x00, 0xe2, 0x74, 0x34, 0x61, 0xbe, 0xf7, 0x80, + 0x66, 0xe6, 0x4e, 0x56, 0x9d, 0x42, 0x13, 0x4e, 0xa9, 0x09, 0x67, 0xc0, 0x33, 0xdc, 0x2a, 0x70, + 0x77, 0x69, 0xf6, 0xcf, 0xa9, 0xa2, 0x0e, 0x34, 0x25, 0xfd, 0x24, 0xa5, 0xdc, 0xa7, 0xed, 0xba, + 0x06, 0xcc, 0xf6, 0xe8, 0x4d, 0xb0, 0x15, 0x8b, 0xdb, 0x0d, 0xcd, 0xe5, 0x7f, 0xa7, 0x69, 0x8a, + 0xc5, 0xc3, 0x6a, 0xdb, 0xc2, 0x39, 0xac, 0xf7, 0x7d, 0x15, 0x1a, 0x85, 0xc8, 0xd0, 0x35, 0x68, + 0x46, 0x54, 0x4a, 0x12, 0xea, 0x44, 0xed, 0x97, 0x66, 0x32, 0x43, 0x21, 0x04, 0xb5, 0x88, 0x46, + 0x85, 0x16, 0x5b, 0x58, 0xaf, 0xf3, 0x0c, 0x14, 0x8b, 0xa8, 0x48, 0x95, 0x37, 0xa6, 0x2c, 0x1c, + 0x2b, 0x9d, 0x62, 0x0d, 0x2f, 0x19, 0xeb, 0xae, 0x36, 0xa2, 0xff, 0x43, 0x2b, 0xe5, 0x22, 0x09, + 0x68, 0x42, 0x03, 0x9d, 0x63, 0x13, 0x1f, 0x1b, 0xd0, 0x10, 0x56, 0xe8, 0x54, 0x51, 0x2e, 0x99, + 0xe0, 0x9e, 0x88, 0x15, 0x13, 0x5c, 0xb6, 0xff, 0x58, 0x98, 0x43, 0x6a, 0x79, 0x86, 0xff, 0xb8, + 0x80, 0xa3, 0xfb, 0xd0, 0xe5, 0x82, 0x7b, 0x7e, 0xc2, 0x14, 0xf3, 0xc9, 0xc4, 0x3b, 0x25, 0xe0, + 0x85, 0x39, 0x01, 0xd7, 0xb9, 0xe0, 0xb7, 0x8d, 0xef, 0x07, 0x2f, 0xc4, 0xee, 0x7d, 0x63, 0x41, + 0xb3, 0x7c, 0x66, 0xe8, 0x7d, 0x58, 0xcc, 0xa5, 0x4d, 0x13, 0xad, 0xd1, 0xb2, 0x76, 0x17, 0x4f, + 0xa9, 0xfc, 0x9e, 0x86, 0xe9, 0xb7, 0x79, 0x4e, 0xce, 0xd6, 0x12, 0xf5, 0xc1, 0x3e, 0xa0, 0xd4, + 0xc8, 0xe7, 0xb4, 0x2b, 0xdb, 0xa1, 0x14, 0xe7, 0x90, 0xf2, 0x72, 0xed, 0x57, 0xbb, 0xdc, 0xaf, + 0x2c, 0x80, 0xe3, 0x33, 0x5f, 0x10, 0xab, 0xf5, 0x6a, 0x62, 0xbd, 0x09, 0xad, 0x48, 0x04, 0xf4, + 0xac, 0xa6, 0x73, 0x4f, 0x04, 0xb4, 0x68, 0x3a, 0x91, 0x59, 0x3d, 0x27, 0x52, 0xfb, 0x79, 0x91, + 0xf6, 0x9e, 0x56, 0xa1, 0x59, 0xba, 0xa0, 0x77, 0xa0, 0x21, 0x19, 0x0f, 0x27, 0xd4, 0x70, 0xea, + 0xcd, 0x89, 0xef, 0xec, 0x69, 0xe4, 0x6e, 0x05, 0x1b, 0x1f, 0xf4, 0x36, 0xd4, 0x75, 0x73, 0x37, + 0xe4, 0x5e, 0x9b, 0xe7, 0x7c, 0x2f, 0x07, 0xee, 0x56, 0x70, 0xe1, 0xd1, 0x19, 0x40, 0xa3, 0x08, + 0x87, 0xde, 0x82, 0x5a, 0xce, 0x5b, 0x13, 0x38, 0xbf, 0x75, 0xf9, 0x44, 0x8c, 0xb2, 0xdd, 0x9f, + 0xbc, 0xc3, 0x3c, 0x1e, 0xd6, 0x0e, 0x9d, 0x87, 0x16, 0xd4, 0x75, 0x54, 0x74, 0x17, 0x9a, 0x23, + 0xa6, 0x48, 0x92, 0x90, 0xb2, 0xb6, 0x6e, 0x19, 0xa6, 0x18, 0x4a, 0xce, 0x6c, 0x06, 0x95, 0xb1, + 0x6e, 0x8b, 0x28, 0x26, 0xbe, 0x1a, 0x32, 0x35, 0xc8, 0xdd, 0xf0, 0x2c, 0x00, 0xba, 0x05, 0x30, + 0xab, 0x7a, 0xde, 0xf0, 0xec, 0xb3, 0xca, 0xde, 0x2a, 0xcb, 0x2e, 0x87, 0x75, 0xb0, 0x65, 0x1a, + 0xf5, 0x3e, 0xaf, 0x82, 0xbd, 0x43, 0x29, 0xca, 0xa0, 0x41, 0xa2, 0xbc, 0x77, 0x18, 0x61, 0xce, + 0xc6, 0x4c, 0x3e, 0xfb, 0x4e, 0x50, 0x61, 0x7c, 0xb8, 0xf3, 0xf8, 0xd7, 0x4b, 0x95, 0xef, 0x7e, + 0xbb, 0xd4, 0x0f, 0x99, 0x1a, 0xa7, 0x23, 0xc7, 0x17, 0x91, 0x5b, 0xce, 0x55, 0xfd, 0xb7, 0x29, + 0x83, 0x07, 0xae, 0xca, 0x62, 0x2a, 0xb5, 0x83, 0xfc, 0xfa, 0xd9, 0xa3, 0xab, 0x8b, 0x13, 0x1a, + 0x12, 0x3f, 0xf3, 0xf2, 0xe9, 0x29, 0xbf, 0x7d, 0xf6, 0xe8, 0xaa, 0x85, 0xcd, 0x81, 0x68, 0x1d, + 0x5a, 0x21, 0x91, 0xde, 0x84, 0x45, 0x4c, 0xe9, 0xeb, 0xa9, 0xe1, 0x66, 0x48, 0xe4, 0x87, 0xf9, + 0x1e, 0x39, 0x50, 0x8f, 0x49, 0x46, 0x93, 0xa2, 0x05, 0x0e, 0xdb, 0x3f, 0xff, 0xb0, 0xb9, 0x6a, + 0x98, 0x0d, 0x82, 0x20, 0xa1, 0x52, 0xee, 0xa9, 0x84, 0xf1, 0x10, 0x17, 0x30, 0xb4, 0x05, 0x0b, + 0x61, 0x42, 0xb8, 0x32, 0x3d, 0x71, 0x9e, 0x47, 0x09, 0xec, 0xfd, 0x68, 0x81, 0xbd, 0xcf, 0xe2, + 0xff, 0xb2, 0x06, 0xd7, 0xa0, 0xa1, 0x58, 0x1c, 0xd3, 0xa4, 0xe8, 0x92, 0x73, 0x58, 0x1b, 0xdc, + 0xad, 0x6a, 0xdb, 0xea, 0xfd, 0x64, 0xc1, 0xd2, 0x20, 0x9d, 0x16, 0x8f, 0x77, 0x9b, 0x28, 0x92, + 0xa7, 0x4f, 0x0a, 0xb8, 0x56, 0xd7, 0xdc, 0xf4, 0x0d, 0x10, 0xbd, 0x0b, 0xcd, 0x5c, 0xbe, 0x5e, + 0x20, 0x7c, 0xf3, 0x3a, 0x2e, 0xbf, 0xa4, 0x2b, 0x9d, 0x9c, 0x79, 0x78, 0x41, 0x9a, 0xd1, 0x5c, + 0xbe, 0x0a, 0xfb, 0x6f, 0xbe, 0x0a, 0xb4, 0x0c, 0xb6, 0x64, 0xa1, 0xbe, 0xa7, 0x45, 0x9c, 0x2f, + 0x87, 0xef, 0x3d, 0x3e, 0xec, 0x5a, 0x4f, 0x0e, 0xbb, 0xd6, 0xd3, 0xc3, 0xae, 0xf5, 0xf0, 0xa8, + 0x5b, 0x79, 0x72, 0xd4, 0xad, 0xfc, 0x72, 0xd4, 0xad, 0xdc, 0xbf, 0x72, 0x76, 0xa1, 0x5d, 0x35, + 0x1d, 0x35, 0x74, 0x83, 0xba, 0xf1, 0x67, 0x00, 0x00, 0x00, 0xff, 0xff, 0xb1, 0x48, 0xf7, 0x44, + 0x41, 0x0a, 0x00, 0x00, } func (m *Tx) Marshal() (dAtA []byte, err error) { @@ -1364,6 +1381,16 @@ func (m *TxBody) MarshalToSizedBuffer(dAtA []byte) (int, error) { dAtA[i] = 0xfa } } + if m.Unordered { + i-- + if m.Unordered { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x20 + } if m.TimeoutHeight != 0 { i = encodeVarintTx(dAtA, i, uint64(m.TimeoutHeight)) i-- @@ -1942,6 +1969,9 @@ func (m *TxBody) Size() (n int) { if m.TimeoutHeight != 0 { n += 1 + sovTx(uint64(m.TimeoutHeight)) } + if m.Unordered { + n += 2 + } if len(m.ExtensionOptions) > 0 { for _, e := range m.ExtensionOptions { l = e.Size() @@ -2955,6 +2985,26 @@ func (m *TxBody) Unmarshal(dAtA []byte) error { break } } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Unordered", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Unordered = bool(v != 0) case 1023: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field ExtensionOptions", wireType) From bd176418ea2b62d11e25977453263c508d2804b2 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Wed, 6 Dec 2023 10:47:41 -0700 Subject: [PATCH 02/20] updates --- docs/architecture/adr-070-unordered-account.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/adr-070-unordered-account.md b/docs/architecture/adr-070-unordered-account.md index 53477fe973c4..6527c11d7d12 100644 --- a/docs/architecture/adr-070-unordered-account.md +++ b/docs/architecture/adr-070-unordered-account.md @@ -2,7 +2,7 @@ ## Changelog -* Dec 4, 2023: Initial Draft (@yihuang, @alexanderbez) +* Dec 4, 2023: Initial Draft (@yihuang, @tac0turtle, @alexanderbez) ## Status From 990dd48f88e36f7f42f0df2438c83f3b885b8cbf Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Wed, 6 Dec 2023 11:00:50 -0700 Subject: [PATCH 03/20] updates --- api/cosmos/tx/v1beta1/tx.pulsar.go | 12 ++++++++++-- proto/cosmos/tx/v1beta1/tx.proto | 12 ++++++++++-- types/tx/tx.pb.go | 12 ++++++++++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/api/cosmos/tx/v1beta1/tx.pulsar.go b/api/cosmos/tx/v1beta1/tx.pulsar.go index 2c2b63638401..eb43d518b4d3 100644 --- a/api/cosmos/tx/v1beta1/tx.pulsar.go +++ b/api/cosmos/tx/v1beta1/tx.pulsar.go @@ -8471,14 +8471,22 @@ type TxBody struct { // but should be called `note` instead (see // https://github.com/cosmos/cosmos-sdk/issues/9122). Memo string `protobuf:"bytes,2,opt,name=memo,proto3" json:"memo,omitempty"` - // timeout is the block height after which this transaction will not - // be processed by the chain + // timeout_height is the block height after which this transaction will not + // be processed by the chain. + // + // Note, if unordered=true this value MUST be set + // and will act as a short-lived TTL in which the transaction is deemed valid + // and kept in memory to prevent duplicates. TimeoutHeight uint64 `protobuf:"varint,3,opt,name=timeout_height,json=timeoutHeight,proto3" json:"timeout_height,omitempty"` // unordered, when set to true, indicates that the transaction signer(s) // intend for the transaction to be evaluated and executed in an un-ordered // fashion. Specifically, the account's nonce will NOT be checked or // incremented, which allows for fire-and-forget as well as concurrent // transaction execution. + // + // Note, when set to true, the existing 'timeout_height' value must be set and + // will be used to correspond to a height in which the transaction is deemed + // valid. Unordered bool `protobuf:"varint,4,opt,name=unordered,proto3" json:"unordered,omitempty"` // extension_options are arbitrary options that can be added by chains // when the default options are not sufficient. If any of these are present diff --git a/proto/cosmos/tx/v1beta1/tx.proto b/proto/cosmos/tx/v1beta1/tx.proto index 8e6e375cc8c9..6cbf6a0cdc50 100644 --- a/proto/cosmos/tx/v1beta1/tx.proto +++ b/proto/cosmos/tx/v1beta1/tx.proto @@ -109,8 +109,12 @@ message TxBody { // https://github.com/cosmos/cosmos-sdk/issues/9122). string memo = 2; - // timeout is the block height after which this transaction will not - // be processed by the chain + // timeout_height is the block height after which this transaction will not + // be processed by the chain. + // + // Note, if unordered=true this value MUST be set + // and will act as a short-lived TTL in which the transaction is deemed valid + // and kept in memory to prevent duplicates. uint64 timeout_height = 3; // unordered, when set to true, indicates that the transaction signer(s) @@ -118,6 +122,10 @@ message TxBody { // fashion. Specifically, the account's nonce will NOT be checked or // incremented, which allows for fire-and-forget as well as concurrent // transaction execution. + // + // Note, when set to true, the existing 'timeout_height' value must be set and + // will be used to correspond to a height in which the transaction is deemed + // valid. bool unordered = 4; // extension_options are arbitrary options that can be added by chains diff --git a/types/tx/tx.pb.go b/types/tx/tx.pb.go index 32b3e3fa2ee0..e76864156971 100644 --- a/types/tx/tx.pb.go +++ b/types/tx/tx.pb.go @@ -359,14 +359,22 @@ type TxBody struct { // but should be called `note` instead (see // https://github.com/cosmos/cosmos-sdk/issues/9122). Memo string `protobuf:"bytes,2,opt,name=memo,proto3" json:"memo,omitempty"` - // timeout is the block height after which this transaction will not - // be processed by the chain + // timeout_height is the block height after which this transaction will not + // be processed by the chain. + // + // Note, if unordered=true this value MUST be set + // and will act as a short-lived TTL in which the transaction is deemed valid + // and kept in memory to prevent duplicates. TimeoutHeight uint64 `protobuf:"varint,3,opt,name=timeout_height,json=timeoutHeight,proto3" json:"timeout_height,omitempty"` // unordered, when set to true, indicates that the transaction signer(s) // intend for the transaction to be evaluated and executed in an un-ordered // fashion. Specifically, the account's nonce will NOT be checked or // incremented, which allows for fire-and-forget as well as concurrent // transaction execution. + // + // Note, when set to true, the existing 'timeout_height' value must be set and + // will be used to correspond to a height in which the transaction is deemed + // valid. Unordered bool `protobuf:"varint,4,opt,name=unordered,proto3" json:"unordered,omitempty"` // extension_options are arbitrary options that can be added by chains // when the default options are not sufficient. If any of these are present From 60a2d4a026949e434c9aa0b52c0a52b271554706 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Wed, 6 Dec 2023 11:14:33 -0700 Subject: [PATCH 04/20] updates --- types/tx_msg.go | 8 ++++++++ x/auth/ante/sigverify.go | 39 +++++++++++++++++++++++++++++---------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/types/tx_msg.go b/types/tx_msg.go index 399dafd4cc43..1f81dbbc0912 100644 --- a/types/tx_msg.go +++ b/types/tx_msg.go @@ -79,6 +79,14 @@ type ( GetTimeoutHeight() uint64 } + // TxWithUnordered extends the Tx interface by allowing a transaction to set + // the unordered field, which implicitly relies on TxWithTimeoutHeight. + TxWithUnordered interface { + TxWithTimeoutHeight + + GetUnordered() bool + } + // HasValidateBasic defines a type that has a ValidateBasic method. // ValidateBasic is deprecated and now facultative. // Prefer validating messages directly in the msg server. diff --git a/x/auth/ante/sigverify.go b/x/auth/ante/sigverify.go index 987c7dedb83f..c8966d112afb 100644 --- a/x/auth/ante/sigverify.go +++ b/x/auth/ante/sigverify.go @@ -393,15 +393,19 @@ func (svd SigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simul return next(ctx, tx, simulate) } -// IncrementSequenceDecorator handles incrementing sequences of all signers. +// IncrementSequenceDecorator handles incrementing sequences of all signers, which +// should happen after signature verification, i.e. after SigVerificationDecorator. +// // Use the IncrementSequenceDecorator decorator to prevent replay attacks. Note, // there is need to execute IncrementSequenceDecorator on RecheckTx since // BaseApp.Commit() will set the check state based on the latest header. // // NOTE: Since CheckTx and DeliverTx state are managed separately, subsequent and -// sequential txs orginating from the same account cannot be handled correctly in +// sequential txs originating from the same account cannot be handled correctly in // a reliable way unless sequence numbers are managed and tracked manually by a -// client. It is recommended to instead use multiple messages in a tx. +// client. In such cases, where unordered or parallel transactions are desired, +// it is recommended to to set unordered=true with a reasonable timeout_height +// value. type IncrementSequenceDecorator struct { ak AccountKeeper } @@ -413,6 +417,14 @@ func NewIncrementSequenceDecorator(ak AccountKeeper) IncrementSequenceDecorator } func (isd IncrementSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + // Bypass incrementing sequence for transactions with unordered set to true. + // The actual parameters of the un-ordered tx will be checked in a separate + // decorator. + unorderedTx, ok := tx.(sdk.TxWithUnordered) + if ok && unorderedTx.GetUnordered() { + return next(ctx, tx, simulate) + } + sigTx, ok := tx.(authsigning.SigVerifiableTx) if !ok { return ctx, errorsmod.Wrap(sdkerrors.ErrTxDecode, "invalid transaction type") @@ -427,7 +439,7 @@ func (isd IncrementSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, sim for _, signer := range signers { acc := isd.ak.GetAccount(ctx, signer) if err := acc.SetSequence(acc.GetSequence() + 1); err != nil { - panic(err) + panic(fmt.Errorf("failed to set account sequence: %w", err)) } pubKey := acc.GetPubKey() @@ -474,8 +486,7 @@ func (vscd ValidateSigCountDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, sim for _, pk := range pubKeys { sigCount += CountSubKeys(pk) if uint64(sigCount) > params.TxSigLimit { - return ctx, errorsmod.Wrapf(sdkerrors.ErrTooManySignatures, - "signatures: %d, limit: %d", sigCount, params.TxSigLimit) + return ctx, errorsmod.Wrapf(sdkerrors.ErrTooManySignatures, "signatures: %d, limit: %d", sigCount, params.TxSigLimit) } } @@ -485,10 +496,9 @@ func (vscd ValidateSigCountDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, sim // DefaultSigVerificationGasConsumer is the default implementation of SignatureVerificationGasConsumer. It consumes gas // for signature verification based upon the public key type. The cost is fetched from the given params and is matched // by the concrete type. -func DefaultSigVerificationGasConsumer( - meter storetypes.GasMeter, sig signing.SignatureV2, params types.Params, -) error { +func DefaultSigVerificationGasConsumer(meter storetypes.GasMeter, sig signing.SignatureV2, params types.Params) error { pubkey := sig.PubKey + switch pubkey := pubkey.(type) { case *ed25519.PubKey: meter.ConsumeGas(params.SigVerifyCostED25519, "ante verify: ed25519") @@ -507,10 +517,12 @@ func DefaultSigVerificationGasConsumer( if !ok { return fmt.Errorf("expected %T, got, %T", &signing.MultiSignatureData{}, sig.Data) } + err := ConsumeMultisignatureVerificationGas(meter, multisignature, pubkey, params, sig.Sequence) if err != nil { return err } + return nil default: @@ -535,10 +547,12 @@ func ConsumeMultisignatureVerificationGas( Data: sig.Signatures[sigIndex], Sequence: accSeq, } + err := DefaultSigVerificationGasConsumer(meter, sigV2, params) if err != nil { return err } + sigIndex++ } @@ -562,6 +576,7 @@ func CountSubKeys(pub cryptotypes.PubKey) int { if pub == nil { return 0 } + v, ok := pub.(*kmultisig.LegacyAminoPubKey) if !ok { return 1 @@ -587,6 +602,7 @@ func signatureDataToBz(data signing.SignatureData) ([][]byte, error) { switch data := data.(type) { case *signing.SingleSignatureData: return [][]byte{data.Signature}, nil + case *signing.MultiSignatureData: sigs := [][]byte{} var err error @@ -596,19 +612,22 @@ func signatureDataToBz(data signing.SignatureData) ([][]byte, error) { if err != nil { return nil, err } + sigs = append(sigs, nestedSigs...) } multiSignature := cryptotypes.MultiSignature{ Signatures: sigs, } + aggregatedSig, err := multiSignature.Marshal() if err != nil { return nil, err } - sigs = append(sigs, aggregatedSig) + sigs = append(sigs, aggregatedSig) return sigs, nil + default: return nil, sdkerrors.ErrInvalidType.Wrapf("unexpected signature data type %T", data) } From 03597f30838a5246c06de29574459d867542206e Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Wed, 6 Dec 2023 11:15:41 -0700 Subject: [PATCH 05/20] updates --- x/auth/ante/sigverify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/auth/ante/sigverify.go b/x/auth/ante/sigverify.go index c8966d112afb..f8b7290fc946 100644 --- a/x/auth/ante/sigverify.go +++ b/x/auth/ante/sigverify.go @@ -439,7 +439,7 @@ func (isd IncrementSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, sim for _, signer := range signers { acc := isd.ak.GetAccount(ctx, signer) if err := acc.SetSequence(acc.GetSequence() + 1); err != nil { - panic(fmt.Errorf("failed to set account sequence: %w", err)) + panic(err) } pubKey := acc.GetPubKey() From 1e2254ca8d87405e7bd0dcf3dbcbd4d9bdeaad1e Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Wed, 6 Dec 2023 11:19:58 -0700 Subject: [PATCH 06/20] updates --- x/auth/ante/unordered.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 x/auth/ante/unordered.go diff --git a/x/auth/ante/unordered.go b/x/auth/ante/unordered.go new file mode 100644 index 000000000000..d1bb09c33a7b --- /dev/null +++ b/x/auth/ante/unordered.go @@ -0,0 +1,16 @@ +package ante + +// 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 it's +// nonce incremented, which allows fire-and-forget along with possible parallel +// transaction processing, without having to deal with nonces. +// +// The transaction sender must ensure that unordered=true and a height_timeout +// is appropriately set. The AnteHandler will check that the transaction is not +// a duplicate and will evict it from memory when the timeout is reached. +type UnorderedTxDecorator struct{} + +func NewUnorderedTxDecorator() UnorderedTxDecorator { + return UnorderedTxDecorator{} +} From bc356554ecb4d852b37cfe34dc5a228831098b51 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Wed, 6 Dec 2023 12:26:19 -0700 Subject: [PATCH 07/20] updates --- x/auth/ante/unordered.go | 45 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/x/auth/ante/unordered.go b/x/auth/ante/unordered.go index d1bb09c33a7b..057921d5ae54 100644 --- a/x/auth/ante/unordered.go +++ b/x/auth/ante/unordered.go @@ -1,16 +1,53 @@ package ante +import ( + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const ( + // DefaultMaxUnOrderedTTL defines the default maximum TTL an un-ordered transaction + // can set. + DefaultMaxUnOrderedTTL = 1024 +) + +var _ sdk.AnteDecorator = (*UnorderedTxDecorator)(nil) + // 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 it's // nonce incremented, which allows fire-and-forget along with possible parallel // transaction processing, without having to deal with nonces. // -// The transaction sender must ensure that unordered=true and a height_timeout +// 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 memory when the timeout is reached. -type UnorderedTxDecorator struct{} +type UnorderedTxDecorator struct { + // maxUnOrderedTTL defines the maximum TTL a transaction can define. + maxUnOrderedTTL uint64 +} + +func NewUnorderedTxDecorator(maxTTL uint64) *UnorderedTxDecorator { + return &UnorderedTxDecorator{ + maxUnOrderedTTL: maxTTL, + } +} + +func (d *UnorderedTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, 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 next(ctx, tx, simulate) + } + + if unorderedTx.GetTimeoutHeight() == 0 { + return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "unordered transaction must have timeout_height set") + } -func NewUnorderedTxDecorator() UnorderedTxDecorator { - return UnorderedTxDecorator{} + if unorderedTx.GetTimeoutHeight() > uint64(ctx.BlockHeight())+d.maxUnOrderedTTL { + return ctx, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "unordered tx ttl exceeds %d", d.maxUnOrderedTTL) + } } From 7a421d4664d9ea32d43ff0e385f405b1e5c7ff3a Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Sat, 9 Dec 2023 13:30:35 -0600 Subject: [PATCH 08/20] updates --- .../architecture/adr-070-unordered-account.md | 60 ++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/docs/architecture/adr-070-unordered-account.md b/docs/architecture/adr-070-unordered-account.md index 6527c11d7d12..fb682d4bfff1 100644 --- a/docs/architecture/adr-070-unordered-account.md +++ b/docs/architecture/adr-070-unordered-account.md @@ -53,14 +53,43 @@ message TxBody { } ``` -### `DedupTxHashManager` +### Replay Protection + +In order to provide replay protection, a user should ensure that the transaction's +TTL value is relatively short-lived but long enough to provide enough time to be +included in a block, e.g. ~H+50. + +We facilitate this by storing the transaction's hash in a durable map, `UnorderedTxManager`, +to prevent duplicates, i.e. replay attacks. Upon transaction ingress during `CheckTx`, +we check if the transaction's hash exists in this map or if the TTL value is stale, +i.e. before the current block. If so, we reject it. Upon inclusion in a block +during `DeliverTx`, the transaction's hash is set in the map along with it's TTL +value. + +This map is evaluated at the end of each block, e.g. ABCI `Commit`, and all stale +transactions, i.e. transactions's TTL value who's now beyond the committed block, +are purged from the map. + +An important point to note is that in theory, it may be possible to submit an unordered +transaction twice, or multiple times, before the transaction is included in a block. +However, we'll note a few important layers of protection and mitigation: + +* Assuming CometBFT is used as the underlying consensus engine and a non-noop mempool + is used, CometBFT will reject the duplicate for you. +* For applications that leverage ABCI++, `ProcessProposal` should evaluate and reject + malicious proposals with duplicate transactions. +* For applications that leverage their own application mempool, their mempool should + reject the duplicate for you. +* Finally, worst case if the duplicate transaction is somehow selected for a block + proposal, 2nd and all further attempts to evaluate it, will fail during `DeliverTx`, + so worst case you just end up filling up block space with a duplicate transaction. ```golang const PurgeLoopSleepMS = 500 -// DedupTxHashManager contains the tx hash dictionary for duplicates checking, -// and expire them when block number progresses. -type DedupTxHashManager struct { +// UnorderedTxManager contains the tx hash dictionary for duplicates checking, +// and expire them when block production progresses. +type UnorderedTxManager struct { mutex sync.RWMutex // tx hash -> expire block number // for duplicates checking and expiration @@ -176,9 +205,11 @@ func channelBatchRecv[T any](ch <-chan *T) []*T { } ``` -### Ante Handlers +### AnteHandler Decorator -Bypass the nonce decorator for un-ordered transactions. +In order to facilitate bypassing nonce verification, we have to modify the existing +`IncrementSequenceDecorator` AnteHandler decorator to skip the nonce verification +when the transaction is marked as un-ordered. ```golang func (isd IncrementSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { @@ -186,22 +217,23 @@ func (isd IncrementSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, sim return next(ctx, tx, simulate) } - // the previous logic + // ... } ``` -A decorator for the new logic. +In addition, we need to introduce a new decorator to perform the un-ordered transaction +verification and map lookup. ```golang -type TxHash [32]byte - const ( - // MaxUnOrderedTTL defines the maximum ttl an un-order tx can set - MaxUnOrderedTTL = 1024 + // DefaultMaxUnOrderedTTL defines the default maximum TTL an un-ordered transaction + // can set. + DefaultMaxUnOrderedTTL = 1024 ) type DedupTxDecorator struct { - m *DedupTxHashManager + m *UnorderedTxManager + maxUnOrderedTTL uint64 } func (dtd *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { @@ -224,7 +256,7 @@ func (dtd *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate boo } if !ctx.IsCheckTx() { - // a new tx included in the block, add the hash to the dictionary + // a new tx included in the block, add the hash to the unordered tx manager dtd.m.Add(tx.Hash(), tx.TimeoutHeight()) } From 38f1ec4eb5170337adfb60589012b5817d93d2f5 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Sat, 9 Dec 2023 14:23:33 -0600 Subject: [PATCH 09/20] updates --- .../architecture/adr-070-unordered-account.md | 133 ++++++++++-------- 1 file changed, 71 insertions(+), 62 deletions(-) diff --git a/docs/architecture/adr-070-unordered-account.md b/docs/architecture/adr-070-unordered-account.md index fb682d4bfff1..6f5dc0b474ca 100644 --- a/docs/architecture/adr-070-unordered-account.md +++ b/docs/architecture/adr-070-unordered-account.md @@ -85,76 +85,107 @@ However, we'll note a few important layers of protection and mitigation: so worst case you just end up filling up block space with a duplicate transaction. ```golang +type TxHash [32]byte + const PurgeLoopSleepMS = 500 // UnorderedTxManager contains the tx hash dictionary for duplicates checking, // and expire them when block production progresses. type UnorderedTxManager struct { - mutex sync.RWMutex - // tx hash -> expire block number - // for duplicates checking and expiration - hashes map[TxHash]uint64 - // channel to receive latest block numbers + // blockCh defines a channel to receive newly committed block heights blockCh chan uint64 + + mu sync.RWMutex + // hashes defines a map from tx hash -> TTL value, which is used for duplicate + // checking and replay protection. + hashes map[TxHash]uint64 } -func NewDedupTxHashManager() *DedupTxHashManager { - m := &DedupTxHashManager{ +func NewUnorderedTxManager() *UnorderedTxManager { + m := &UnorderedTxManager{ hashes: make(map[TxHash]uint64), blockCh: make(ch *uint64, 16), } + + return m +} + +func (m *UnorderedTxManager) Start() { go m.purgeLoop() - return m } -func (dtm *DedupTxHashManager) Close() error { - close(dtm.blockCh) - dtm.blockCh = nil +func (m *UnorderedTxManager) Close() error { + close(m.blockCh) + m.blockCh = nil return nil } -func (dtm *DedupTxHashManager) Contains(hash TxHash) (ok bool) { - dtm.mutex.RLock() - defer dtm.mutex.RUnlock() +func (m *UnorderedTxManager) Contains(hash TxHash) bool{ + m.mu.RLock() + defer m.mu.RUnlock() - _, ok = dtm.hashes[hash] - return + _, ok := m.hashes[hash] + return ok } -func (dtm *DedupTxHashManager) Size() int { - dtm.mutex.RLock() - defer dtm.mutex.RUnlock() +func (m *UnorderedTxManager) Size() int { + m.mu.RLock() + defer m.mu.RUnlock() - return len(dtm.hashes) + return len(m.hashes) } -func (dtm *DedupTxHashManager) Add(hash TxHash, expire uint64) (ok bool) { - dtm.mutex.Lock() - defer dtm.mutex.Unlock() +func (m *UnorderedTxManager) Add(hash TxHash, expire uint64) { + m.mu.Lock() + defer m.mu.Unlock() - dtm.hashes[hash] = expire - return + m.hashes[hash] = expire } -// OnNewBlock send the latest block number to the background purge loop, -// it should be called in abci commit event. -func (dtm *DedupTxHashManager) OnNewBlock(blockNumber uint64) { - dtm.blockCh <- &blockNumber +// OnNewBlock send the latest block number to the background purge loop, which +// should be called in ABCI Commit event. +func (m *UnorderedTxManager) OnNewBlock(blockHeight uint64) { + m.blockCh <- blockHeight } -// purgeLoop removes expired tx hashes at background -func (dtm *DedupTxHashManager) purgeLoop() error { +// expiredTxs returns expired tx hashes based on the provided block height. +func (m *UnorderedTxManager) expiredTxs(blockHeight uint64) []TxHash { + m.mu.RLock() + defer m.mu.RUnlock() + + var result []TxHash + for txHash, expire := range m.hashes { + if blockHeight > expire { + result = append(result, txHash) + } + } + + return result +} + +func (m *UnorderedTxManager) purge(txHashes []TxHash) { + m.mu.Lock() + defer m.mu.Unlock() + + for _, txHash := range txHashes { + delete(dtm.hashes, txHash) + } +} + + +// purgeLoop removes expired tx hashes in the background +func (m *UnorderedTxManager) purgeLoop() error { for { - blocks := channelBatchRecv(dtm.blockCh) + blocks := channelBatchRecv(m.blockCh) if len(blocks) == 0 { // channel closed break } latest := *blocks[len(blocks)-1] - hashes := dtm.expired(latest) + hashes := m.expired(latest) if len(hashes) > 0 { - dtm.purge(hashes) + m.purge(hashes) } // avoid burning cpu in catching up phase @@ -162,28 +193,6 @@ func (dtm *DedupTxHashManager) purgeLoop() error { } } -// expired find out expired tx hashes based on latest block number -func (dtm *DedupTxHashManager) expired(block uint64) []TxHash { - dtm.mutex.RLock() - defer dtm.mutex.RUnlock() - - var result []TxHash - for h, expire := range dtm.hashes { - if block > expire { - result = append(result, h) - } - } - return result -} - -func (dtm *DedupTxHashManager) purge(hashes []TxHash) { - dtm.mutex.Lock() - defer dtm.mutex.Unlock() - - for _, hash := range hashes { - delete(dtm.hashes, hash) - } -} // channelBatchRecv try to exhaust the channel buffer when it's not empty, // and block when it's empty. @@ -236,7 +245,7 @@ type DedupTxDecorator struct { maxUnOrderedTTL uint64 } -func (dtd *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { +func (d *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { // only apply to un-ordered transactions if !tx.UnOrdered() { return next(ctx, tx, simulate) @@ -246,18 +255,18 @@ func (dtd *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate boo return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "unordered tx must set timeout-height") } - if tx.TimeoutHeight() > ctx.BlockHeight() + MaxUnOrderedTTL { - return nil, errorsmod.Wrapf(sdkerrors.ErrLogic, "unordered tx ttl exceeds %d", MaxUnOrderedTTL) + if tx.TimeoutHeight() > ctx.BlockHeight() + d.maxUnOrderedTTL { + return nil, errorsmod.Wrapf(sdkerrors.ErrLogic, "unordered tx ttl exceeds %d", d.maxUnOrderedTTL) } // check for duplicates - if dtd.m.Contains(tx.Hash()) { + if d.m.Contains(tx.Hash()) { return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "tx is duplicated") } if !ctx.IsCheckTx() { // a new tx included in the block, add the hash to the unordered tx manager - dtd.m.Add(tx.Hash(), tx.TimeoutHeight()) + d.m.Add(tx.Hash(), tx.TimeoutHeight()) } return next(ctx, tx, simulate) @@ -266,7 +275,7 @@ func (dtd *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate boo ### `OnNewBlock` -Wire the `OnNewBlock` method of `DedupTxHashManager` into the BaseApp's ABCI Commit event. +Wire the `OnNewBlock` method of `UnorderedTxManager` into the BaseApp's ABCI `Commit` event. ### Start Up From cded15eeb9ad42aeec4ca96d6a35127f56567eaf Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Mon, 11 Dec 2023 11:36:05 -0800 Subject: [PATCH 10/20] updates --- .../architecture/adr-070-unordered-account.md | 23 +-- x/auth/ante/unordered.go | 146 +++++++++++++++++- 2 files changed, 157 insertions(+), 12 deletions(-) diff --git a/docs/architecture/adr-070-unordered-account.md b/docs/architecture/adr-070-unordered-account.md index 6f5dc0b474ca..ac63a4a40daa 100644 --- a/docs/architecture/adr-070-unordered-account.md +++ b/docs/architecture/adr-070-unordered-account.md @@ -96,15 +96,16 @@ type UnorderedTxManager struct { blockCh chan uint64 mu sync.RWMutex - // hashes defines a map from tx hash -> TTL value, which is used for duplicate - // checking and replay protection. - hashes map[TxHash]uint64 + // txHashes defines a map from tx hash -> TTL value, which is used for duplicate + // checking and replay protection, as well as purging the map when the TTL is + // expired. + txHashes map[TxHash]uint64 } func NewUnorderedTxManager() *UnorderedTxManager { m := &UnorderedTxManager{ - hashes: make(map[TxHash]uint64), - blockCh: make(ch *uint64, 16), + blockCh: make(chan uint64, 16), + txHashes: make(map[TxHash]uint64), } return m @@ -124,7 +125,7 @@ func (m *UnorderedTxManager) Contains(hash TxHash) bool{ m.mu.RLock() defer m.mu.RUnlock() - _, ok := m.hashes[hash] + _, ok := m.txHashes[hash] return ok } @@ -132,14 +133,14 @@ func (m *UnorderedTxManager) Size() int { m.mu.RLock() defer m.mu.RUnlock() - return len(m.hashes) + return len(m.txHashes) } func (m *UnorderedTxManager) Add(hash TxHash, expire uint64) { m.mu.Lock() defer m.mu.Unlock() - m.hashes[hash] = expire + m.txHashes[hash] = expire } // OnNewBlock send the latest block number to the background purge loop, which @@ -154,7 +155,7 @@ func (m *UnorderedTxManager) expiredTxs(blockHeight uint64) []TxHash { defer m.mu.RUnlock() var result []TxHash - for txHash, expire := range m.hashes { + for txHash, expire := range m.txHashes { if blockHeight > expire { result = append(result, txHash) } @@ -168,7 +169,7 @@ func (m *UnorderedTxManager) purge(txHashes []TxHash) { defer m.mu.Unlock() for _, txHash := range txHashes { - delete(dtm.hashes, txHash) + delete(m.txHashes, txHash) } } @@ -279,7 +280,7 @@ Wire the `OnNewBlock` method of `UnorderedTxManager` into the BaseApp's ABCI `Co ### Start Up -On start up, the node needs to re-fill the tx hash dictionary of `DedupTxHashManager` +On start up, the node needs to re-fill the tx hash dictionary of `UnorderedTxManager` by scanning `MaxUnOrderedTTL` number of historical blocks for existing un-expired un-ordered transactions. diff --git a/x/auth/ante/unordered.go b/x/auth/ante/unordered.go index 057921d5ae54..8fbe885eb50b 100644 --- a/x/auth/ante/unordered.go +++ b/x/auth/ante/unordered.go @@ -1,6 +1,10 @@ package ante import ( + "context" + "sync" + "time" + errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" @@ -13,6 +17,133 @@ const ( DefaultMaxUnOrderedTTL = 1024 ) +// TxHash defines a transaction hash type alias, which is a fixed array of 32 bytes. +type TxHash [32]byte + +// UnorderedTxManager contains the tx hash dictionary for duplicates checking, +// and expire them when block production progresses. +type UnorderedTxManager struct { + // blockCh defines a channel to receive newly committed block heights + blockCh chan uint64 + + mu sync.RWMutex + // txHashes defines a map from tx hash -> TTL value, which is used for duplicate + // checking and replay protection, as well as purging the map when the TTL is + // expired. + txHashes map[TxHash]uint64 +} + +func NewUnorderedTxManager() *UnorderedTxManager { + m := &UnorderedTxManager{ + blockCh: make(chan uint64, 16), + txHashes: make(map[TxHash]uint64), + } + + return m +} + +func (m *UnorderedTxManager) Start() { + go m.purgeLoop() +} + +func (m *UnorderedTxManager) Close() error { + close(m.blockCh) + m.blockCh = nil + + return nil +} + +func (m *UnorderedTxManager) Contains(hash TxHash) bool { + m.mu.RLock() + defer m.mu.RUnlock() + + _, ok := m.txHashes[hash] + return ok +} + +func (m *UnorderedTxManager) Size() int { + m.mu.RLock() + defer m.mu.RUnlock() + + return len(m.txHashes) +} + +func (m *UnorderedTxManager) Add(txHash TxHash, ttl uint64) { + m.mu.Lock() + defer m.mu.Unlock() + + m.txHashes[txHash] = ttl +} + +// OnNewBlock send the latest block number to the background purge loop, which +// should be called in ABCI Commit event. +func (m *UnorderedTxManager) OnNewBlock(blockHeight uint64) { + m.blockCh <- blockHeight +} + +// expiredTxs returns expired tx hashes based on the provided block height. +func (m *UnorderedTxManager) expiredTxs(blockHeight uint64) []TxHash { + m.mu.RLock() + defer m.mu.RUnlock() + + var result []TxHash + for txHash, ttl := range m.txHashes { + if blockHeight > ttl { + result = append(result, txHash) + } + } + + return result +} + +func (m *UnorderedTxManager) purge(txHashes []TxHash) { + m.mu.Lock() + defer m.mu.Unlock() + + for _, txHash := range txHashes { + delete(m.txHashes, txHash) + } +} + +// purgeLoop removes expired tx hashes in the background +func (m *UnorderedTxManager) purgeLoop() { + for { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + latestHeight, ok := m.batchReceive(ctx) + if !ok { + // channel closed + break + } + + hashes := m.expiredTxs(latestHeight) + if len(hashes) > 0 { + m.purge(hashes) + } + } +} + +func (m *UnorderedTxManager) batchReceive(ctx context.Context) (uint64, bool) { + var latestHeight uint64 + + for { + select { + case <-ctx.Done(): + return latestHeight, true + + case blockHeight, ok := <-m.blockCh: + if !ok { + // channel is closed + return 0, false + } + if blockHeight > latestHeight { + latestHeight = blockHeight + } + } + } +} + var _ sdk.AnteDecorator = (*UnorderedTxDecorator)(nil) // UnorderedTxDecorator defines an AnteHandler decorator that is responsible for @@ -27,9 +158,10 @@ var _ sdk.AnteDecorator = (*UnorderedTxDecorator)(nil) type UnorderedTxDecorator struct { // maxUnOrderedTTL defines the maximum TTL a transaction can define. maxUnOrderedTTL uint64 + txManager *UnorderedTxManager } -func NewUnorderedTxDecorator(maxTTL uint64) *UnorderedTxDecorator { +func NewUnorderedTxDecorator(maxTTL uint64, m *UnorderedTxManager) *UnorderedTxDecorator { return &UnorderedTxDecorator{ maxUnOrderedTTL: maxTTL, } @@ -50,4 +182,16 @@ func (d *UnorderedTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b if unorderedTx.GetTimeoutHeight() > uint64(ctx.BlockHeight())+d.maxUnOrderedTTL { return ctx, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "unordered tx ttl exceeds %d", d.maxUnOrderedTTL) } + + // // check for duplicates + // if d.m.Contains(tx.Hash()) { + // return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "tx is duplicated") + // } + + // if !ctx.IsCheckTx() { + // // a new tx included in the block, add the hash to the unordered tx manager + // d.m.Add(tx.Hash(), tx.TimeoutHeight()) + // } + + return next(ctx, tx, simulate) } From dbb80dda2229534a9b8123cd72594a0975250fe9 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Mon, 11 Dec 2023 15:17:56 -0800 Subject: [PATCH 11/20] updates --- x/auth/ante/unordered.go | 29 +++++++---- x/auth/ante/unordered_test.go | 97 +++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 x/auth/ante/unordered_test.go diff --git a/x/auth/ante/unordered.go b/x/auth/ante/unordered.go index 8fbe885eb50b..110c9255b950 100644 --- a/x/auth/ante/unordered.go +++ b/x/auth/ante/unordered.go @@ -2,6 +2,7 @@ package ante import ( "context" + "crypto/sha256" "sync" "time" @@ -25,6 +26,8 @@ type TxHash [32]byte type UnorderedTxManager struct { // blockCh defines a channel to receive newly committed block heights blockCh chan uint64 + // doneCh allows us to ensure the purgeLoop has gracefully terminated prior to closing + doneCh chan struct{} mu sync.RWMutex // txHashes defines a map from tx hash -> TTL value, which is used for duplicate @@ -36,6 +39,7 @@ type UnorderedTxManager struct { func NewUnorderedTxManager() *UnorderedTxManager { m := &UnorderedTxManager{ blockCh: make(chan uint64, 16), + doneCh: make(chan struct{}), txHashes: make(map[TxHash]uint64), } @@ -48,6 +52,7 @@ func (m *UnorderedTxManager) Start() { func (m *UnorderedTxManager) Close() error { close(m.blockCh) + <-m.doneCh m.blockCh = nil return nil @@ -114,7 +119,8 @@ func (m *UnorderedTxManager) purgeLoop() { latestHeight, ok := m.batchReceive(ctx) if !ok { // channel closed - break + m.doneCh <- struct{}{} + return } hashes := m.expiredTxs(latestHeight) @@ -155,6 +161,9 @@ var _ sdk.AnteDecorator = (*UnorderedTxDecorator)(nil) // 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 memory 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 UnorderedTxManager. type UnorderedTxDecorator struct { // maxUnOrderedTTL defines the maximum TTL a transaction can define. maxUnOrderedTTL uint64 @@ -183,15 +192,17 @@ func (d *UnorderedTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b return ctx, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "unordered tx ttl exceeds %d", d.maxUnOrderedTTL) } - // // check for duplicates - // if d.m.Contains(tx.Hash()) { - // return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "tx is duplicated") - // } + txHash := sha256.Sum256(ctx.TxBytes()) + + // check for duplicates + if d.txManager.Contains(txHash) { + return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "tx %X is duplicated") + } - // if !ctx.IsCheckTx() { - // // a new tx included in the block, add the hash to the unordered tx manager - // d.m.Add(tx.Hash(), tx.TimeoutHeight()) - // } + if ctx.ExecMode() == sdk.ExecModeFinalize { + // a new tx included in the block, add the hash to the unordered tx manager + d.txManager.Add(txHash, unorderedTx.GetTimeoutHeight()) + } return next(ctx, tx, simulate) } diff --git a/x/auth/ante/unordered_test.go b/x/auth/ante/unordered_test.go new file mode 100644 index 000000000000..17607de390a9 --- /dev/null +++ b/x/auth/ante/unordered_test.go @@ -0,0 +1,97 @@ +package ante_test + +import ( + "testing" + "time" + + "cosmossdk.io/x/auth/ante" + "github.com/stretchr/testify/require" +) + +func TestUnorderedTxManager_Close(t *testing.T) { + txm := ante.NewUnorderedTxManager() + require.NoError(t, txm.Close()) + require.Panics(t, func() { txm.Close() }) +} + +func TestUnorderedTxManager_SimpleSize(t *testing.T) { + txm := ante.NewUnorderedTxManager() + defer txm.Close() + + txm.Add([32]byte{0xFF}, 100) + txm.Add([32]byte{0xAA}, 100) + txm.Add([32]byte{0xCC}, 100) + + require.Equal(t, 3, txm.Size()) +} + +func TestUnorderedTxManager_SimpleContains(t *testing.T) { + txm := ante.NewUnorderedTxManager() + defer txm.Close() + + for i := 0; i < 10; i++ { + txHash := [32]byte{byte(i)} + txm.Add(txHash, 100) + require.True(t, txm.Contains(txHash)) + } + + for i := 10; i < 20; i++ { + txHash := [32]byte{byte(i)} + require.False(t, txm.Contains(txHash)) + } +} + +func TestUnorderedTxManager_Flow(t *testing.T) { + txm := ante.NewUnorderedTxManager() + defer txm.Close() + + txm.Start() + + // Seed the manager with a txs, some of which should eventually be purged and + // the others will remain. Txs with TTL less than or equal to 50 should be purged. + for i := 1; i <= 100; i++ { + txHash := [32]byte{byte(i)} + + if i <= 50 { + txm.Add(txHash, uint64(i)) + } else { + txm.Add(txHash, 100) + } + } + + // start a goroutine that mimics new blocks being made every 500ms + doneBlockCh := make(chan bool) + go func() { + ticker := time.NewTicker(time.Millisecond * 500) + defer ticker.Stop() + + var ( + height uint64 = 1 + i = 101 + ) + for range ticker.C { + txm.OnNewBlock(height) + height++ + + if height > 51 { + doneBlockCh <- true + return + } else { + txm.Add([32]byte{byte(i)}, 50) + } + } + }() + + // Eventually all the txs that should be expired by block 50 should be purged. + // The remaining txs should remain. + require.Eventually( + t, + func() bool { + return txm.Size() == 50 + }, + 2*time.Minute, + 5*time.Second, + ) + + <-doneBlockCh +} From f88f3833f961ced6ff53dfdcf56f95043f9a3b96 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Mon, 11 Dec 2023 15:31:51 -0800 Subject: [PATCH 12/20] updates --- x/auth/ante/unordered.go | 11 +++++------ x/auth/ante/unordered_test.go | 6 ++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/x/auth/ante/unordered.go b/x/auth/ante/unordered.go index 110c9255b950..6bf8a144167e 100644 --- a/x/auth/ante/unordered.go +++ b/x/auth/ante/unordered.go @@ -113,10 +113,7 @@ func (m *UnorderedTxManager) purge(txHashes []TxHash) { // purgeLoop removes expired tx hashes in the background func (m *UnorderedTxManager) purgeLoop() { for { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - latestHeight, ok := m.batchReceive(ctx) + latestHeight, ok := m.batchReceive() if !ok { // channel closed m.doneCh <- struct{}{} @@ -130,9 +127,11 @@ func (m *UnorderedTxManager) purgeLoop() { } } -func (m *UnorderedTxManager) batchReceive(ctx context.Context) (uint64, bool) { - var latestHeight uint64 +func (m *UnorderedTxManager) batchReceive() (uint64, bool) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + var latestHeight uint64 for { select { case <-ctx.Done(): diff --git a/x/auth/ante/unordered_test.go b/x/auth/ante/unordered_test.go index 17607de390a9..550a77a05f55 100644 --- a/x/auth/ante/unordered_test.go +++ b/x/auth/ante/unordered_test.go @@ -10,6 +10,8 @@ import ( func TestUnorderedTxManager_Close(t *testing.T) { txm := ante.NewUnorderedTxManager() + txm.Start() + require.NoError(t, txm.Close()) require.Panics(t, func() { txm.Close() }) } @@ -18,6 +20,8 @@ func TestUnorderedTxManager_SimpleSize(t *testing.T) { txm := ante.NewUnorderedTxManager() defer txm.Close() + txm.Start() + txm.Add([32]byte{0xFF}, 100) txm.Add([32]byte{0xAA}, 100) txm.Add([32]byte{0xCC}, 100) @@ -29,6 +33,8 @@ func TestUnorderedTxManager_SimpleContains(t *testing.T) { txm := ante.NewUnorderedTxManager() defer txm.Close() + txm.Start() + for i := 0; i < 10; i++ { txHash := [32]byte{byte(i)} txm.Add(txHash, 100) From deb44e18105886bcaa03925774e74d1178b55b66 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Tue, 12 Dec 2023 09:20:45 -0800 Subject: [PATCH 13/20] updates --- client/tx_config.go | 1 + x/auth/ante/unordered.go | 1 + x/auth/ante/unordered_test.go | 132 ++++++++++++++++++++++++++++++++++ x/auth/tx/builder.go | 12 ++++ 4 files changed, 146 insertions(+) diff --git a/client/tx_config.go b/client/tx_config.go index c28139be909a..fe60fe4625c1 100644 --- a/client/tx_config.go +++ b/client/tx_config.go @@ -48,6 +48,7 @@ type ( SetFeePayer(feePayer sdk.AccAddress) SetGasLimit(limit uint64) SetTimeoutHeight(height uint64) + SetUnordered(v bool) SetFeeGranter(feeGranter sdk.AccAddress) AddAuxSignerData(tx.AuxSignerData) error } diff --git a/x/auth/ante/unordered.go b/x/auth/ante/unordered.go index 6bf8a144167e..4abc12c92d14 100644 --- a/x/auth/ante/unordered.go +++ b/x/auth/ante/unordered.go @@ -172,6 +172,7 @@ type UnorderedTxDecorator struct { func NewUnorderedTxDecorator(maxTTL uint64, m *UnorderedTxManager) *UnorderedTxDecorator { return &UnorderedTxDecorator{ maxUnOrderedTTL: maxTTL, + txManager: m, } } diff --git a/x/auth/ante/unordered_test.go b/x/auth/ante/unordered_test.go index 550a77a05f55..3537e49694fa 100644 --- a/x/auth/ante/unordered_test.go +++ b/x/auth/ante/unordered_test.go @@ -1,11 +1,17 @@ package ante_test import ( + "crypto/sha256" "testing" "time" "cosmossdk.io/x/auth/ante" "github.com/stretchr/testify/require" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" ) func TestUnorderedTxManager_Close(t *testing.T) { @@ -101,3 +107,129 @@ func TestUnorderedTxManager_Flow(t *testing.T) { <-doneBlockCh } + +func TestUnorderedTxDecorator_OrderedTx(t *testing.T) { + txm := ante.NewUnorderedTxManager() + defer txm.Close() + + txm.Start() + + chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(ante.DefaultMaxUnOrderedTTL, txm)) + + tx, txBz := genUnorderedTx(t, false, 0) + ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100) + + _, err := chain(ctx, tx, false) + require.NoError(t, err) +} + +func TestUnorderedTxDecorator_UnorderedTx_NoTTL(t *testing.T) { + txm := ante.NewUnorderedTxManager() + defer txm.Close() + + txm.Start() + + chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(ante.DefaultMaxUnOrderedTTL, txm)) + + tx, txBz := genUnorderedTx(t, true, 0) + ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100) + + _, err := chain(ctx, tx, false) + require.Error(t, err) +} + +func TestUnorderedTxDecorator_UnorderedTx_InvalidTTL(t *testing.T) { + txm := ante.NewUnorderedTxManager() + defer txm.Close() + + txm.Start() + + chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(ante.DefaultMaxUnOrderedTTL, txm)) + + tx, txBz := genUnorderedTx(t, true, 100+ante.DefaultMaxUnOrderedTTL+1) + ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100) + + _, err := chain(ctx, tx, false) + require.Error(t, err) +} + +func TestUnorderedTxDecorator_UnorderedTx_AlreadyExists(t *testing.T) { + txm := ante.NewUnorderedTxManager() + defer txm.Close() + + txm.Start() + + chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(ante.DefaultMaxUnOrderedTTL, txm)) + + tx, txBz := genUnorderedTx(t, true, 150) + ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100) + + txHash := sha256.Sum256(txBz) + txm.Add(txHash, 150) + + _, err := chain(ctx, tx, false) + require.Error(t, err) +} + +func TestUnorderedTxDecorator_UnorderedTx_ValidCheckTx(t *testing.T) { + txm := ante.NewUnorderedTxManager() + defer txm.Close() + + txm.Start() + + chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(ante.DefaultMaxUnOrderedTTL, txm)) + + tx, txBz := genUnorderedTx(t, true, 150) + ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100).WithExecMode(sdk.ExecModeCheck) + + _, err := chain(ctx, tx, false) + require.NoError(t, err) +} + +func TestUnorderedTxDecorator_UnorderedTx_ValidDeliverTx(t *testing.T) { + txm := ante.NewUnorderedTxManager() + defer txm.Close() + + txm.Start() + + chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(ante.DefaultMaxUnOrderedTTL, txm)) + + tx, txBz := genUnorderedTx(t, true, 150) + ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100).WithExecMode(sdk.ExecModeFinalize) + + _, err := chain(ctx, tx, false) + require.NoError(t, err) + + txHash := sha256.Sum256(txBz) + require.True(t, txm.Contains(txHash)) +} + +func genUnorderedTx(t *testing.T, unordered bool, ttl uint64) (sdk.Tx, []byte) { + t.Helper() + + s := SetupTestSuite(t, true) + s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() + + // keys and addresses + 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(unordered) + s.txBuilder.SetTimeoutHeight(ttl) + + 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) + + txBz, err := s.encCfg.TxConfig.TxEncoder()(tx) + require.NoError(t, err) + + return tx, txBz +} diff --git a/x/auth/tx/builder.go b/x/auth/tx/builder.go index 3539c3dbcff3..ba7955b0ec57 100644 --- a/x/auth/tx/builder.go +++ b/x/auth/tx/builder.go @@ -221,6 +221,11 @@ func (w *wrapper) GetTimeoutHeight() uint64 { return w.tx.Body.TimeoutHeight } +// GetUnordered returns the transaction's unordered field (if set). +func (w *wrapper) GetUnordered() bool { + return w.tx.Body.Unordered +} + func (w *wrapper) GetSignaturesV2() ([]signing.SignatureV2, error) { signerInfos := w.tx.AuthInfo.SignerInfos sigs := w.tx.Signatures @@ -283,6 +288,13 @@ func (w *wrapper) SetTimeoutHeight(height uint64) { w.bodyBz = nil } +func (w *wrapper) SetUnordered(v bool) { + w.tx.Body.Unordered = v + + // set bodyBz to nil because the cached bodyBz no longer matches tx.Body + w.bodyBz = nil +} + func (w *wrapper) SetMemo(memo string) { w.tx.Body.Memo = memo From fb216de9545c7396ef27a0b6a0957d44aea94366 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Tue, 12 Dec 2023 09:50:02 -0800 Subject: [PATCH 14/20] updates --- client/flags/flags.go | 2 ++ client/tx/factory.go | 10 ++++++++++ x/auth/signing/sig_verifiable_tx.go | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/client/flags/flags.go b/client/flags/flags.go index 26a93a5e6e2f..f2af30eb0714 100644 --- a/client/flags/flags.go +++ b/client/flags/flags.go @@ -74,6 +74,7 @@ const ( FlagOffset = "offset" FlagCountTotal = "count-total" FlagTimeoutHeight = "timeout-height" + FlagUnordered = "unordered" FlagKeyAlgorithm = "algo" FlagKeyType = "key-type" FlagFeePayer = "fee-payer" @@ -136,6 +137,7 @@ func AddTxFlagsToCmd(cmd *cobra.Command) { f.BoolP(FlagSkipConfirmation, "y", false, "Skip tx broadcasting prompt confirmation") f.String(FlagSignMode, "", "Choose sign mode (direct|amino-json|direct-aux|textual), this is an advanced feature") f.Uint64(FlagTimeoutHeight, 0, "Set a block timeout height to prevent the tx from being committed past a certain height") + f.Bool(FlagUnordered, false, "Enable unordered transaction delivery; must be used in conjunction with --timeout-height") f.String(FlagFeePayer, "", "Fee payer pays fees for the transaction instead of deducting from the signer") f.String(FlagFeeGranter, "", "Fee granter grants fees for the transaction") f.String(FlagTip, "", "Tip is the amount that is going to be transferred to the fee payer on the target chain. This flag is only valid when used with --aux, and is ignored if the target chain didn't enable the TipDecorator") diff --git a/client/tx/factory.go b/client/tx/factory.go index 76829eda7745..15294e3bc84e 100644 --- a/client/tx/factory.go +++ b/client/tx/factory.go @@ -36,6 +36,7 @@ type Factory struct { gasAdjustment float64 chainID string fromName string + unordered bool offline bool generateOnly bool memo string @@ -86,6 +87,7 @@ func NewFactoryCLI(clientCtx client.Context, flagSet *pflag.FlagSet) (Factory, e gasAdj := clientCtx.Viper.GetFloat64(flags.FlagGasAdjustment) memo := clientCtx.Viper.GetString(flags.FlagNote) timeoutHeight := clientCtx.Viper.GetUint64(flags.FlagTimeoutHeight) + unordered := clientCtx.Viper.GetBool(flags.FlagUnordered) gasStr := clientCtx.Viper.GetString(flags.FlagGas) gasSetting, _ := flags.ParseGasSetting(gasStr) @@ -103,6 +105,7 @@ func NewFactoryCLI(clientCtx client.Context, flagSet *pflag.FlagSet) (Factory, e accountNumber: accNum, sequence: accSeq, timeoutHeight: timeoutHeight, + unordered: unordered, gasAdjustment: gasAdj, memo: memo, signMode: signMode, @@ -132,6 +135,7 @@ func (f Factory) Fees() sdk.Coins { return f.fees } func (f Factory) GasPrices() sdk.DecCoins { return f.gasPrices } func (f Factory) AccountRetriever() client.AccountRetriever { return f.accountRetriever } func (f Factory) TimeoutHeight() uint64 { return f.timeoutHeight } +func (f Factory) Unordered() bool { return f.unordered } // SimulateAndExecute returns the option to simulate and then execute the transaction // using the gas from the simulation results @@ -237,6 +241,12 @@ func (f Factory) WithTimeoutHeight(height uint64) Factory { return f } +// WithUnordered returns a copy of the Factory with an updated unordered field. +func (f Factory) WithUnordered(v bool) Factory { + f.unordered = v + return f +} + // WithFeeGranter returns a copy of the Factory with an updated fee granter. func (f Factory) WithFeeGranter(fg sdk.AccAddress) Factory { f.feeGranter = fg diff --git a/x/auth/signing/sig_verifiable_tx.go b/x/auth/signing/sig_verifiable_tx.go index c8a752e7e475..4c6934c1c8e7 100644 --- a/x/auth/signing/sig_verifiable_tx.go +++ b/x/auth/signing/sig_verifiable_tx.go @@ -22,6 +22,6 @@ type Tx interface { types.TxWithMemo types.FeeTx - types.TxWithTimeoutHeight + types.TxWithUnordered types.HasValidateBasic } From 6194fdb9d0c0395aac3044d3c332f72cf91fa4c3 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Tue, 12 Dec 2023 09:57:23 -0800 Subject: [PATCH 15/20] lint++ --- x/auth/ante/unordered_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x/auth/ante/unordered_test.go b/x/auth/ante/unordered_test.go index 3537e49694fa..3b85b69434bb 100644 --- a/x/auth/ante/unordered_test.go +++ b/x/auth/ante/unordered_test.go @@ -5,9 +5,10 @@ import ( "testing" "time" - "cosmossdk.io/x/auth/ante" "github.com/stretchr/testify/require" + "cosmossdk.io/x/auth/ante" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" From 6c5f722052d4f927f35400e4774cc4b6ed3c3a3d Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Wed, 13 Dec 2023 14:02:59 -0800 Subject: [PATCH 16/20] updates --- x/auth/ante/unordered.go | 145 +----------------------- x/auth/ante/unordered_test.go | 122 +++----------------- x/auth/ante/unorderedtx/manager.go | 144 +++++++++++++++++++++++ x/auth/ante/unorderedtx/manager_test.go | 104 +++++++++++++++++ 4 files changed, 265 insertions(+), 250 deletions(-) create mode 100644 x/auth/ante/unorderedtx/manager.go create mode 100644 x/auth/ante/unorderedtx/manager_test.go diff --git a/x/auth/ante/unordered.go b/x/auth/ante/unordered.go index 4abc12c92d14..9e0a933dd703 100644 --- a/x/auth/ante/unordered.go +++ b/x/auth/ante/unordered.go @@ -1,154 +1,15 @@ package ante import ( - "context" "crypto/sha256" - "sync" - "time" errorsmod "cosmossdk.io/errors" + "cosmossdk.io/x/auth/ante/unorderedtx" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) -const ( - // DefaultMaxUnOrderedTTL defines the default maximum TTL an un-ordered transaction - // can set. - DefaultMaxUnOrderedTTL = 1024 -) - -// TxHash defines a transaction hash type alias, which is a fixed array of 32 bytes. -type TxHash [32]byte - -// UnorderedTxManager contains the tx hash dictionary for duplicates checking, -// and expire them when block production progresses. -type UnorderedTxManager struct { - // blockCh defines a channel to receive newly committed block heights - blockCh chan uint64 - // doneCh allows us to ensure the purgeLoop has gracefully terminated prior to closing - doneCh chan struct{} - - mu sync.RWMutex - // txHashes defines a map from tx hash -> TTL value, which is used for duplicate - // checking and replay protection, as well as purging the map when the TTL is - // expired. - txHashes map[TxHash]uint64 -} - -func NewUnorderedTxManager() *UnorderedTxManager { - m := &UnorderedTxManager{ - blockCh: make(chan uint64, 16), - doneCh: make(chan struct{}), - txHashes: make(map[TxHash]uint64), - } - - return m -} - -func (m *UnorderedTxManager) Start() { - go m.purgeLoop() -} - -func (m *UnorderedTxManager) Close() error { - close(m.blockCh) - <-m.doneCh - m.blockCh = nil - - return nil -} - -func (m *UnorderedTxManager) Contains(hash TxHash) bool { - m.mu.RLock() - defer m.mu.RUnlock() - - _, ok := m.txHashes[hash] - return ok -} - -func (m *UnorderedTxManager) Size() int { - m.mu.RLock() - defer m.mu.RUnlock() - - return len(m.txHashes) -} - -func (m *UnorderedTxManager) Add(txHash TxHash, ttl uint64) { - m.mu.Lock() - defer m.mu.Unlock() - - m.txHashes[txHash] = ttl -} - -// OnNewBlock send the latest block number to the background purge loop, which -// should be called in ABCI Commit event. -func (m *UnorderedTxManager) OnNewBlock(blockHeight uint64) { - m.blockCh <- blockHeight -} - -// expiredTxs returns expired tx hashes based on the provided block height. -func (m *UnorderedTxManager) expiredTxs(blockHeight uint64) []TxHash { - m.mu.RLock() - defer m.mu.RUnlock() - - var result []TxHash - for txHash, ttl := range m.txHashes { - if blockHeight > ttl { - result = append(result, txHash) - } - } - - return result -} - -func (m *UnorderedTxManager) purge(txHashes []TxHash) { - m.mu.Lock() - defer m.mu.Unlock() - - for _, txHash := range txHashes { - delete(m.txHashes, txHash) - } -} - -// purgeLoop removes expired tx hashes in the background -func (m *UnorderedTxManager) purgeLoop() { - for { - latestHeight, ok := m.batchReceive() - if !ok { - // channel closed - m.doneCh <- struct{}{} - return - } - - hashes := m.expiredTxs(latestHeight) - if len(hashes) > 0 { - m.purge(hashes) - } - } -} - -func (m *UnorderedTxManager) batchReceive() (uint64, bool) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - var latestHeight uint64 - for { - select { - case <-ctx.Done(): - return latestHeight, true - - case blockHeight, ok := <-m.blockCh: - if !ok { - // channel is closed - return 0, false - } - if blockHeight > latestHeight { - latestHeight = blockHeight - } - } - } -} - var _ sdk.AnteDecorator = (*UnorderedTxDecorator)(nil) // UnorderedTxDecorator defines an AnteHandler decorator that is responsible for @@ -166,10 +27,10 @@ var _ sdk.AnteDecorator = (*UnorderedTxDecorator)(nil) type UnorderedTxDecorator struct { // maxUnOrderedTTL defines the maximum TTL a transaction can define. maxUnOrderedTTL uint64 - txManager *UnorderedTxManager + txManager *unorderedtx.Manager } -func NewUnorderedTxDecorator(maxTTL uint64, m *UnorderedTxManager) *UnorderedTxDecorator { +func NewUnorderedTxDecorator(maxTTL uint64, m *unorderedtx.Manager) *UnorderedTxDecorator { return &UnorderedTxDecorator{ maxUnOrderedTTL: maxTTL, txManager: m, diff --git a/x/auth/ante/unordered_test.go b/x/auth/ante/unordered_test.go index 3b85b69434bb..13792822d07c 100644 --- a/x/auth/ante/unordered_test.go +++ b/x/auth/ante/unordered_test.go @@ -3,11 +3,11 @@ package ante_test import ( "crypto/sha256" "testing" - "time" "github.com/stretchr/testify/require" "cosmossdk.io/x/auth/ante" + "cosmossdk.io/x/auth/ante/unorderedtx" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/testutil/testdata" @@ -15,107 +15,13 @@ import ( "github.com/cosmos/cosmos-sdk/types/tx/signing" ) -func TestUnorderedTxManager_Close(t *testing.T) { - txm := ante.NewUnorderedTxManager() - txm.Start() - - require.NoError(t, txm.Close()) - require.Panics(t, func() { txm.Close() }) -} - -func TestUnorderedTxManager_SimpleSize(t *testing.T) { - txm := ante.NewUnorderedTxManager() - defer txm.Close() - - txm.Start() - - txm.Add([32]byte{0xFF}, 100) - txm.Add([32]byte{0xAA}, 100) - txm.Add([32]byte{0xCC}, 100) - - require.Equal(t, 3, txm.Size()) -} - -func TestUnorderedTxManager_SimpleContains(t *testing.T) { - txm := ante.NewUnorderedTxManager() - defer txm.Close() - - txm.Start() - - for i := 0; i < 10; i++ { - txHash := [32]byte{byte(i)} - txm.Add(txHash, 100) - require.True(t, txm.Contains(txHash)) - } - - for i := 10; i < 20; i++ { - txHash := [32]byte{byte(i)} - require.False(t, txm.Contains(txHash)) - } -} - -func TestUnorderedTxManager_Flow(t *testing.T) { - txm := ante.NewUnorderedTxManager() - defer txm.Close() - - txm.Start() - - // Seed the manager with a txs, some of which should eventually be purged and - // the others will remain. Txs with TTL less than or equal to 50 should be purged. - for i := 1; i <= 100; i++ { - txHash := [32]byte{byte(i)} - - if i <= 50 { - txm.Add(txHash, uint64(i)) - } else { - txm.Add(txHash, 100) - } - } - - // start a goroutine that mimics new blocks being made every 500ms - doneBlockCh := make(chan bool) - go func() { - ticker := time.NewTicker(time.Millisecond * 500) - defer ticker.Stop() - - var ( - height uint64 = 1 - i = 101 - ) - for range ticker.C { - txm.OnNewBlock(height) - height++ - - if height > 51 { - doneBlockCh <- true - return - } else { - txm.Add([32]byte{byte(i)}, 50) - } - } - }() - - // Eventually all the txs that should be expired by block 50 should be purged. - // The remaining txs should remain. - require.Eventually( - t, - func() bool { - return txm.Size() == 50 - }, - 2*time.Minute, - 5*time.Second, - ) - - <-doneBlockCh -} - func TestUnorderedTxDecorator_OrderedTx(t *testing.T) { - txm := ante.NewUnorderedTxManager() + txm := unorderedtx.NewManager() defer txm.Close() txm.Start() - chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(ante.DefaultMaxUnOrderedTTL, txm)) + chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, txm)) tx, txBz := genUnorderedTx(t, false, 0) ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100) @@ -125,12 +31,12 @@ func TestUnorderedTxDecorator_OrderedTx(t *testing.T) { } func TestUnorderedTxDecorator_UnorderedTx_NoTTL(t *testing.T) { - txm := ante.NewUnorderedTxManager() + txm := unorderedtx.NewManager() defer txm.Close() txm.Start() - chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(ante.DefaultMaxUnOrderedTTL, txm)) + chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, txm)) tx, txBz := genUnorderedTx(t, true, 0) ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100) @@ -140,14 +46,14 @@ func TestUnorderedTxDecorator_UnorderedTx_NoTTL(t *testing.T) { } func TestUnorderedTxDecorator_UnorderedTx_InvalidTTL(t *testing.T) { - txm := ante.NewUnorderedTxManager() + txm := unorderedtx.NewManager() defer txm.Close() txm.Start() - chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(ante.DefaultMaxUnOrderedTTL, txm)) + chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, txm)) - tx, txBz := genUnorderedTx(t, true, 100+ante.DefaultMaxUnOrderedTTL+1) + tx, txBz := genUnorderedTx(t, true, 100+unorderedtx.DefaultMaxUnOrderedTTL+1) ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100) _, err := chain(ctx, tx, false) @@ -155,12 +61,12 @@ func TestUnorderedTxDecorator_UnorderedTx_InvalidTTL(t *testing.T) { } func TestUnorderedTxDecorator_UnorderedTx_AlreadyExists(t *testing.T) { - txm := ante.NewUnorderedTxManager() + txm := unorderedtx.NewManager() defer txm.Close() txm.Start() - chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(ante.DefaultMaxUnOrderedTTL, txm)) + chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, txm)) tx, txBz := genUnorderedTx(t, true, 150) ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100) @@ -173,12 +79,12 @@ func TestUnorderedTxDecorator_UnorderedTx_AlreadyExists(t *testing.T) { } func TestUnorderedTxDecorator_UnorderedTx_ValidCheckTx(t *testing.T) { - txm := ante.NewUnorderedTxManager() + txm := unorderedtx.NewManager() defer txm.Close() txm.Start() - chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(ante.DefaultMaxUnOrderedTTL, txm)) + chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, txm)) tx, txBz := genUnorderedTx(t, true, 150) ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100).WithExecMode(sdk.ExecModeCheck) @@ -188,12 +94,12 @@ func TestUnorderedTxDecorator_UnorderedTx_ValidCheckTx(t *testing.T) { } func TestUnorderedTxDecorator_UnorderedTx_ValidDeliverTx(t *testing.T) { - txm := ante.NewUnorderedTxManager() + txm := unorderedtx.NewManager() defer txm.Close() txm.Start() - chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(ante.DefaultMaxUnOrderedTTL, txm)) + chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, txm)) tx, txBz := genUnorderedTx(t, true, 150) ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100).WithExecMode(sdk.ExecModeFinalize) diff --git a/x/auth/ante/unorderedtx/manager.go b/x/auth/ante/unorderedtx/manager.go new file mode 100644 index 000000000000..3a6474baacbc --- /dev/null +++ b/x/auth/ante/unorderedtx/manager.go @@ -0,0 +1,144 @@ +package unorderedtx + +import ( + "context" + "sync" + "time" +) + +const ( + // DefaultMaxUnOrderedTTL defines the default maximum TTL an un-ordered transaction + // can set. + DefaultMaxUnOrderedTTL = 1024 +) + +// TxHash defines a transaction hash type alias, which is a fixed array of 32 bytes. +type TxHash [32]byte + +// Manager contains the tx hash dictionary for duplicates checking, and expire +// them when block production progresses. +type Manager struct { + // blockCh defines a channel to receive newly committed block heights + blockCh chan uint64 + // doneCh allows us to ensure the purgeLoop has gracefully terminated prior to closing + doneCh chan struct{} + + mu sync.RWMutex + // txHashes defines a map from tx hash -> TTL value, which is used for duplicate + // checking and replay protection, as well as purging the map when the TTL is + // expired. + txHashes map[TxHash]uint64 +} + +func NewManager() *Manager { + m := &Manager{ + blockCh: make(chan uint64, 16), + doneCh: make(chan struct{}), + txHashes: make(map[TxHash]uint64), + } + + return m +} + +func (m *Manager) Start() { + go m.purgeLoop() +} + +func (m *Manager) Close() error { + close(m.blockCh) + <-m.doneCh + m.blockCh = nil + + return nil +} + +func (m *Manager) Contains(hash TxHash) bool { + m.mu.RLock() + defer m.mu.RUnlock() + + _, ok := m.txHashes[hash] + return ok +} + +func (m *Manager) Size() int { + m.mu.RLock() + defer m.mu.RUnlock() + + return len(m.txHashes) +} + +func (m *Manager) Add(txHash TxHash, ttl uint64) { + m.mu.Lock() + defer m.mu.Unlock() + + m.txHashes[txHash] = ttl +} + +// OnNewBlock send the latest block number to the background purge loop, which +// should be called in ABCI Commit event. +func (m *Manager) OnNewBlock(blockHeight uint64) { + m.blockCh <- blockHeight +} + +// expiredTxs returns expired tx hashes based on the provided block height. +func (m *Manager) expiredTxs(blockHeight uint64) []TxHash { + m.mu.RLock() + defer m.mu.RUnlock() + + var result []TxHash + for txHash, ttl := range m.txHashes { + if blockHeight > ttl { + result = append(result, txHash) + } + } + + return result +} + +func (m *Manager) purge(txHashes []TxHash) { + m.mu.Lock() + defer m.mu.Unlock() + + for _, txHash := range txHashes { + delete(m.txHashes, txHash) + } +} + +// purgeLoop removes expired tx hashes in the background +func (m *Manager) purgeLoop() { + for { + latestHeight, ok := m.batchReceive() + if !ok { + // channel closed + m.doneCh <- struct{}{} + return + } + + hashes := m.expiredTxs(latestHeight) + if len(hashes) > 0 { + m.purge(hashes) + } + } +} + +func (m *Manager) batchReceive() (uint64, bool) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var latestHeight uint64 + for { + select { + case <-ctx.Done(): + return latestHeight, true + + case blockHeight, ok := <-m.blockCh: + if !ok { + // channel is closed + return 0, false + } + if blockHeight > latestHeight { + latestHeight = blockHeight + } + } + } +} diff --git a/x/auth/ante/unorderedtx/manager_test.go b/x/auth/ante/unorderedtx/manager_test.go new file mode 100644 index 000000000000..caf03a3c269b --- /dev/null +++ b/x/auth/ante/unorderedtx/manager_test.go @@ -0,0 +1,104 @@ +package unorderedtx_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/x/auth/ante/unorderedtx" +) + +func TestUnorderedTxManager_Close(t *testing.T) { + txm := unorderedtx.NewManager() + txm.Start() + + require.NoError(t, txm.Close()) + require.Panics(t, func() { txm.Close() }) +} + +func TestUnorderedTxManager_SimpleSize(t *testing.T) { + txm := unorderedtx.NewManager() + defer txm.Close() + + txm.Start() + + txm.Add([32]byte{0xFF}, 100) + txm.Add([32]byte{0xAA}, 100) + txm.Add([32]byte{0xCC}, 100) + + require.Equal(t, 3, txm.Size()) +} + +func TestUnorderedTxManager_SimpleContains(t *testing.T) { + txm := unorderedtx.NewManager() + defer txm.Close() + + txm.Start() + + for i := 0; i < 10; i++ { + txHash := [32]byte{byte(i)} + txm.Add(txHash, 100) + require.True(t, txm.Contains(txHash)) + } + + for i := 10; i < 20; i++ { + txHash := [32]byte{byte(i)} + require.False(t, txm.Contains(txHash)) + } +} + +func TestUnorderedTxManager_Flow(t *testing.T) { + txm := unorderedtx.NewManager() + defer txm.Close() + + txm.Start() + + // Seed the manager with a txs, some of which should eventually be purged and + // the others will remain. Txs with TTL less than or equal to 50 should be purged. + for i := 1; i <= 100; i++ { + txHash := [32]byte{byte(i)} + + if i <= 50 { + txm.Add(txHash, uint64(i)) + } else { + txm.Add(txHash, 100) + } + } + + // start a goroutine that mimics new blocks being made every 500ms + doneBlockCh := make(chan bool) + go func() { + ticker := time.NewTicker(time.Millisecond * 500) + defer ticker.Stop() + + var ( + height uint64 = 1 + i = 101 + ) + for range ticker.C { + txm.OnNewBlock(height) + height++ + + if height > 51 { + doneBlockCh <- true + return + } else { + txm.Add([32]byte{byte(i)}, 50) + } + } + }() + + // Eventually all the txs that should be expired by block 50 should be purged. + // The remaining txs should remain. + require.Eventually( + t, + func() bool { + return txm.Size() == 50 + }, + 2*time.Minute, + 5*time.Second, + ) + + <-doneBlockCh +} From 453e270a04e9dede50d0f8f403ff98d0f24ab229 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Wed, 3 Jan 2024 14:09:48 -0500 Subject: [PATCH 17/20] feat: [ADR-070] Unordered Transactions (2/2) (#18739) Co-authored-by: yihuang --- UPGRADING.md | 75 +++++++++ .../architecture/adr-070-unordered-account.md | 20 ++- simapp/ante.go | 3 + simapp/app.go | 29 ++++ simapp/app_v2.go | 31 ++++ x/auth/ante/unordered.go | 13 +- x/auth/ante/unordered_test.go | 36 +++-- x/auth/ante/unorderedtx/manager.go | 147 +++++++++++++++++- x/auth/ante/unorderedtx/manager_test.go | 60 ++++++- x/auth/ante/unorderedtx/snapshotter.go | 92 +++++++++++ x/auth/ante/unorderedtx/snapshotter_test.go | 56 +++++++ 11 files changed, 530 insertions(+), 32 deletions(-) create mode 100644 x/auth/ante/unorderedtx/snapshotter.go create mode 100644 x/auth/ante/unorderedtx/snapshotter_test.go diff --git a/UPGRADING.md b/UPGRADING.md index c78ec3560413..51d50d3a8dd1 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -5,6 +5,81 @@ Note, always read the **SimApp** section for more information on application wir ## [Unreleased] +### Unordered Transactions + +The Cosmos SDK now supports unordered transactions. This means that transactions +can be executed in any order and doesn't require the client to deal with or manage +nonces. This also means the order of execution is not guaranteed. To enable unordered +transactions in your application: + +* Update the `App` constructor to create, load, and save the unordered transaction + manager. + + ```go + func NewApp(...) *App { + // ... + + // create, start, and load the unordered tx manager + utxDataDir := filepath.Join(cast.ToString(appOpts.Get(flags.FlagHome)), "data") + app.UnorderedTxManager = unorderedtx.NewManager(utxDataDir) + app.UnorderedTxManager.Start() + + if err := app.UnorderedTxManager.OnInit(); err != nil { + panic(fmt.Errorf("failed to initialize unordered tx manager: %w", err)) + } + } + ``` + +* Add the decorator to the existing AnteHandler chain, which should be as early + as possible. + + ```go + anteDecorators := []sdk.AnteDecorator{ + ante.NewSetUpContextDecorator(), + // ... + ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, app.UnorderedTxManager), + // ... + } + + return sdk.ChainAnteDecorators(anteDecorators...), nil + ``` + +* If the App has a SnapshotManager defined, you must also register the extension + for the TxManager. + + ```go + if manager := app.SnapshotManager(); manager != nil { + err := manager.RegisterExtensions(unorderedtx.NewSnapshotter(app.UnorderedTxManager)) + if err != nil { + panic(fmt.Errorf("failed to register snapshot extension: %s", err)) + } + } + ``` + +* Create or update the App's `Close()` method to close the unordered tx manager. + Note, this is critical as it ensures the manager's state is written to file + such that when the node restarts, it can recover the state to provide replay + protection. + + ```go + func (app *App) Close() error { + // ... + + // close the unordered tx manager + if e := app.UnorderedTxManager.Close(); e != nil { + err = errors.Join(err, e) + } + + return err + } + ``` + +To submit an unordered transaction, the client must set the `unordered` flag to +`true` and ensure a reasonable `timeout_height` is set. The `timeout_height` is +used as a TTL for the transaction and is used to provide replay protection. See +[ADR-070](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-070-unordered-account.md) +for more details. + ### Params * Params Migrations were removed. It is required to migrate to 0.50 prior to upgrading to .51. diff --git a/docs/architecture/adr-070-unordered-account.md b/docs/architecture/adr-070-unordered-account.md index ac63a4a40daa..c2d6e382f13b 100644 --- a/docs/architecture/adr-070-unordered-account.md +++ b/docs/architecture/adr-070-unordered-account.md @@ -278,14 +278,22 @@ func (d *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, Wire the `OnNewBlock` method of `UnorderedTxManager` into the BaseApp's ABCI `Commit` event. -### Start Up +### State Management -On start up, the node needs to re-fill the tx hash dictionary of `UnorderedTxManager` -by scanning `MaxUnOrderedTTL` number of historical blocks for existing un-expired -un-ordered transactions. +On start up, the node needs to ensure the TxManager's state contains all un-expired +transactions that have been committed to the chain. This is critical since if the +state is not properly initialized, the node will not reject duplicate transactions +and thus will not provide replay protection, and will likely get an app hash mismatch error. -An alternative design is to store the tx hash dictionary in kv store, then no need -to warm up on start up. +We propose to write all un-expired unordered transactions from the TxManager's to +file on disk. On start up, the node will read this file and re-populate the TxManager's +map. The write to file will happen when the node gracefully shuts down on `Close()`. + +Note, this is not a perfect solution, in the context of store v1. With store v2, +we can omit explicit file handling altogether and simply write the all the transactions +to non-consensus state, i.e State Storage (SS). + +Alternatively, we can write all the transactions to consensus state. ## Consequences diff --git a/simapp/ante.go b/simapp/ante.go index 918244a81405..8ff82263566d 100644 --- a/simapp/ante.go +++ b/simapp/ante.go @@ -4,6 +4,7 @@ import ( "errors" "cosmossdk.io/x/auth/ante" + "cosmossdk.io/x/auth/ante/unorderedtx" circuitante "cosmossdk.io/x/circuit/ante" sdk "github.com/cosmos/cosmos-sdk/types" @@ -13,6 +14,7 @@ import ( type HandlerOptions struct { ante.HandlerOptions CircuitKeeper circuitante.CircuitBreaker + TxManager *unorderedtx.Manager } // NewAnteHandler returns an AnteHandler that checks and increments sequence @@ -37,6 +39,7 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker), ante.NewValidateBasicDecorator(), ante.NewTxTimeoutHeightDecorator(), + ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, options.TxManager), ante.NewValidateMemoDecorator(options.AccountKeeper), ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker), diff --git a/simapp/app.go b/simapp/app.go index 4bffaaa09a7a..7007d9a62efe 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -26,6 +26,7 @@ import ( "cosmossdk.io/x/accounts/testing/counter" "cosmossdk.io/x/auth" "cosmossdk.io/x/auth/ante" + "cosmossdk.io/x/auth/ante/unorderedtx" authcodec "cosmossdk.io/x/auth/codec" authkeeper "cosmossdk.io/x/auth/keeper" "cosmossdk.io/x/auth/posthandler" @@ -169,6 +170,8 @@ type SimApp struct { ModuleManager *module.Manager BasicModuleManager module.BasicManager + UnorderedTxManager *unorderedtx.Manager + // simulation manager sm *module.SimulationManager @@ -519,6 +522,25 @@ func NewSimApp( } app.sm = module.NewSimulationManagerFromAppModules(app.ModuleManager.Modules, overrideModules) + // create, start, and load the unordered tx manager + utxDataDir := filepath.Join(cast.ToString(appOpts.Get(flags.FlagHome)), "data") + app.UnorderedTxManager = unorderedtx.NewManager(utxDataDir) + app.UnorderedTxManager.Start() + + if err := app.UnorderedTxManager.OnInit(); err != nil { + panic(fmt.Errorf("failed to initialize unordered tx manager: %w", err)) + } + + // register custom snapshot extensions (if any) + if manager := app.SnapshotManager(); manager != nil { + err := manager.RegisterExtensions( + unorderedtx.NewSnapshotter(app.UnorderedTxManager), + ) + if err != nil { + panic(fmt.Errorf("failed to register snapshot extension: %s", err)) + } + } + app.sm.RegisterStoreDecoders() // initialize stores @@ -579,6 +601,7 @@ func (app *SimApp) setAnteHandler(txConfig client.TxConfig) { SigGasConsumer: ante.DefaultSigVerificationGasConsumer, }, &app.CircuitKeeper, + app.UnorderedTxManager, }, ) if err != nil { @@ -600,6 +623,12 @@ func (app *SimApp) setPostHandler() { app.SetPostHandler(postHandler) } +// Close implements the Application interface and closes all necessary application +// resources. +func (app *SimApp) Close() error { + return app.UnorderedTxManager.Close() +} + // Name returns the name of the App func (app *SimApp) Name() string { return app.BaseApp.Name() } diff --git a/simapp/app_v2.go b/simapp/app_v2.go index 172e6e7af408..0c4ea6f63ac8 100644 --- a/simapp/app_v2.go +++ b/simapp/app_v2.go @@ -3,16 +3,19 @@ package simapp import ( + "fmt" "io" "os" "path/filepath" dbm "github.com/cosmos/cosmos-db" + "github.com/spf13/cast" "cosmossdk.io/depinject" "cosmossdk.io/log" storetypes "cosmossdk.io/store/types" "cosmossdk.io/x/auth" + "cosmossdk.io/x/auth/ante/unorderedtx" authkeeper "cosmossdk.io/x/auth/keeper" authsims "cosmossdk.io/x/auth/simulation" authtypes "cosmossdk.io/x/auth/types" @@ -34,6 +37,7 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/runtime" @@ -64,6 +68,8 @@ type SimApp struct { txConfig client.TxConfig interfaceRegistry codectypes.InterfaceRegistry + UnorderedTxManager *unorderedtx.Manager + // keepers AuthKeeper authkeeper.AccountKeeper BankKeeper bankkeeper.Keeper @@ -256,6 +262,25 @@ func NewSimApp( // return app.App.InitChainer(ctx, req) // }) + // create, start, and load the unordered tx manager + utxDataDir := filepath.Join(cast.ToString(appOpts.Get(flags.FlagHome)), "data") + app.UnorderedTxManager = unorderedtx.NewManager(utxDataDir) + app.UnorderedTxManager.Start() + + if err := app.UnorderedTxManager.OnInit(); err != nil { + panic(fmt.Errorf("failed to initialize unordered tx manager: %w", err)) + } + + // register custom snapshot extensions (if any) + if manager := app.SnapshotManager(); manager != nil { + err := manager.RegisterExtensions( + unorderedtx.NewSnapshotter(app.UnorderedTxManager), + ) + if err != nil { + panic(fmt.Errorf("failed to register snapshot extension: %s", err)) + } + } + if err := app.Load(loadLatest); err != nil { panic(err) } @@ -263,6 +288,12 @@ func NewSimApp( return app } +// Close implements the Application interface and closes all necessary application +// resources. +func (app *SimApp) Close() error { + return app.UnorderedTxManager.Close() +} + // LegacyAmino returns SimApp's amino codec. // // NOTE: This is solely to be used for testing purposes as it may be desirable diff --git a/x/auth/ante/unordered.go b/x/auth/ante/unordered.go index 9e0a933dd703..c110e63650ce 100644 --- a/x/auth/ante/unordered.go +++ b/x/auth/ante/unordered.go @@ -45,11 +45,16 @@ func (d *UnorderedTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b return next(ctx, tx, simulate) } - if unorderedTx.GetTimeoutHeight() == 0 { + // TTL is defined as a specific block height at which this tx is no longer valid + ttl := unorderedTx.GetTimeoutHeight() + + if ttl == 0 { return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "unordered transaction must have timeout_height set") } - - if unorderedTx.GetTimeoutHeight() > uint64(ctx.BlockHeight())+d.maxUnOrderedTTL { + if ttl < uint64(ctx.BlockHeight()) { + return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "unordered transaction has a timeout_height that has already passed") + } + if ttl > uint64(ctx.BlockHeight())+d.maxUnOrderedTTL { return ctx, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "unordered tx ttl exceeds %d", d.maxUnOrderedTTL) } @@ -62,7 +67,7 @@ func (d *UnorderedTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b if ctx.ExecMode() == sdk.ExecModeFinalize { // a new tx included in the block, add the hash to the unordered tx manager - d.txManager.Add(txHash, unorderedTx.GetTimeoutHeight()) + d.txManager.Add(txHash, ttl) } return next(ctx, tx, simulate) diff --git a/x/auth/ante/unordered_test.go b/x/auth/ante/unordered_test.go index 13792822d07c..61653ee75a46 100644 --- a/x/auth/ante/unordered_test.go +++ b/x/auth/ante/unordered_test.go @@ -16,8 +16,10 @@ import ( ) func TestUnorderedTxDecorator_OrderedTx(t *testing.T) { - txm := unorderedtx.NewManager() - defer txm.Close() + txm := unorderedtx.NewManager(t.TempDir()) + defer func() { + require.NoError(t, txm.Close()) + }() txm.Start() @@ -31,8 +33,10 @@ func TestUnorderedTxDecorator_OrderedTx(t *testing.T) { } func TestUnorderedTxDecorator_UnorderedTx_NoTTL(t *testing.T) { - txm := unorderedtx.NewManager() - defer txm.Close() + txm := unorderedtx.NewManager(t.TempDir()) + defer func() { + require.NoError(t, txm.Close()) + }() txm.Start() @@ -46,8 +50,10 @@ func TestUnorderedTxDecorator_UnorderedTx_NoTTL(t *testing.T) { } func TestUnorderedTxDecorator_UnorderedTx_InvalidTTL(t *testing.T) { - txm := unorderedtx.NewManager() - defer txm.Close() + txm := unorderedtx.NewManager(t.TempDir()) + defer func() { + require.NoError(t, txm.Close()) + }() txm.Start() @@ -61,8 +67,10 @@ func TestUnorderedTxDecorator_UnorderedTx_InvalidTTL(t *testing.T) { } func TestUnorderedTxDecorator_UnorderedTx_AlreadyExists(t *testing.T) { - txm := unorderedtx.NewManager() - defer txm.Close() + txm := unorderedtx.NewManager(t.TempDir()) + defer func() { + require.NoError(t, txm.Close()) + }() txm.Start() @@ -79,8 +87,10 @@ func TestUnorderedTxDecorator_UnorderedTx_AlreadyExists(t *testing.T) { } func TestUnorderedTxDecorator_UnorderedTx_ValidCheckTx(t *testing.T) { - txm := unorderedtx.NewManager() - defer txm.Close() + txm := unorderedtx.NewManager(t.TempDir()) + defer func() { + require.NoError(t, txm.Close()) + }() txm.Start() @@ -94,8 +104,10 @@ func TestUnorderedTxDecorator_UnorderedTx_ValidCheckTx(t *testing.T) { } func TestUnorderedTxDecorator_UnorderedTx_ValidDeliverTx(t *testing.T) { - txm := unorderedtx.NewManager() - defer txm.Close() + txm := unorderedtx.NewManager(t.TempDir()) + defer func() { + require.NoError(t, txm.Close()) + }() txm.Start() diff --git a/x/auth/ante/unorderedtx/manager.go b/x/auth/ante/unorderedtx/manager.go index 3a6474baacbc..14fbe018b83b 100644 --- a/x/auth/ante/unorderedtx/manager.go +++ b/x/auth/ante/unorderedtx/manager.go @@ -1,15 +1,29 @@ package unorderedtx import ( + "bufio" + "bytes" "context" + "encoding/binary" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "sort" "sync" "time" + + "golang.org/x/exp/maps" ) const ( // DefaultMaxUnOrderedTTL defines the default maximum TTL an un-ordered transaction // can set. DefaultMaxUnOrderedTTL = 1024 + + dirName = "unordered_txs" + fileName = "data" ) // TxHash defines a transaction hash type alias, which is a fixed array of 32 bytes. @@ -23,6 +37,16 @@ type Manager struct { // doneCh allows us to ensure the purgeLoop has gracefully terminated prior to closing doneCh chan struct{} + // dataDir defines the directory to store unexpired unordered transactions + // + // XXX: Note, ideally we avoid the need to store unexpired unordered transactions + // directly to file. However, store v1 does not allow such a primitive. But, + // once store v2 is fully integrated, we can remove manual file handling and + // store the unexpired unordered transactions directly to SS. + // + // Ref: https://github.com/cosmos/cosmos-sdk/issues/18467 + dataDir string + mu sync.RWMutex // txHashes defines a map from tx hash -> TTL value, which is used for duplicate // checking and replay protection, as well as purging the map when the TTL is @@ -30,8 +54,14 @@ type Manager struct { txHashes map[TxHash]uint64 } -func NewManager() *Manager { +func NewManager(dataDir string) *Manager { + path := filepath.Join(dataDir, dirName) + if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { + _ = os.Mkdir(path, os.ModePerm) + } + m := &Manager{ + dataDir: dataDir, blockCh: make(chan uint64, 16), doneCh: make(chan struct{}), txHashes: make(map[TxHash]uint64), @@ -44,12 +74,18 @@ func (m *Manager) Start() { go m.purgeLoop() } +// Close must be called when a node gracefully shuts down. Typically, this should +// be called in an application's Close() function, which is called by the server. +// Note, Start() must be called in order for Close() to not hang. +// +// It will free all necessary resources as well as writing all unexpired unordered +// transactions along with their TTL values to file. func (m *Manager) Close() error { close(m.blockCh) <-m.doneCh m.blockCh = nil - return nil + return m.flushToFile() } func (m *Manager) Contains(hash TxHash) bool { @@ -74,12 +110,106 @@ func (m *Manager) Add(txHash TxHash, ttl uint64) { m.txHashes[txHash] = ttl } -// OnNewBlock send the latest block number to the background purge loop, which +// OnInit must be called when a node starts up. Typically, this should be called +// in an application's constructor, which is called by the server. +func (m *Manager) OnInit() error { + f, err := os.Open(filepath.Join(m.dataDir, dirName, fileName)) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // File does not exist, which we can assume that there are no unexpired + // unordered transactions. + return nil + } + + return fmt.Errorf("failed to open unconfirmed txs file: %w", err) + } + defer f.Close() + + var ( + r = bufio.NewReader(f) + buf = make([]byte, chunkSize) + ) + for { + n, err := io.ReadFull(r, buf) + if err != nil { + if errors.Is(err, io.EOF) { + break + } else { + return fmt.Errorf("failed to read unconfirmed txs file: %w", err) + } + } + if n != 32+8 { + return fmt.Errorf("read unexpected number of bytes from unconfirmed txs file: %d", n) + } + + var txHash TxHash + copy(txHash[:], buf[:txHashSize]) + + m.Add(txHash, binary.BigEndian.Uint64(buf[txHashSize:])) + } + + return nil +} + +// OnNewBlock sends the latest block number to the background purge loop, which // should be called in ABCI Commit event. func (m *Manager) OnNewBlock(blockHeight uint64) { m.blockCh <- blockHeight } +func (m *Manager) exportSnapshot(height uint64, snapshotWriter func([]byte) error) error { + var buf bytes.Buffer + w := bufio.NewWriter(&buf) + + keys := maps.Keys(m.txHashes) + sort.Slice(keys, func(i, j int) bool { return bytes.Compare(keys[i][:], keys[j][:]) < 0 }) + + for _, txHash := range keys { + ttl := m.txHashes[txHash] + if height > ttl { + // skip expired txs that have yet to be purged + continue + } + + chunk := unorderedTxToBytes(txHash, ttl) + + if _, err := w.Write(chunk); err != nil { + return fmt.Errorf("failed to write unordered tx to buffer: %w", err) + } + } + + if err := w.Flush(); err != nil { + return fmt.Errorf("failed to flush unordered txs buffer: %w", err) + } + + return snapshotWriter(buf.Bytes()) +} + +// flushToFile writes all unexpired unordered transactions along with their TTL +// to file, overwriting the existing file if it exists. +func (m *Manager) flushToFile() error { + f, err := os.Create(filepath.Join(m.dataDir, dirName, fileName)) + if err != nil { + return fmt.Errorf("failed to create unordered txs file: %w", err) + } + defer f.Close() + + w := bufio.NewWriter(f) + for txHash, ttl := range m.txHashes { + chunk := unorderedTxToBytes(txHash, ttl) + + if _, err = w.Write(chunk); err != nil { + return fmt.Errorf("failed to write unordered tx to buffer: %w", err) + } + } + + if err = w.Flush(); err != nil { + return fmt.Errorf("failed to flush unordered txs buffer: %w", err) + } + + return nil +} + // expiredTxs returns expired tx hashes based on the provided block height. func (m *Manager) expiredTxs(blockHeight uint64) []TxHash { m.mu.RLock() @@ -142,3 +272,14 @@ func (m *Manager) batchReceive() (uint64, bool) { } } } + +func unorderedTxToBytes(txHash TxHash, ttl uint64) []byte { + chunk := make([]byte, chunkSize) + copy(chunk[:txHashSize], txHash[:]) + + ttlBz := make([]byte, ttlSize) + binary.BigEndian.PutUint64(ttlBz, ttl) + copy(chunk[txHashSize:], ttlBz) + + return chunk +} diff --git a/x/auth/ante/unorderedtx/manager_test.go b/x/auth/ante/unorderedtx/manager_test.go index caf03a3c269b..04138e344657 100644 --- a/x/auth/ante/unorderedtx/manager_test.go +++ b/x/auth/ante/unorderedtx/manager_test.go @@ -10,7 +10,7 @@ import ( ) func TestUnorderedTxManager_Close(t *testing.T) { - txm := unorderedtx.NewManager() + txm := unorderedtx.NewManager(t.TempDir()) txm.Start() require.NoError(t, txm.Close()) @@ -18,8 +18,10 @@ func TestUnorderedTxManager_Close(t *testing.T) { } func TestUnorderedTxManager_SimpleSize(t *testing.T) { - txm := unorderedtx.NewManager() - defer txm.Close() + txm := unorderedtx.NewManager(t.TempDir()) + defer func() { + require.NoError(t, txm.Close()) + }() txm.Start() @@ -31,8 +33,10 @@ func TestUnorderedTxManager_SimpleSize(t *testing.T) { } func TestUnorderedTxManager_SimpleContains(t *testing.T) { - txm := unorderedtx.NewManager() - defer txm.Close() + txm := unorderedtx.NewManager(t.TempDir()) + defer func() { + require.NoError(t, txm.Close()) + }() txm.Start() @@ -48,9 +52,51 @@ func TestUnorderedTxManager_SimpleContains(t *testing.T) { } } +func TestUnorderedTxManager_InitEmpty(t *testing.T) { + txm := unorderedtx.NewManager(t.TempDir()) + defer func() { + require.NoError(t, txm.Close()) + }() + + txm.Start() + + require.NoError(t, txm.OnInit()) +} + +func TestUnorderedTxManager_CloseInit(t *testing.T) { + dataDir := t.TempDir() + txm := unorderedtx.NewManager(dataDir) + txm.Start() + + // add a handful of unordered txs + for i := 0; i < 100; i++ { + txm.Add([32]byte{byte(i)}, 100) + } + + // close the manager, which should flush all unexpired txs to file + require.NoError(t, txm.Close()) + + // create a new manager, start it + txm2 := unorderedtx.NewManager(dataDir) + defer func() { + require.NoError(t, txm2.Close()) + }() + + // start and execute OnInit, which should load the unexpired txs from file + txm2.Start() + require.NoError(t, txm2.OnInit()) + require.Equal(t, 100, txm2.Size()) + + for i := 0; i < 100; i++ { + require.True(t, txm2.Contains([32]byte{byte(i)})) + } +} + func TestUnorderedTxManager_Flow(t *testing.T) { - txm := unorderedtx.NewManager() - defer txm.Close() + txm := unorderedtx.NewManager(t.TempDir()) + defer func() { + require.NoError(t, txm.Close()) + }() txm.Start() diff --git a/x/auth/ante/unorderedtx/snapshotter.go b/x/auth/ante/unorderedtx/snapshotter.go new file mode 100644 index 000000000000..5941a11a6888 --- /dev/null +++ b/x/auth/ante/unorderedtx/snapshotter.go @@ -0,0 +1,92 @@ +package unorderedtx + +import ( + "encoding/binary" + "errors" + "io" + + snapshot "cosmossdk.io/store/snapshots/types" +) + +const ( + txHashSize = 32 + ttlSize = 8 + chunkSize = txHashSize + ttlSize +) + +var _ snapshot.ExtensionSnapshotter = &Snapshotter{} + +const ( + // SnapshotFormat defines the snapshot format of exported unordered transactions. + // No protobuf envelope, no metadata. + SnapshotFormat = 1 + + // SnapshotName defines the snapshot name of exported unordered transactions. + SnapshotName = "unordered_txs" +) + +type Snapshotter struct { + m *Manager +} + +func NewSnapshotter(m *Manager) *Snapshotter { + return &Snapshotter{m: m} +} + +func (s *Snapshotter) SnapshotName() string { + return SnapshotName +} + +func (s *Snapshotter) SnapshotFormat() uint32 { + return SnapshotFormat +} + +func (s *Snapshotter) SupportedFormats() []uint32 { + return []uint32{SnapshotFormat} +} + +func (s *Snapshotter) SnapshotExtension(height uint64, payloadWriter snapshot.ExtensionPayloadWriter) error { + // export all unordered transactions as a single blob + return s.m.exportSnapshot(height, payloadWriter) +} + +func (s *Snapshotter) RestoreExtension(height uint64, format uint32, payloadReader snapshot.ExtensionPayloadReader) error { + if format == SnapshotFormat { + return s.restore(height, payloadReader) + } + + return snapshot.ErrUnknownFormat +} + +func (s *Snapshotter) restore(height uint64, payloadReader snapshot.ExtensionPayloadReader) error { + // the payload should be the entire set of unordered transactions + payload, err := payloadReader() + if err != nil { + if errors.Is(err, io.EOF) { + return io.ErrUnexpectedEOF + } + + return err + } + + if len(payload)%chunkSize != 0 { + return errors.New("invalid unordered txs payload length") + } + + var i int + for i < len(payload) { + var txHash TxHash + copy(txHash[:], payload[i:i+txHashSize]) + + ttl := binary.BigEndian.Uint64(payload[i+txHashSize : i+chunkSize]) + + if height < ttl { + // only add unordered transactions that are still valid, i.e. unexpired + s.m.Add(txHash, ttl) + } + + i += chunkSize + } + + return nil +} diff --git a/x/auth/ante/unorderedtx/snapshotter_test.go b/x/auth/ante/unorderedtx/snapshotter_test.go new file mode 100644 index 000000000000..1645fbb90677 --- /dev/null +++ b/x/auth/ante/unorderedtx/snapshotter_test.go @@ -0,0 +1,56 @@ +package unorderedtx_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/x/auth/ante/unorderedtx" +) + +func TestSnapshotter(t *testing.T) { + dataDir := t.TempDir() + txm := unorderedtx.NewManager(dataDir) + + // add a handful of unordered txs + for i := 0; i < 100; i++ { + txm.Add([32]byte{byte(i)}, 100) + } + + var unorderedTxBz []byte + s := unorderedtx.NewSnapshotter(txm) + w := func(bz []byte) error { + unorderedTxBz = bz + return nil + } + + err := s.SnapshotExtension(50, w) + require.NoError(t, err) + require.NotEmpty(t, unorderedTxBz) + + pr := func() ([]byte, error) { + return unorderedTxBz, nil + } + + // restore with an invalid format which should result in an error + err = s.RestoreExtension(50, 2, pr) + require.Error(t, err) + + // restore with height > ttl which should result in no unordered txs synced + txm2 := unorderedtx.NewManager(dataDir) + s2 := unorderedtx.NewSnapshotter(txm2) + err = s2.RestoreExtension(200, unorderedtx.SnapshotFormat, pr) + require.NoError(t, err) + require.Empty(t, txm2.Size()) + + // restore with with height < ttl which should result in all unordered txs synced + txm3 := unorderedtx.NewManager(dataDir) + s3 := unorderedtx.NewSnapshotter(txm3) + err = s3.RestoreExtension(50, unorderedtx.SnapshotFormat, pr) + require.NoError(t, err) + require.Equal(t, 100, txm3.Size()) + + for i := 0; i < 100; i++ { + require.True(t, txm3.Contains([32]byte{byte(i)})) + } +} From be3cad7a55c34b08574e5187fc1b0cd6da42ca8c Mon Sep 17 00:00:00 2001 From: Facundo Date: Thu, 4 Jan 2024 10:18:07 +0100 Subject: [PATCH 18/20] fix TestUnknownFields by moving some_new_field to tag 5 --- testutil/testdata/testpb/unknonwnproto.proto | 2 +- testutil/testdata/testpb/unknonwnproto.pulsar.go | 8 ++++---- testutil/testdata/unknonwnproto.pb.go | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/testutil/testdata/testpb/unknonwnproto.proto b/testutil/testdata/testpb/unknonwnproto.proto index ac91b9e2a662..94037635ec17 100644 --- a/testutil/testdata/testpb/unknonwnproto.proto +++ b/testutil/testdata/testpb/unknonwnproto.proto @@ -290,7 +290,7 @@ message TestUpdatedTxBody { repeated google.protobuf.Any messages = 1; string memo = 2; int64 timeout_height = 3; - uint64 some_new_field = 4; + uint64 some_new_field = 5; string some_new_field_non_critical_field = 1050; repeated google.protobuf.Any extension_options = 1023; repeated google.protobuf.Any non_critical_extension_options = 2047; diff --git a/testutil/testdata/testpb/unknonwnproto.pulsar.go b/testutil/testdata/testpb/unknonwnproto.pulsar.go index 84dbe6fbdb53..e8a5d52c8529 100644 --- a/testutil/testdata/testpb/unknonwnproto.pulsar.go +++ b/testutil/testdata/testpb/unknonwnproto.pulsar.go @@ -23036,7 +23036,7 @@ func (x *fastReflection_TestUpdatedTxBody) ProtoMethods() *protoiface.Methods { if x.SomeNewField != 0 { i = runtime.EncodeVarint(dAtA, i, uint64(x.SomeNewField)) i-- - dAtA[i] = 0x20 + dAtA[i] = 0x28 } if x.TimeoutHeight != 0 { i = runtime.EncodeVarint(dAtA, i, uint64(x.TimeoutHeight)) @@ -23200,7 +23200,7 @@ func (x *fastReflection_TestUpdatedTxBody) ProtoMethods() *protoiface.Methods { break } } - case 4: + case 5: if wireType != 0 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field SomeNewField", wireType) } @@ -26508,7 +26508,7 @@ type TestUpdatedTxBody struct { Messages []*anypb.Any `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` Memo string `protobuf:"bytes,2,opt,name=memo,proto3" json:"memo,omitempty"` TimeoutHeight int64 `protobuf:"varint,3,opt,name=timeout_height,json=timeoutHeight,proto3" json:"timeout_height,omitempty"` - SomeNewField uint64 `protobuf:"varint,4,opt,name=some_new_field,json=someNewField,proto3" json:"some_new_field,omitempty"` + SomeNewField uint64 `protobuf:"varint,5,opt,name=some_new_field,json=someNewField,proto3" json:"some_new_field,omitempty"` SomeNewFieldNonCriticalField string `protobuf:"bytes,1050,opt,name=some_new_field_non_critical_field,json=someNewFieldNonCriticalField,proto3" json:"some_new_field_non_critical_field,omitempty"` ExtensionOptions []*anypb.Any `protobuf:"bytes,1023,rep,name=extension_options,json=extensionOptions,proto3" json:"extension_options,omitempty"` NonCriticalExtensionOptions []*anypb.Any `protobuf:"bytes,2047,rep,name=non_critical_extension_options,json=nonCriticalExtensionOptions,proto3" json:"non_critical_extension_options,omitempty"` @@ -27410,7 +27410,7 @@ var file_testpb_unknonwnproto_proto_rawDesc = []byte{ 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x6e, 0x65, 0x77, 0x5f, 0x66, 0x69, - 0x65, 0x6c, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x73, 0x6f, 0x6d, 0x65, 0x4e, + 0x65, 0x6c, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x73, 0x6f, 0x6d, 0x65, 0x4e, 0x65, 0x77, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x48, 0x0a, 0x21, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x6e, 0x65, 0x77, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6e, 0x6f, 0x6e, 0x5f, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x9a, 0x08, 0x20, diff --git a/testutil/testdata/unknonwnproto.pb.go b/testutil/testdata/unknonwnproto.pb.go index 3522a7253598..406d624a3c24 100644 --- a/testutil/testdata/unknonwnproto.pb.go +++ b/testutil/testdata/unknonwnproto.pb.go @@ -2578,7 +2578,7 @@ type TestUpdatedTxBody struct { Messages []*types.Any `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` Memo string `protobuf:"bytes,2,opt,name=memo,proto3" json:"memo,omitempty"` TimeoutHeight int64 `protobuf:"varint,3,opt,name=timeout_height,json=timeoutHeight,proto3" json:"timeout_height,omitempty"` - SomeNewField uint64 `protobuf:"varint,4,opt,name=some_new_field,json=someNewField,proto3" json:"some_new_field,omitempty"` + SomeNewField uint64 `protobuf:"varint,5,opt,name=some_new_field,json=someNewField,proto3" json:"some_new_field,omitempty"` SomeNewFieldNonCriticalField string `protobuf:"bytes,1050,opt,name=some_new_field_non_critical_field,json=someNewFieldNonCriticalField,proto3" json:"some_new_field_non_critical_field,omitempty"` ExtensionOptions []*types.Any `protobuf:"bytes,1023,rep,name=extension_options,json=extensionOptions,proto3" json:"extension_options,omitempty"` NonCriticalExtensionOptions []*types.Any `protobuf:"bytes,2047,rep,name=non_critical_extension_options,json=nonCriticalExtensionOptions,proto3" json:"non_critical_extension_options,omitempty"` @@ -2906,7 +2906,7 @@ var fileDescriptor_fe4560133be9209a = []byte{ 0x1a, 0x5f, 0x6a, 0x70, 0x6d, 0xc5, 0x85, 0x3e, 0x73, 0x17, 0xf8, 0x0e, 0x14, 0x66, 0x84, 0x73, 0x7b, 0xac, 0x3c, 0xd0, 0x36, 0xa6, 0x56, 0x82, 0x92, 0xd5, 0x3c, 0x23, 0x33, 0x16, 0x57, 0xb3, 0x1c, 0x4b, 0x13, 0x84, 0x37, 0x23, 0x2c, 0x10, 0x83, 0x09, 0xf1, 0xc6, 0x13, 0x11, 0xf1, 0x78, - 0x25, 0x92, 0x1e, 0x2a, 0x21, 0x7e, 0x1f, 0xca, 0x9c, 0xcd, 0xc8, 0x60, 0x79, 0x6d, 0xca, 0xaa, + 0x25, 0x92, 0x1e, 0x2a, 0x21, 0x7e, 0x1f, 0xca, 0x9c, 0xcd, 0xc8, 0x60, 0x79, 0x6d, 0xca, 0xa9, 0x6b, 0x53, 0x49, 0x4a, 0x8f, 0x22, 0x63, 0xf1, 0x21, 0xfc, 0x60, 0x15, 0x35, 0x58, 0xd3, 0x82, 0x7f, 0x17, 0xb6, 0xe0, 0xf7, 0xd2, 0x3b, 0x8f, 0x5e, 0x6f, 0xc7, 0x7d, 0xb8, 0x46, 0xe6, 0x82, 0x50, 0x99, 0x23, 0x03, 0xa6, 0x3e, 0xe5, 0x72, 0xfd, 0xdf, 0xbb, 0xe7, 0xb8, 0x59, 0x49, 0xf0, @@ -2921,7 +2921,7 @@ var fileDescriptor_fe4560133be9209a = []byte{ 0x3c, 0x7f, 0x55, 0xdb, 0xf9, 0xeb, 0xab, 0xda, 0xce, 0x67, 0xcd, 0xb1, 0x27, 0x26, 0xc1, 0xb0, 0xe9, 0xb0, 0x59, 0x2b, 0xfa, 0xc8, 0x1f, 0xfe, 0xdd, 0xe6, 0xee, 0x71, 0x4b, 0x56, 0x7d, 0x20, 0xbc, 0xa9, 0x1a, 0xb8, 0xb6, 0xb0, 0x87, 0x79, 0x45, 0x74, 0xe7, 0x3f, 0x01, 0x00, 0x00, 0xff, - 0xff, 0x3a, 0xea, 0x0d, 0xa7, 0x67, 0x18, 0x00, 0x00, + 0xff, 0x33, 0x3d, 0xcf, 0x3a, 0x67, 0x18, 0x00, 0x00, } func (m *Customer1) Marshal() (dAtA []byte, err error) { @@ -5261,7 +5261,7 @@ func (m *TestUpdatedTxBody) MarshalToSizedBuffer(dAtA []byte) (int, error) { if m.SomeNewField != 0 { i = encodeVarintUnknonwnproto(dAtA, i, uint64(m.SomeNewField)) i-- - dAtA[i] = 0x20 + dAtA[i] = 0x28 } if m.TimeoutHeight != 0 { i = encodeVarintUnknonwnproto(dAtA, i, uint64(m.TimeoutHeight)) @@ -12602,7 +12602,7 @@ func (m *TestUpdatedTxBody) Unmarshal(dAtA []byte) error { break } } - case 4: + case 5: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field SomeNewField", wireType) } From 11cddfeb23149a2c2de3782f3e513269616d453e Mon Sep 17 00:00:00 2001 From: Facundo Date: Thu, 4 Jan 2024 16:26:39 +0100 Subject: [PATCH 19/20] add comment --- x/auth/tx/encode_decode_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x/auth/tx/encode_decode_test.go b/x/auth/tx/encode_decode_test.go index ae44448213a8..d2c1fff5fdab 100644 --- a/x/auth/tx/encode_decode_test.go +++ b/x/auth/tx/encode_decode_test.go @@ -65,6 +65,8 @@ func TestUnknownFields(t *testing.T) { shouldErr: false, }, { + // If new fields are added to TxBody the number for some_new_field in the proto definition must be set to + // one that it's not used in TxBody. name: "critical fields in TxBody should error on decode", body: &testdata.TestUpdatedTxBody{ Memo: "foo", From 4f1c99223dd0daa54f575687b0e93097512c9ee8 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Thu, 4 Jan 2024 08:50:16 -0800 Subject: [PATCH 20/20] cl++ --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df8b4b03b090..aaf8af571310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* (x/auth) Support the ability to broadcast unordered transactions per ADR-070. See UPGRADING.md for more details on integration. * (client) [#18557](https://github.com/cosmos/cosmos-sdk/pull/18557) Add `--qrcode` flag to `keys show` command to support displaying keys address QR code. * (x/staking) [#18142](https://github.com/cosmos/cosmos-sdk/pull/18142) Introduce `key_rotation_fee` param to calculate fees while rotating the keys * (client) [#18101](https://github.com/cosmos/cosmos-sdk/pull/18101) Add a `keyring-default-keyname` in `client.toml` for specifying a default key name, and skip the need to use the `--from` flag when signing transactions. @@ -189,7 +190,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (x/auth) [#18351](https://github.com/cosmos/cosmos-sdk/pull/18351) Auth module was moved to its own go.mod `cosmossdk.io/x/auth` * (types) [#18372](https://github.com/cosmos/cosmos-sdk/pull/18372) Removed global configuration for coin type and purpose. Setters and getters should be removed and access directly to defined types. * (types) [#18695](https://github.com/cosmos/cosmos-sdk/pull/18695) Removed global configuration for txEncoder. -* (server) [#18909](https://github.com/cosmos/cosmos-sdk/pull/18909) Remove configuration endpoint on grpc reflection endpoint in favour of auth module bech32prefix endpoint already exposed. +* (server) [#18909](https://github.com/cosmos/cosmos-sdk/pull/18909) Remove configuration endpoint on grpc reflection endpoint in favour of auth module bech32prefix endpoint already exposed. ### CLI Breaking Changes