diff --git a/api/uexecutor/v1/tx.pulsar.go b/api/uexecutor/v1/tx.pulsar.go index c6a198080..1f497a19f 100644 --- a/api/uexecutor/v1/tx.pulsar.go +++ b/api/uexecutor/v1/tx.pulsar.go @@ -1870,31 +1870,27 @@ func (x *fastReflection_MsgExecutePayloadResponse) ProtoMethods() *protoiface.Me } var ( - md_MsgMigrateUEA protoreflect.MessageDescriptor - fd_MsgMigrateUEA_signer protoreflect.FieldDescriptor - fd_MsgMigrateUEA_universal_account_id protoreflect.FieldDescriptor - fd_MsgMigrateUEA_migration_payload protoreflect.FieldDescriptor - fd_MsgMigrateUEA_signature protoreflect.FieldDescriptor + md_MsgVoteInbound protoreflect.MessageDescriptor + fd_MsgVoteInbound_signer protoreflect.FieldDescriptor + fd_MsgVoteInbound_inbound protoreflect.FieldDescriptor ) func init() { file_uexecutor_v1_tx_proto_init() - md_MsgMigrateUEA = File_uexecutor_v1_tx_proto.Messages().ByName("MsgMigrateUEA") - fd_MsgMigrateUEA_signer = md_MsgMigrateUEA.Fields().ByName("signer") - fd_MsgMigrateUEA_universal_account_id = md_MsgMigrateUEA.Fields().ByName("universal_account_id") - fd_MsgMigrateUEA_migration_payload = md_MsgMigrateUEA.Fields().ByName("migration_payload") - fd_MsgMigrateUEA_signature = md_MsgMigrateUEA.Fields().ByName("signature") + md_MsgVoteInbound = File_uexecutor_v1_tx_proto.Messages().ByName("MsgVoteInbound") + fd_MsgVoteInbound_signer = md_MsgVoteInbound.Fields().ByName("signer") + fd_MsgVoteInbound_inbound = md_MsgVoteInbound.Fields().ByName("inbound") } -var _ protoreflect.Message = (*fastReflection_MsgMigrateUEA)(nil) +var _ protoreflect.Message = (*fastReflection_MsgVoteInbound)(nil) -type fastReflection_MsgMigrateUEA MsgMigrateUEA +type fastReflection_MsgVoteInbound MsgVoteInbound -func (x *MsgMigrateUEA) ProtoReflect() protoreflect.Message { - return (*fastReflection_MsgMigrateUEA)(x) +func (x *MsgVoteInbound) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgVoteInbound)(x) } -func (x *MsgMigrateUEA) slowProtoReflect() protoreflect.Message { +func (x *MsgVoteInbound) slowProtoReflect() protoreflect.Message { mi := &file_uexecutor_v1_tx_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1906,43 +1902,43 @@ func (x *MsgMigrateUEA) slowProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -var _fastReflection_MsgMigrateUEA_messageType fastReflection_MsgMigrateUEA_messageType -var _ protoreflect.MessageType = fastReflection_MsgMigrateUEA_messageType{} +var _fastReflection_MsgVoteInbound_messageType fastReflection_MsgVoteInbound_messageType +var _ protoreflect.MessageType = fastReflection_MsgVoteInbound_messageType{} -type fastReflection_MsgMigrateUEA_messageType struct{} +type fastReflection_MsgVoteInbound_messageType struct{} -func (x fastReflection_MsgMigrateUEA_messageType) Zero() protoreflect.Message { - return (*fastReflection_MsgMigrateUEA)(nil) +func (x fastReflection_MsgVoteInbound_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgVoteInbound)(nil) } -func (x fastReflection_MsgMigrateUEA_messageType) New() protoreflect.Message { - return new(fastReflection_MsgMigrateUEA) +func (x fastReflection_MsgVoteInbound_messageType) New() protoreflect.Message { + return new(fastReflection_MsgVoteInbound) } -func (x fastReflection_MsgMigrateUEA_messageType) Descriptor() protoreflect.MessageDescriptor { - return md_MsgMigrateUEA +func (x fastReflection_MsgVoteInbound_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgVoteInbound } // Descriptor returns message descriptor, which contains only the protobuf // type information for the message. -func (x *fastReflection_MsgMigrateUEA) Descriptor() protoreflect.MessageDescriptor { - return md_MsgMigrateUEA +func (x *fastReflection_MsgVoteInbound) Descriptor() protoreflect.MessageDescriptor { + return md_MsgVoteInbound } // Type returns the message type, which encapsulates both Go and protobuf // type information. If the Go type information is not needed, // it is recommended that the message descriptor be used instead. -func (x *fastReflection_MsgMigrateUEA) Type() protoreflect.MessageType { - return _fastReflection_MsgMigrateUEA_messageType +func (x *fastReflection_MsgVoteInbound) Type() protoreflect.MessageType { + return _fastReflection_MsgVoteInbound_messageType } // New returns a newly allocated and mutable empty message. -func (x *fastReflection_MsgMigrateUEA) New() protoreflect.Message { - return new(fastReflection_MsgMigrateUEA) +func (x *fastReflection_MsgVoteInbound) New() protoreflect.Message { + return new(fastReflection_MsgVoteInbound) } // Interface unwraps the message reflection interface and // returns the underlying ProtoMessage interface. -func (x *fastReflection_MsgMigrateUEA) Interface() protoreflect.ProtoMessage { - return (*MsgMigrateUEA)(x) +func (x *fastReflection_MsgVoteInbound) Interface() protoreflect.ProtoMessage { + return (*MsgVoteInbound)(x) } // Range iterates over every populated field in an undefined order, @@ -1950,28 +1946,16 @@ func (x *fastReflection_MsgMigrateUEA) Interface() protoreflect.ProtoMessage { // Range returns immediately if f returns false. // While iterating, mutating operations may only be performed // on the current field descriptor. -func (x *fastReflection_MsgMigrateUEA) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { +func (x *fastReflection_MsgVoteInbound) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { if x.Signer != "" { value := protoreflect.ValueOfString(x.Signer) - if !f(fd_MsgMigrateUEA_signer, value) { - return - } - } - if x.UniversalAccountId != nil { - value := protoreflect.ValueOfMessage(x.UniversalAccountId.ProtoReflect()) - if !f(fd_MsgMigrateUEA_universal_account_id, value) { - return - } - } - if x.MigrationPayload != nil { - value := protoreflect.ValueOfMessage(x.MigrationPayload.ProtoReflect()) - if !f(fd_MsgMigrateUEA_migration_payload, value) { + if !f(fd_MsgVoteInbound_signer, value) { return } } - if x.Signature != "" { - value := protoreflect.ValueOfString(x.Signature) - if !f(fd_MsgMigrateUEA_signature, value) { + if x.Inbound != nil { + value := protoreflect.ValueOfMessage(x.Inbound.ProtoReflect()) + if !f(fd_MsgVoteInbound_inbound, value) { return } } @@ -1988,21 +1972,17 @@ func (x *fastReflection_MsgMigrateUEA) Range(f func(protoreflect.FieldDescriptor // In other cases (aside from the nullable cases above), // a proto3 scalar field is populated if it contains a non-zero value, and // a repeated field is populated if it is non-empty. -func (x *fastReflection_MsgMigrateUEA) Has(fd protoreflect.FieldDescriptor) bool { +func (x *fastReflection_MsgVoteInbound) Has(fd protoreflect.FieldDescriptor) bool { switch fd.FullName() { - case "uexecutor.v1.MsgMigrateUEA.signer": + case "uexecutor.v1.MsgVoteInbound.signer": return x.Signer != "" - case "uexecutor.v1.MsgMigrateUEA.universal_account_id": - return x.UniversalAccountId != nil - case "uexecutor.v1.MsgMigrateUEA.migration_payload": - return x.MigrationPayload != nil - case "uexecutor.v1.MsgMigrateUEA.signature": - return x.Signature != "" + case "uexecutor.v1.MsgVoteInbound.inbound": + return x.Inbound != nil default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgMigrateUEA")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgMigrateUEA does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteInbound does not contain field %s", fd.FullName())) } } @@ -2012,21 +1992,17 @@ func (x *fastReflection_MsgMigrateUEA) Has(fd protoreflect.FieldDescriptor) bool // associated with the given field number. // // Clear is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgMigrateUEA) Clear(fd protoreflect.FieldDescriptor) { +func (x *fastReflection_MsgVoteInbound) Clear(fd protoreflect.FieldDescriptor) { switch fd.FullName() { - case "uexecutor.v1.MsgMigrateUEA.signer": + case "uexecutor.v1.MsgVoteInbound.signer": x.Signer = "" - case "uexecutor.v1.MsgMigrateUEA.universal_account_id": - x.UniversalAccountId = nil - case "uexecutor.v1.MsgMigrateUEA.migration_payload": - x.MigrationPayload = nil - case "uexecutor.v1.MsgMigrateUEA.signature": - x.Signature = "" + case "uexecutor.v1.MsgVoteInbound.inbound": + x.Inbound = nil default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgMigrateUEA")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgMigrateUEA does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteInbound does not contain field %s", fd.FullName())) } } @@ -2036,25 +2012,19 @@ func (x *fastReflection_MsgMigrateUEA) Clear(fd protoreflect.FieldDescriptor) { // the default value of a bytes scalar is guaranteed to be a copy. // For unpopulated composite types, it returns an empty, read-only view // of the value; to obtain a mutable reference, use Mutable. -func (x *fastReflection_MsgMigrateUEA) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteInbound) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { switch descriptor.FullName() { - case "uexecutor.v1.MsgMigrateUEA.signer": + case "uexecutor.v1.MsgVoteInbound.signer": value := x.Signer return protoreflect.ValueOfString(value) - case "uexecutor.v1.MsgMigrateUEA.universal_account_id": - value := x.UniversalAccountId - return protoreflect.ValueOfMessage(value.ProtoReflect()) - case "uexecutor.v1.MsgMigrateUEA.migration_payload": - value := x.MigrationPayload + case "uexecutor.v1.MsgVoteInbound.inbound": + value := x.Inbound return protoreflect.ValueOfMessage(value.ProtoReflect()) - case "uexecutor.v1.MsgMigrateUEA.signature": - value := x.Signature - return protoreflect.ValueOfString(value) default: if descriptor.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgMigrateUEA")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgMigrateUEA does not contain field %s", descriptor.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteInbound does not contain field %s", descriptor.FullName())) } } @@ -2068,21 +2038,17 @@ func (x *fastReflection_MsgMigrateUEA) Get(descriptor protoreflect.FieldDescript // empty, read-only value, then it panics. // // Set is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgMigrateUEA) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { +func (x *fastReflection_MsgVoteInbound) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { switch fd.FullName() { - case "uexecutor.v1.MsgMigrateUEA.signer": + case "uexecutor.v1.MsgVoteInbound.signer": x.Signer = value.Interface().(string) - case "uexecutor.v1.MsgMigrateUEA.universal_account_id": - x.UniversalAccountId = value.Message().Interface().(*UniversalAccountId) - case "uexecutor.v1.MsgMigrateUEA.migration_payload": - x.MigrationPayload = value.Message().Interface().(*MigrationPayload) - case "uexecutor.v1.MsgMigrateUEA.signature": - x.Signature = value.Interface().(string) + case "uexecutor.v1.MsgVoteInbound.inbound": + x.Inbound = value.Message().Interface().(*Inbound) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgMigrateUEA")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgMigrateUEA does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteInbound does not contain field %s", fd.FullName())) } } @@ -2096,60 +2062,48 @@ func (x *fastReflection_MsgMigrateUEA) Set(fd protoreflect.FieldDescriptor, valu // It panics if the field does not contain a composite type. // // Mutable is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgMigrateUEA) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteInbound) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { - case "uexecutor.v1.MsgMigrateUEA.universal_account_id": - if x.UniversalAccountId == nil { - x.UniversalAccountId = new(UniversalAccountId) + case "uexecutor.v1.MsgVoteInbound.inbound": + if x.Inbound == nil { + x.Inbound = new(Inbound) } - return protoreflect.ValueOfMessage(x.UniversalAccountId.ProtoReflect()) - case "uexecutor.v1.MsgMigrateUEA.migration_payload": - if x.MigrationPayload == nil { - x.MigrationPayload = new(MigrationPayload) - } - return protoreflect.ValueOfMessage(x.MigrationPayload.ProtoReflect()) - case "uexecutor.v1.MsgMigrateUEA.signer": - panic(fmt.Errorf("field signer of message uexecutor.v1.MsgMigrateUEA is not mutable")) - case "uexecutor.v1.MsgMigrateUEA.signature": - panic(fmt.Errorf("field signature of message uexecutor.v1.MsgMigrateUEA is not mutable")) + return protoreflect.ValueOfMessage(x.Inbound.ProtoReflect()) + case "uexecutor.v1.MsgVoteInbound.signer": + panic(fmt.Errorf("field signer of message uexecutor.v1.MsgVoteInbound is not mutable")) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgMigrateUEA")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgMigrateUEA does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteInbound does not contain field %s", fd.FullName())) } } // NewField returns a new value that is assignable to the field // for the given descriptor. For scalars, this returns the default value. // For lists, maps, and messages, this returns a new, empty, mutable value. -func (x *fastReflection_MsgMigrateUEA) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteInbound) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { - case "uexecutor.v1.MsgMigrateUEA.signer": + case "uexecutor.v1.MsgVoteInbound.signer": return protoreflect.ValueOfString("") - case "uexecutor.v1.MsgMigrateUEA.universal_account_id": - m := new(UniversalAccountId) - return protoreflect.ValueOfMessage(m.ProtoReflect()) - case "uexecutor.v1.MsgMigrateUEA.migration_payload": - m := new(MigrationPayload) + case "uexecutor.v1.MsgVoteInbound.inbound": + m := new(Inbound) return protoreflect.ValueOfMessage(m.ProtoReflect()) - case "uexecutor.v1.MsgMigrateUEA.signature": - return protoreflect.ValueOfString("") default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgMigrateUEA")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgMigrateUEA does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteInbound does not contain field %s", fd.FullName())) } } // WhichOneof reports which field within the oneof is populated, // returning nil if none are populated. // It panics if the oneof descriptor does not belong to this message. -func (x *fastReflection_MsgMigrateUEA) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { +func (x *fastReflection_MsgVoteInbound) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { switch d.FullName() { default: - panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgMigrateUEA", d.FullName())) + panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgVoteInbound", d.FullName())) } panic("unreachable") } @@ -2157,7 +2111,7 @@ func (x *fastReflection_MsgMigrateUEA) WhichOneof(d protoreflect.OneofDescriptor // GetUnknown retrieves the entire list of unknown fields. // The caller may only mutate the contents of the RawFields // if the mutated bytes are stored back into the message with SetUnknown. -func (x *fastReflection_MsgMigrateUEA) GetUnknown() protoreflect.RawFields { +func (x *fastReflection_MsgVoteInbound) GetUnknown() protoreflect.RawFields { return x.unknownFields } @@ -2168,7 +2122,7 @@ func (x *fastReflection_MsgMigrateUEA) GetUnknown() protoreflect.RawFields { // An empty RawFields may be passed to clear the fields. // // SetUnknown is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgMigrateUEA) SetUnknown(fields protoreflect.RawFields) { +func (x *fastReflection_MsgVoteInbound) SetUnknown(fields protoreflect.RawFields) { x.unknownFields = fields } @@ -2180,7 +2134,7 @@ func (x *fastReflection_MsgMigrateUEA) SetUnknown(fields protoreflect.RawFields) // message type, but the details are implementation dependent. // Validity is not part of the protobuf data model, and may not // be preserved in marshaling or other operations. -func (x *fastReflection_MsgMigrateUEA) IsValid() bool { +func (x *fastReflection_MsgVoteInbound) IsValid() bool { return x != nil } @@ -2190,9 +2144,9 @@ func (x *fastReflection_MsgMigrateUEA) IsValid() bool { // The returned methods type is identical to // "google.golang.org/protobuf/runtime/protoiface".Methods. // Consult the protoiface package documentation for details. -func (x *fastReflection_MsgMigrateUEA) ProtoMethods() *protoiface.Methods { +func (x *fastReflection_MsgVoteInbound) ProtoMethods() *protoiface.Methods { size := func(input protoiface.SizeInput) protoiface.SizeOutput { - x := input.Message.Interface().(*MsgMigrateUEA) + x := input.Message.Interface().(*MsgVoteInbound) if x == nil { return protoiface.SizeOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -2208,16 +2162,8 @@ func (x *fastReflection_MsgMigrateUEA) ProtoMethods() *protoiface.Methods { if l > 0 { n += 1 + l + runtime.Sov(uint64(l)) } - if x.UniversalAccountId != nil { - l = options.Size(x.UniversalAccountId) - n += 1 + l + runtime.Sov(uint64(l)) - } - if x.MigrationPayload != nil { - l = options.Size(x.MigrationPayload) - n += 1 + l + runtime.Sov(uint64(l)) - } - l = len(x.Signature) - if l > 0 { + if x.Inbound != nil { + l = options.Size(x.Inbound) n += 1 + l + runtime.Sov(uint64(l)) } if x.unknownFields != nil { @@ -2230,7 +2176,7 @@ func (x *fastReflection_MsgMigrateUEA) ProtoMethods() *protoiface.Methods { } marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { - x := input.Message.Interface().(*MsgMigrateUEA) + x := input.Message.Interface().(*MsgVoteInbound) if x == nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -2249,29 +2195,8 @@ func (x *fastReflection_MsgMigrateUEA) ProtoMethods() *protoiface.Methods { i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } - if len(x.Signature) > 0 { - i -= len(x.Signature) - copy(dAtA[i:], x.Signature) - i = runtime.EncodeVarint(dAtA, i, uint64(len(x.Signature))) - i-- - dAtA[i] = 0x22 - } - if x.MigrationPayload != nil { - encoded, err := options.Marshal(x.MigrationPayload) - if err != nil { - return protoiface.MarshalOutput{ - NoUnkeyedLiterals: input.NoUnkeyedLiterals, - Buf: input.Buf, - }, err - } - i -= len(encoded) - copy(dAtA[i:], encoded) - i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) - i-- - dAtA[i] = 0x1a - } - if x.UniversalAccountId != nil { - encoded, err := options.Marshal(x.UniversalAccountId) + if x.Inbound != nil { + encoded, err := options.Marshal(x.Inbound) if err != nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -2302,7 +2227,7 @@ func (x *fastReflection_MsgMigrateUEA) ProtoMethods() *protoiface.Methods { }, nil } unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { - x := input.Message.Interface().(*MsgMigrateUEA) + x := input.Message.Interface().(*MsgVoteInbound) if x == nil { return protoiface.UnmarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -2334,10 +2259,10 @@ func (x *fastReflection_MsgMigrateUEA) ProtoMethods() *protoiface.Methods { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgMigrateUEA: wiretype end group for non-group") + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteInbound: wiretype end group for non-group") } if fieldNum <= 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgMigrateUEA: illegal tag %d (wire type %d)", fieldNum, wire) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteInbound: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: @@ -2374,43 +2299,7 @@ func (x *fastReflection_MsgMigrateUEA) ProtoMethods() *protoiface.Methods { iNdEx = postIndex case 2: if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field UniversalAccountId", wireType) - } - var msglen 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++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - if postIndex > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - if x.UniversalAccountId == nil { - x.UniversalAccountId = &UniversalAccountId{} - } - if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.UniversalAccountId); err != nil { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err - } - iNdEx = postIndex - case 3: - if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field MigrationPayload", wireType) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Inbound", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -2437,45 +2326,13 @@ func (x *fastReflection_MsgMigrateUEA) ProtoMethods() *protoiface.Methods { if postIndex > l { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF } - if x.MigrationPayload == nil { - x.MigrationPayload = &MigrationPayload{} + if x.Inbound == nil { + x.Inbound = &Inbound{} } - if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.MigrationPayload); err != nil { + if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.Inbound); err != nil { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err } iNdEx = postIndex - case 4: - if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Signature", wireType) - } - var stringLen uint64 - 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++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - if postIndex > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - x.Signature = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -2512,23 +2369,23 @@ func (x *fastReflection_MsgMigrateUEA) ProtoMethods() *protoiface.Methods { } var ( - md_MsgMigrateUEAResponse protoreflect.MessageDescriptor + md_MsgVoteInboundResponse protoreflect.MessageDescriptor ) func init() { file_uexecutor_v1_tx_proto_init() - md_MsgMigrateUEAResponse = File_uexecutor_v1_tx_proto.Messages().ByName("MsgMigrateUEAResponse") + md_MsgVoteInboundResponse = File_uexecutor_v1_tx_proto.Messages().ByName("MsgVoteInboundResponse") } -var _ protoreflect.Message = (*fastReflection_MsgMigrateUEAResponse)(nil) +var _ protoreflect.Message = (*fastReflection_MsgVoteInboundResponse)(nil) -type fastReflection_MsgMigrateUEAResponse MsgMigrateUEAResponse +type fastReflection_MsgVoteInboundResponse MsgVoteInboundResponse -func (x *MsgMigrateUEAResponse) ProtoReflect() protoreflect.Message { - return (*fastReflection_MsgMigrateUEAResponse)(x) +func (x *MsgVoteInboundResponse) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgVoteInboundResponse)(x) } -func (x *MsgMigrateUEAResponse) slowProtoReflect() protoreflect.Message { +func (x *MsgVoteInboundResponse) slowProtoReflect() protoreflect.Message { mi := &file_uexecutor_v1_tx_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2540,43 +2397,43 @@ func (x *MsgMigrateUEAResponse) slowProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -var _fastReflection_MsgMigrateUEAResponse_messageType fastReflection_MsgMigrateUEAResponse_messageType -var _ protoreflect.MessageType = fastReflection_MsgMigrateUEAResponse_messageType{} +var _fastReflection_MsgVoteInboundResponse_messageType fastReflection_MsgVoteInboundResponse_messageType +var _ protoreflect.MessageType = fastReflection_MsgVoteInboundResponse_messageType{} -type fastReflection_MsgMigrateUEAResponse_messageType struct{} +type fastReflection_MsgVoteInboundResponse_messageType struct{} -func (x fastReflection_MsgMigrateUEAResponse_messageType) Zero() protoreflect.Message { - return (*fastReflection_MsgMigrateUEAResponse)(nil) +func (x fastReflection_MsgVoteInboundResponse_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgVoteInboundResponse)(nil) } -func (x fastReflection_MsgMigrateUEAResponse_messageType) New() protoreflect.Message { - return new(fastReflection_MsgMigrateUEAResponse) +func (x fastReflection_MsgVoteInboundResponse_messageType) New() protoreflect.Message { + return new(fastReflection_MsgVoteInboundResponse) } -func (x fastReflection_MsgMigrateUEAResponse_messageType) Descriptor() protoreflect.MessageDescriptor { - return md_MsgMigrateUEAResponse +func (x fastReflection_MsgVoteInboundResponse_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgVoteInboundResponse } // Descriptor returns message descriptor, which contains only the protobuf // type information for the message. -func (x *fastReflection_MsgMigrateUEAResponse) Descriptor() protoreflect.MessageDescriptor { - return md_MsgMigrateUEAResponse +func (x *fastReflection_MsgVoteInboundResponse) Descriptor() protoreflect.MessageDescriptor { + return md_MsgVoteInboundResponse } // Type returns the message type, which encapsulates both Go and protobuf // type information. If the Go type information is not needed, // it is recommended that the message descriptor be used instead. -func (x *fastReflection_MsgMigrateUEAResponse) Type() protoreflect.MessageType { - return _fastReflection_MsgMigrateUEAResponse_messageType +func (x *fastReflection_MsgVoteInboundResponse) Type() protoreflect.MessageType { + return _fastReflection_MsgVoteInboundResponse_messageType } // New returns a newly allocated and mutable empty message. -func (x *fastReflection_MsgMigrateUEAResponse) New() protoreflect.Message { - return new(fastReflection_MsgMigrateUEAResponse) +func (x *fastReflection_MsgVoteInboundResponse) New() protoreflect.Message { + return new(fastReflection_MsgVoteInboundResponse) } // Interface unwraps the message reflection interface and // returns the underlying ProtoMessage interface. -func (x *fastReflection_MsgMigrateUEAResponse) Interface() protoreflect.ProtoMessage { - return (*MsgMigrateUEAResponse)(x) +func (x *fastReflection_MsgVoteInboundResponse) Interface() protoreflect.ProtoMessage { + return (*MsgVoteInboundResponse)(x) } // Range iterates over every populated field in an undefined order, @@ -2584,7 +2441,7 @@ func (x *fastReflection_MsgMigrateUEAResponse) Interface() protoreflect.ProtoMes // Range returns immediately if f returns false. // While iterating, mutating operations may only be performed // on the current field descriptor. -func (x *fastReflection_MsgMigrateUEAResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { +func (x *fastReflection_MsgVoteInboundResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { } // Has reports whether a field is populated. @@ -2598,13 +2455,13 @@ func (x *fastReflection_MsgMigrateUEAResponse) Range(f func(protoreflect.FieldDe // In other cases (aside from the nullable cases above), // a proto3 scalar field is populated if it contains a non-zero value, and // a repeated field is populated if it is non-empty. -func (x *fastReflection_MsgMigrateUEAResponse) Has(fd protoreflect.FieldDescriptor) bool { +func (x *fastReflection_MsgVoteInboundResponse) Has(fd protoreflect.FieldDescriptor) bool { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgMigrateUEAResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgMigrateUEAResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteInboundResponse does not contain field %s", fd.FullName())) } } @@ -2614,13 +2471,13 @@ func (x *fastReflection_MsgMigrateUEAResponse) Has(fd protoreflect.FieldDescript // associated with the given field number. // // Clear is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgMigrateUEAResponse) Clear(fd protoreflect.FieldDescriptor) { +func (x *fastReflection_MsgVoteInboundResponse) Clear(fd protoreflect.FieldDescriptor) { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgMigrateUEAResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgMigrateUEAResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteInboundResponse does not contain field %s", fd.FullName())) } } @@ -2630,13 +2487,13 @@ func (x *fastReflection_MsgMigrateUEAResponse) Clear(fd protoreflect.FieldDescri // the default value of a bytes scalar is guaranteed to be a copy. // For unpopulated composite types, it returns an empty, read-only view // of the value; to obtain a mutable reference, use Mutable. -func (x *fastReflection_MsgMigrateUEAResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteInboundResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { switch descriptor.FullName() { default: if descriptor.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgMigrateUEAResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgMigrateUEAResponse does not contain field %s", descriptor.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteInboundResponse does not contain field %s", descriptor.FullName())) } } @@ -2650,13 +2507,13 @@ func (x *fastReflection_MsgMigrateUEAResponse) Get(descriptor protoreflect.Field // empty, read-only value, then it panics. // // Set is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgMigrateUEAResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { +func (x *fastReflection_MsgVoteInboundResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgMigrateUEAResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgMigrateUEAResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteInboundResponse does not contain field %s", fd.FullName())) } } @@ -2670,36 +2527,36 @@ func (x *fastReflection_MsgMigrateUEAResponse) Set(fd protoreflect.FieldDescript // It panics if the field does not contain a composite type. // // Mutable is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgMigrateUEAResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteInboundResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgMigrateUEAResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgMigrateUEAResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteInboundResponse does not contain field %s", fd.FullName())) } } // NewField returns a new value that is assignable to the field // for the given descriptor. For scalars, this returns the default value. // For lists, maps, and messages, this returns a new, empty, mutable value. -func (x *fastReflection_MsgMigrateUEAResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteInboundResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgMigrateUEAResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgMigrateUEAResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteInboundResponse does not contain field %s", fd.FullName())) } } // WhichOneof reports which field within the oneof is populated, // returning nil if none are populated. // It panics if the oneof descriptor does not belong to this message. -func (x *fastReflection_MsgMigrateUEAResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { +func (x *fastReflection_MsgVoteInboundResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { switch d.FullName() { default: - panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgMigrateUEAResponse", d.FullName())) + panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgVoteInboundResponse", d.FullName())) } panic("unreachable") } @@ -2707,7 +2564,7 @@ func (x *fastReflection_MsgMigrateUEAResponse) WhichOneof(d protoreflect.OneofDe // GetUnknown retrieves the entire list of unknown fields. // The caller may only mutate the contents of the RawFields // if the mutated bytes are stored back into the message with SetUnknown. -func (x *fastReflection_MsgMigrateUEAResponse) GetUnknown() protoreflect.RawFields { +func (x *fastReflection_MsgVoteInboundResponse) GetUnknown() protoreflect.RawFields { return x.unknownFields } @@ -2718,7 +2575,7 @@ func (x *fastReflection_MsgMigrateUEAResponse) GetUnknown() protoreflect.RawFiel // An empty RawFields may be passed to clear the fields. // // SetUnknown is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgMigrateUEAResponse) SetUnknown(fields protoreflect.RawFields) { +func (x *fastReflection_MsgVoteInboundResponse) SetUnknown(fields protoreflect.RawFields) { x.unknownFields = fields } @@ -2730,7 +2587,7 @@ func (x *fastReflection_MsgMigrateUEAResponse) SetUnknown(fields protoreflect.Ra // message type, but the details are implementation dependent. // Validity is not part of the protobuf data model, and may not // be preserved in marshaling or other operations. -func (x *fastReflection_MsgMigrateUEAResponse) IsValid() bool { +func (x *fastReflection_MsgVoteInboundResponse) IsValid() bool { return x != nil } @@ -2740,9 +2597,9 @@ func (x *fastReflection_MsgMigrateUEAResponse) IsValid() bool { // The returned methods type is identical to // "google.golang.org/protobuf/runtime/protoiface".Methods. // Consult the protoiface package documentation for details. -func (x *fastReflection_MsgMigrateUEAResponse) ProtoMethods() *protoiface.Methods { +func (x *fastReflection_MsgVoteInboundResponse) ProtoMethods() *protoiface.Methods { size := func(input protoiface.SizeInput) protoiface.SizeOutput { - x := input.Message.Interface().(*MsgMigrateUEAResponse) + x := input.Message.Interface().(*MsgVoteInboundResponse) if x == nil { return protoiface.SizeOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -2764,7 +2621,7 @@ func (x *fastReflection_MsgMigrateUEAResponse) ProtoMethods() *protoiface.Method } marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { - x := input.Message.Interface().(*MsgMigrateUEAResponse) + x := input.Message.Interface().(*MsgVoteInboundResponse) if x == nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -2794,7 +2651,7 @@ func (x *fastReflection_MsgMigrateUEAResponse) ProtoMethods() *protoiface.Method }, nil } unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { - x := input.Message.Interface().(*MsgMigrateUEAResponse) + x := input.Message.Interface().(*MsgVoteInboundResponse) if x == nil { return protoiface.UnmarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -2826,10 +2683,10 @@ func (x *fastReflection_MsgMigrateUEAResponse) ProtoMethods() *protoiface.Method fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgMigrateUEAResponse: wiretype end group for non-group") + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteInboundResponse: wiretype end group for non-group") } if fieldNum <= 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgMigrateUEAResponse: illegal tag %d (wire type %d)", fieldNum, wire) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteInboundResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { default: @@ -2868,27 +2725,31 @@ func (x *fastReflection_MsgMigrateUEAResponse) ProtoMethods() *protoiface.Method } var ( - md_MsgVoteInbound protoreflect.MessageDescriptor - fd_MsgVoteInbound_signer protoreflect.FieldDescriptor - fd_MsgVoteInbound_inbound protoreflect.FieldDescriptor + md_MsgVoteOutbound protoreflect.MessageDescriptor + fd_MsgVoteOutbound_signer protoreflect.FieldDescriptor + fd_MsgVoteOutbound_tx_id protoreflect.FieldDescriptor + fd_MsgVoteOutbound_utx_id protoreflect.FieldDescriptor + fd_MsgVoteOutbound_observed_tx protoreflect.FieldDescriptor ) func init() { file_uexecutor_v1_tx_proto_init() - md_MsgVoteInbound = File_uexecutor_v1_tx_proto.Messages().ByName("MsgVoteInbound") - fd_MsgVoteInbound_signer = md_MsgVoteInbound.Fields().ByName("signer") - fd_MsgVoteInbound_inbound = md_MsgVoteInbound.Fields().ByName("inbound") + md_MsgVoteOutbound = File_uexecutor_v1_tx_proto.Messages().ByName("MsgVoteOutbound") + fd_MsgVoteOutbound_signer = md_MsgVoteOutbound.Fields().ByName("signer") + fd_MsgVoteOutbound_tx_id = md_MsgVoteOutbound.Fields().ByName("tx_id") + fd_MsgVoteOutbound_utx_id = md_MsgVoteOutbound.Fields().ByName("utx_id") + fd_MsgVoteOutbound_observed_tx = md_MsgVoteOutbound.Fields().ByName("observed_tx") } -var _ protoreflect.Message = (*fastReflection_MsgVoteInbound)(nil) +var _ protoreflect.Message = (*fastReflection_MsgVoteOutbound)(nil) -type fastReflection_MsgVoteInbound MsgVoteInbound +type fastReflection_MsgVoteOutbound MsgVoteOutbound -func (x *MsgVoteInbound) ProtoReflect() protoreflect.Message { - return (*fastReflection_MsgVoteInbound)(x) +func (x *MsgVoteOutbound) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgVoteOutbound)(x) } -func (x *MsgVoteInbound) slowProtoReflect() protoreflect.Message { +func (x *MsgVoteOutbound) slowProtoReflect() protoreflect.Message { mi := &file_uexecutor_v1_tx_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2900,43 +2761,43 @@ func (x *MsgVoteInbound) slowProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -var _fastReflection_MsgVoteInbound_messageType fastReflection_MsgVoteInbound_messageType -var _ protoreflect.MessageType = fastReflection_MsgVoteInbound_messageType{} +var _fastReflection_MsgVoteOutbound_messageType fastReflection_MsgVoteOutbound_messageType +var _ protoreflect.MessageType = fastReflection_MsgVoteOutbound_messageType{} -type fastReflection_MsgVoteInbound_messageType struct{} +type fastReflection_MsgVoteOutbound_messageType struct{} -func (x fastReflection_MsgVoteInbound_messageType) Zero() protoreflect.Message { - return (*fastReflection_MsgVoteInbound)(nil) +func (x fastReflection_MsgVoteOutbound_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgVoteOutbound)(nil) } -func (x fastReflection_MsgVoteInbound_messageType) New() protoreflect.Message { - return new(fastReflection_MsgVoteInbound) +func (x fastReflection_MsgVoteOutbound_messageType) New() protoreflect.Message { + return new(fastReflection_MsgVoteOutbound) } -func (x fastReflection_MsgVoteInbound_messageType) Descriptor() protoreflect.MessageDescriptor { - return md_MsgVoteInbound +func (x fastReflection_MsgVoteOutbound_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgVoteOutbound } // Descriptor returns message descriptor, which contains only the protobuf // type information for the message. -func (x *fastReflection_MsgVoteInbound) Descriptor() protoreflect.MessageDescriptor { - return md_MsgVoteInbound +func (x *fastReflection_MsgVoteOutbound) Descriptor() protoreflect.MessageDescriptor { + return md_MsgVoteOutbound } // Type returns the message type, which encapsulates both Go and protobuf // type information. If the Go type information is not needed, // it is recommended that the message descriptor be used instead. -func (x *fastReflection_MsgVoteInbound) Type() protoreflect.MessageType { - return _fastReflection_MsgVoteInbound_messageType +func (x *fastReflection_MsgVoteOutbound) Type() protoreflect.MessageType { + return _fastReflection_MsgVoteOutbound_messageType } // New returns a newly allocated and mutable empty message. -func (x *fastReflection_MsgVoteInbound) New() protoreflect.Message { - return new(fastReflection_MsgVoteInbound) +func (x *fastReflection_MsgVoteOutbound) New() protoreflect.Message { + return new(fastReflection_MsgVoteOutbound) } // Interface unwraps the message reflection interface and // returns the underlying ProtoMessage interface. -func (x *fastReflection_MsgVoteInbound) Interface() protoreflect.ProtoMessage { - return (*MsgVoteInbound)(x) +func (x *fastReflection_MsgVoteOutbound) Interface() protoreflect.ProtoMessage { + return (*MsgVoteOutbound)(x) } // Range iterates over every populated field in an undefined order, @@ -2944,16 +2805,28 @@ func (x *fastReflection_MsgVoteInbound) Interface() protoreflect.ProtoMessage { // Range returns immediately if f returns false. // While iterating, mutating operations may only be performed // on the current field descriptor. -func (x *fastReflection_MsgVoteInbound) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { +func (x *fastReflection_MsgVoteOutbound) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { if x.Signer != "" { value := protoreflect.ValueOfString(x.Signer) - if !f(fd_MsgVoteInbound_signer, value) { + if !f(fd_MsgVoteOutbound_signer, value) { return } } - if x.Inbound != nil { - value := protoreflect.ValueOfMessage(x.Inbound.ProtoReflect()) - if !f(fd_MsgVoteInbound_inbound, value) { + if x.TxId != "" { + value := protoreflect.ValueOfString(x.TxId) + if !f(fd_MsgVoteOutbound_tx_id, value) { + return + } + } + if x.UtxId != "" { + value := protoreflect.ValueOfString(x.UtxId) + if !f(fd_MsgVoteOutbound_utx_id, value) { + return + } + } + if x.ObservedTx != nil { + value := protoreflect.ValueOfMessage(x.ObservedTx.ProtoReflect()) + if !f(fd_MsgVoteOutbound_observed_tx, value) { return } } @@ -2970,17 +2843,21 @@ func (x *fastReflection_MsgVoteInbound) Range(f func(protoreflect.FieldDescripto // In other cases (aside from the nullable cases above), // a proto3 scalar field is populated if it contains a non-zero value, and // a repeated field is populated if it is non-empty. -func (x *fastReflection_MsgVoteInbound) Has(fd protoreflect.FieldDescriptor) bool { +func (x *fastReflection_MsgVoteOutbound) Has(fd protoreflect.FieldDescriptor) bool { switch fd.FullName() { - case "uexecutor.v1.MsgVoteInbound.signer": + case "uexecutor.v1.MsgVoteOutbound.signer": return x.Signer != "" - case "uexecutor.v1.MsgVoteInbound.inbound": - return x.Inbound != nil + case "uexecutor.v1.MsgVoteOutbound.tx_id": + return x.TxId != "" + case "uexecutor.v1.MsgVoteOutbound.utx_id": + return x.UtxId != "" + case "uexecutor.v1.MsgVoteOutbound.observed_tx": + return x.ObservedTx != nil default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteInbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutbound does not contain field %s", fd.FullName())) } } @@ -2990,17 +2867,21 @@ func (x *fastReflection_MsgVoteInbound) Has(fd protoreflect.FieldDescriptor) boo // associated with the given field number. // // Clear is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteInbound) Clear(fd protoreflect.FieldDescriptor) { +func (x *fastReflection_MsgVoteOutbound) Clear(fd protoreflect.FieldDescriptor) { switch fd.FullName() { - case "uexecutor.v1.MsgVoteInbound.signer": + case "uexecutor.v1.MsgVoteOutbound.signer": x.Signer = "" - case "uexecutor.v1.MsgVoteInbound.inbound": - x.Inbound = nil + case "uexecutor.v1.MsgVoteOutbound.tx_id": + x.TxId = "" + case "uexecutor.v1.MsgVoteOutbound.utx_id": + x.UtxId = "" + case "uexecutor.v1.MsgVoteOutbound.observed_tx": + x.ObservedTx = nil default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteInbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutbound does not contain field %s", fd.FullName())) } } @@ -3010,19 +2891,25 @@ func (x *fastReflection_MsgVoteInbound) Clear(fd protoreflect.FieldDescriptor) { // the default value of a bytes scalar is guaranteed to be a copy. // For unpopulated composite types, it returns an empty, read-only view // of the value; to obtain a mutable reference, use Mutable. -func (x *fastReflection_MsgVoteInbound) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteOutbound) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { switch descriptor.FullName() { - case "uexecutor.v1.MsgVoteInbound.signer": + case "uexecutor.v1.MsgVoteOutbound.signer": value := x.Signer return protoreflect.ValueOfString(value) - case "uexecutor.v1.MsgVoteInbound.inbound": - value := x.Inbound + case "uexecutor.v1.MsgVoteOutbound.tx_id": + value := x.TxId + return protoreflect.ValueOfString(value) + case "uexecutor.v1.MsgVoteOutbound.utx_id": + value := x.UtxId + return protoreflect.ValueOfString(value) + case "uexecutor.v1.MsgVoteOutbound.observed_tx": + value := x.ObservedTx return protoreflect.ValueOfMessage(value.ProtoReflect()) default: if descriptor.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteInbound does not contain field %s", descriptor.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutbound does not contain field %s", descriptor.FullName())) } } @@ -3036,17 +2923,21 @@ func (x *fastReflection_MsgVoteInbound) Get(descriptor protoreflect.FieldDescrip // empty, read-only value, then it panics. // // Set is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteInbound) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { +func (x *fastReflection_MsgVoteOutbound) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { switch fd.FullName() { - case "uexecutor.v1.MsgVoteInbound.signer": + case "uexecutor.v1.MsgVoteOutbound.signer": x.Signer = value.Interface().(string) - case "uexecutor.v1.MsgVoteInbound.inbound": - x.Inbound = value.Message().Interface().(*Inbound) + case "uexecutor.v1.MsgVoteOutbound.tx_id": + x.TxId = value.Interface().(string) + case "uexecutor.v1.MsgVoteOutbound.utx_id": + x.UtxId = value.Interface().(string) + case "uexecutor.v1.MsgVoteOutbound.observed_tx": + x.ObservedTx = value.Message().Interface().(*OutboundObservation) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteInbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutbound does not contain field %s", fd.FullName())) } } @@ -3060,48 +2951,56 @@ func (x *fastReflection_MsgVoteInbound) Set(fd protoreflect.FieldDescriptor, val // It panics if the field does not contain a composite type. // // Mutable is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteInbound) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteOutbound) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { - case "uexecutor.v1.MsgVoteInbound.inbound": - if x.Inbound == nil { - x.Inbound = new(Inbound) + case "uexecutor.v1.MsgVoteOutbound.observed_tx": + if x.ObservedTx == nil { + x.ObservedTx = new(OutboundObservation) } - return protoreflect.ValueOfMessage(x.Inbound.ProtoReflect()) - case "uexecutor.v1.MsgVoteInbound.signer": - panic(fmt.Errorf("field signer of message uexecutor.v1.MsgVoteInbound is not mutable")) + return protoreflect.ValueOfMessage(x.ObservedTx.ProtoReflect()) + case "uexecutor.v1.MsgVoteOutbound.signer": + panic(fmt.Errorf("field signer of message uexecutor.v1.MsgVoteOutbound is not mutable")) + case "uexecutor.v1.MsgVoteOutbound.tx_id": + panic(fmt.Errorf("field tx_id of message uexecutor.v1.MsgVoteOutbound is not mutable")) + case "uexecutor.v1.MsgVoteOutbound.utx_id": + panic(fmt.Errorf("field utx_id of message uexecutor.v1.MsgVoteOutbound is not mutable")) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteInbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutbound does not contain field %s", fd.FullName())) } } // NewField returns a new value that is assignable to the field // for the given descriptor. For scalars, this returns the default value. // For lists, maps, and messages, this returns a new, empty, mutable value. -func (x *fastReflection_MsgVoteInbound) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteOutbound) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { - case "uexecutor.v1.MsgVoteInbound.signer": + case "uexecutor.v1.MsgVoteOutbound.signer": return protoreflect.ValueOfString("") - case "uexecutor.v1.MsgVoteInbound.inbound": - m := new(Inbound) + case "uexecutor.v1.MsgVoteOutbound.tx_id": + return protoreflect.ValueOfString("") + case "uexecutor.v1.MsgVoteOutbound.utx_id": + return protoreflect.ValueOfString("") + case "uexecutor.v1.MsgVoteOutbound.observed_tx": + m := new(OutboundObservation) return protoreflect.ValueOfMessage(m.ProtoReflect()) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteInbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutbound does not contain field %s", fd.FullName())) } } // WhichOneof reports which field within the oneof is populated, // returning nil if none are populated. // It panics if the oneof descriptor does not belong to this message. -func (x *fastReflection_MsgVoteInbound) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { +func (x *fastReflection_MsgVoteOutbound) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { switch d.FullName() { default: - panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgVoteInbound", d.FullName())) + panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgVoteOutbound", d.FullName())) } panic("unreachable") } @@ -3109,7 +3008,7 @@ func (x *fastReflection_MsgVoteInbound) WhichOneof(d protoreflect.OneofDescripto // GetUnknown retrieves the entire list of unknown fields. // The caller may only mutate the contents of the RawFields // if the mutated bytes are stored back into the message with SetUnknown. -func (x *fastReflection_MsgVoteInbound) GetUnknown() protoreflect.RawFields { +func (x *fastReflection_MsgVoteOutbound) GetUnknown() protoreflect.RawFields { return x.unknownFields } @@ -3120,7 +3019,7 @@ func (x *fastReflection_MsgVoteInbound) GetUnknown() protoreflect.RawFields { // An empty RawFields may be passed to clear the fields. // // SetUnknown is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteInbound) SetUnknown(fields protoreflect.RawFields) { +func (x *fastReflection_MsgVoteOutbound) SetUnknown(fields protoreflect.RawFields) { x.unknownFields = fields } @@ -3132,7 +3031,7 @@ func (x *fastReflection_MsgVoteInbound) SetUnknown(fields protoreflect.RawFields // message type, but the details are implementation dependent. // Validity is not part of the protobuf data model, and may not // be preserved in marshaling or other operations. -func (x *fastReflection_MsgVoteInbound) IsValid() bool { +func (x *fastReflection_MsgVoteOutbound) IsValid() bool { return x != nil } @@ -3142,9 +3041,9 @@ func (x *fastReflection_MsgVoteInbound) IsValid() bool { // The returned methods type is identical to // "google.golang.org/protobuf/runtime/protoiface".Methods. // Consult the protoiface package documentation for details. -func (x *fastReflection_MsgVoteInbound) ProtoMethods() *protoiface.Methods { +func (x *fastReflection_MsgVoteOutbound) ProtoMethods() *protoiface.Methods { size := func(input protoiface.SizeInput) protoiface.SizeOutput { - x := input.Message.Interface().(*MsgVoteInbound) + x := input.Message.Interface().(*MsgVoteOutbound) if x == nil { return protoiface.SizeOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -3160,8 +3059,16 @@ func (x *fastReflection_MsgVoteInbound) ProtoMethods() *protoiface.Methods { if l > 0 { n += 1 + l + runtime.Sov(uint64(l)) } - if x.Inbound != nil { - l = options.Size(x.Inbound) + l = len(x.TxId) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + l = len(x.UtxId) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + if x.ObservedTx != nil { + l = options.Size(x.ObservedTx) n += 1 + l + runtime.Sov(uint64(l)) } if x.unknownFields != nil { @@ -3174,7 +3081,7 @@ func (x *fastReflection_MsgVoteInbound) ProtoMethods() *protoiface.Methods { } marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { - x := input.Message.Interface().(*MsgVoteInbound) + x := input.Message.Interface().(*MsgVoteOutbound) if x == nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -3193,8 +3100,8 @@ func (x *fastReflection_MsgVoteInbound) ProtoMethods() *protoiface.Methods { i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } - if x.Inbound != nil { - encoded, err := options.Marshal(x.Inbound) + if x.ObservedTx != nil { + encoded, err := options.Marshal(x.ObservedTx) if err != nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -3205,6 +3112,20 @@ func (x *fastReflection_MsgVoteInbound) ProtoMethods() *protoiface.Methods { copy(dAtA[i:], encoded) i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) i-- + dAtA[i] = 0x22 + } + if len(x.UtxId) > 0 { + i -= len(x.UtxId) + copy(dAtA[i:], x.UtxId) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.UtxId))) + i-- + dAtA[i] = 0x1a + } + if len(x.TxId) > 0 { + i -= len(x.TxId) + copy(dAtA[i:], x.TxId) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.TxId))) + i-- dAtA[i] = 0x12 } if len(x.Signer) > 0 { @@ -3225,7 +3146,7 @@ func (x *fastReflection_MsgVoteInbound) ProtoMethods() *protoiface.Methods { }, nil } unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { - x := input.Message.Interface().(*MsgVoteInbound) + x := input.Message.Interface().(*MsgVoteOutbound) if x == nil { return protoiface.UnmarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -3257,10 +3178,10 @@ func (x *fastReflection_MsgVoteInbound) ProtoMethods() *protoiface.Methods { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteInbound: wiretype end group for non-group") + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteOutbound: wiretype end group for non-group") } if fieldNum <= 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteInbound: illegal tag %d (wire type %d)", fieldNum, wire) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteOutbound: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: @@ -3297,9 +3218,9 @@ func (x *fastReflection_MsgVoteInbound) ProtoMethods() *protoiface.Methods { iNdEx = postIndex case 2: if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Inbound", wireType) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field TxId", wireType) } - var msglen int + var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow @@ -3309,29 +3230,93 @@ func (x *fastReflection_MsgVoteInbound) ProtoMethods() *protoiface.Methods { } b := dAtA[iNdEx] iNdEx++ - msglen |= int(b&0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } - if msglen < 0 { + intStringLen := int(stringLen) + if intStringLen < 0 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength } - postIndex := iNdEx + msglen + postIndex := iNdEx + intStringLen if postIndex < 0 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength } if postIndex > l { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF } - if x.Inbound == nil { - x.Inbound = &Inbound{} - } - if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.Inbound); err != nil { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err - } + x.TxId = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex - default: + case 3: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field UtxId", wireType) + } + var stringLen uint64 + 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++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.UtxId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ObservedTx", wireType) + } + var msglen 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++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if x.ObservedTx == nil { + x.ObservedTx = &OutboundObservation{} + } + if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.ObservedTx); err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + iNdEx = postIndex + default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) if err != nil { @@ -3367,23 +3352,23 @@ func (x *fastReflection_MsgVoteInbound) ProtoMethods() *protoiface.Methods { } var ( - md_MsgVoteInboundResponse protoreflect.MessageDescriptor + md_MsgVoteOutboundResponse protoreflect.MessageDescriptor ) func init() { file_uexecutor_v1_tx_proto_init() - md_MsgVoteInboundResponse = File_uexecutor_v1_tx_proto.Messages().ByName("MsgVoteInboundResponse") + md_MsgVoteOutboundResponse = File_uexecutor_v1_tx_proto.Messages().ByName("MsgVoteOutboundResponse") } -var _ protoreflect.Message = (*fastReflection_MsgVoteInboundResponse)(nil) +var _ protoreflect.Message = (*fastReflection_MsgVoteOutboundResponse)(nil) -type fastReflection_MsgVoteInboundResponse MsgVoteInboundResponse +type fastReflection_MsgVoteOutboundResponse MsgVoteOutboundResponse -func (x *MsgVoteInboundResponse) ProtoReflect() protoreflect.Message { - return (*fastReflection_MsgVoteInboundResponse)(x) +func (x *MsgVoteOutboundResponse) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgVoteOutboundResponse)(x) } -func (x *MsgVoteInboundResponse) slowProtoReflect() protoreflect.Message { +func (x *MsgVoteOutboundResponse) slowProtoReflect() protoreflect.Message { mi := &file_uexecutor_v1_tx_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3395,43 +3380,43 @@ func (x *MsgVoteInboundResponse) slowProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -var _fastReflection_MsgVoteInboundResponse_messageType fastReflection_MsgVoteInboundResponse_messageType -var _ protoreflect.MessageType = fastReflection_MsgVoteInboundResponse_messageType{} +var _fastReflection_MsgVoteOutboundResponse_messageType fastReflection_MsgVoteOutboundResponse_messageType +var _ protoreflect.MessageType = fastReflection_MsgVoteOutboundResponse_messageType{} -type fastReflection_MsgVoteInboundResponse_messageType struct{} +type fastReflection_MsgVoteOutboundResponse_messageType struct{} -func (x fastReflection_MsgVoteInboundResponse_messageType) Zero() protoreflect.Message { - return (*fastReflection_MsgVoteInboundResponse)(nil) +func (x fastReflection_MsgVoteOutboundResponse_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgVoteOutboundResponse)(nil) } -func (x fastReflection_MsgVoteInboundResponse_messageType) New() protoreflect.Message { - return new(fastReflection_MsgVoteInboundResponse) +func (x fastReflection_MsgVoteOutboundResponse_messageType) New() protoreflect.Message { + return new(fastReflection_MsgVoteOutboundResponse) } -func (x fastReflection_MsgVoteInboundResponse_messageType) Descriptor() protoreflect.MessageDescriptor { - return md_MsgVoteInboundResponse +func (x fastReflection_MsgVoteOutboundResponse_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgVoteOutboundResponse } // Descriptor returns message descriptor, which contains only the protobuf // type information for the message. -func (x *fastReflection_MsgVoteInboundResponse) Descriptor() protoreflect.MessageDescriptor { - return md_MsgVoteInboundResponse +func (x *fastReflection_MsgVoteOutboundResponse) Descriptor() protoreflect.MessageDescriptor { + return md_MsgVoteOutboundResponse } // Type returns the message type, which encapsulates both Go and protobuf // type information. If the Go type information is not needed, // it is recommended that the message descriptor be used instead. -func (x *fastReflection_MsgVoteInboundResponse) Type() protoreflect.MessageType { - return _fastReflection_MsgVoteInboundResponse_messageType +func (x *fastReflection_MsgVoteOutboundResponse) Type() protoreflect.MessageType { + return _fastReflection_MsgVoteOutboundResponse_messageType } // New returns a newly allocated and mutable empty message. -func (x *fastReflection_MsgVoteInboundResponse) New() protoreflect.Message { - return new(fastReflection_MsgVoteInboundResponse) +func (x *fastReflection_MsgVoteOutboundResponse) New() protoreflect.Message { + return new(fastReflection_MsgVoteOutboundResponse) } // Interface unwraps the message reflection interface and // returns the underlying ProtoMessage interface. -func (x *fastReflection_MsgVoteInboundResponse) Interface() protoreflect.ProtoMessage { - return (*MsgVoteInboundResponse)(x) +func (x *fastReflection_MsgVoteOutboundResponse) Interface() protoreflect.ProtoMessage { + return (*MsgVoteOutboundResponse)(x) } // Range iterates over every populated field in an undefined order, @@ -3439,7 +3424,7 @@ func (x *fastReflection_MsgVoteInboundResponse) Interface() protoreflect.ProtoMe // Range returns immediately if f returns false. // While iterating, mutating operations may only be performed // on the current field descriptor. -func (x *fastReflection_MsgVoteInboundResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { +func (x *fastReflection_MsgVoteOutboundResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { } // Has reports whether a field is populated. @@ -3453,13 +3438,13 @@ func (x *fastReflection_MsgVoteInboundResponse) Range(f func(protoreflect.FieldD // In other cases (aside from the nullable cases above), // a proto3 scalar field is populated if it contains a non-zero value, and // a repeated field is populated if it is non-empty. -func (x *fastReflection_MsgVoteInboundResponse) Has(fd protoreflect.FieldDescriptor) bool { +func (x *fastReflection_MsgVoteOutboundResponse) Has(fd protoreflect.FieldDescriptor) bool { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteInboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutboundResponse does not contain field %s", fd.FullName())) } } @@ -3469,13 +3454,13 @@ func (x *fastReflection_MsgVoteInboundResponse) Has(fd protoreflect.FieldDescrip // associated with the given field number. // // Clear is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteInboundResponse) Clear(fd protoreflect.FieldDescriptor) { +func (x *fastReflection_MsgVoteOutboundResponse) Clear(fd protoreflect.FieldDescriptor) { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteInboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutboundResponse does not contain field %s", fd.FullName())) } } @@ -3485,13 +3470,13 @@ func (x *fastReflection_MsgVoteInboundResponse) Clear(fd protoreflect.FieldDescr // the default value of a bytes scalar is guaranteed to be a copy. // For unpopulated composite types, it returns an empty, read-only view // of the value; to obtain a mutable reference, use Mutable. -func (x *fastReflection_MsgVoteInboundResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteOutboundResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { switch descriptor.FullName() { default: if descriptor.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteInboundResponse does not contain field %s", descriptor.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutboundResponse does not contain field %s", descriptor.FullName())) } } @@ -3505,13 +3490,13 @@ func (x *fastReflection_MsgVoteInboundResponse) Get(descriptor protoreflect.Fiel // empty, read-only value, then it panics. // // Set is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteInboundResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { +func (x *fastReflection_MsgVoteOutboundResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteInboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutboundResponse does not contain field %s", fd.FullName())) } } @@ -3525,36 +3510,36 @@ func (x *fastReflection_MsgVoteInboundResponse) Set(fd protoreflect.FieldDescrip // It panics if the field does not contain a composite type. // // Mutable is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteInboundResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteOutboundResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteInboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutboundResponse does not contain field %s", fd.FullName())) } } // NewField returns a new value that is assignable to the field // for the given descriptor. For scalars, this returns the default value. // For lists, maps, and messages, this returns a new, empty, mutable value. -func (x *fastReflection_MsgVoteInboundResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteOutboundResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteInboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteInboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutboundResponse does not contain field %s", fd.FullName())) } } // WhichOneof reports which field within the oneof is populated, // returning nil if none are populated. // It panics if the oneof descriptor does not belong to this message. -func (x *fastReflection_MsgVoteInboundResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { +func (x *fastReflection_MsgVoteOutboundResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { switch d.FullName() { default: - panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgVoteInboundResponse", d.FullName())) + panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgVoteOutboundResponse", d.FullName())) } panic("unreachable") } @@ -3562,7 +3547,7 @@ func (x *fastReflection_MsgVoteInboundResponse) WhichOneof(d protoreflect.OneofD // GetUnknown retrieves the entire list of unknown fields. // The caller may only mutate the contents of the RawFields // if the mutated bytes are stored back into the message with SetUnknown. -func (x *fastReflection_MsgVoteInboundResponse) GetUnknown() protoreflect.RawFields { +func (x *fastReflection_MsgVoteOutboundResponse) GetUnknown() protoreflect.RawFields { return x.unknownFields } @@ -3573,7 +3558,7 @@ func (x *fastReflection_MsgVoteInboundResponse) GetUnknown() protoreflect.RawFie // An empty RawFields may be passed to clear the fields. // // SetUnknown is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteInboundResponse) SetUnknown(fields protoreflect.RawFields) { +func (x *fastReflection_MsgVoteOutboundResponse) SetUnknown(fields protoreflect.RawFields) { x.unknownFields = fields } @@ -3585,7 +3570,7 @@ func (x *fastReflection_MsgVoteInboundResponse) SetUnknown(fields protoreflect.R // message type, but the details are implementation dependent. // Validity is not part of the protobuf data model, and may not // be preserved in marshaling or other operations. -func (x *fastReflection_MsgVoteInboundResponse) IsValid() bool { +func (x *fastReflection_MsgVoteOutboundResponse) IsValid() bool { return x != nil } @@ -3595,9 +3580,9 @@ func (x *fastReflection_MsgVoteInboundResponse) IsValid() bool { // The returned methods type is identical to // "google.golang.org/protobuf/runtime/protoiface".Methods. // Consult the protoiface package documentation for details. -func (x *fastReflection_MsgVoteInboundResponse) ProtoMethods() *protoiface.Methods { +func (x *fastReflection_MsgVoteOutboundResponse) ProtoMethods() *protoiface.Methods { size := func(input protoiface.SizeInput) protoiface.SizeOutput { - x := input.Message.Interface().(*MsgVoteInboundResponse) + x := input.Message.Interface().(*MsgVoteOutboundResponse) if x == nil { return protoiface.SizeOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -3619,7 +3604,7 @@ func (x *fastReflection_MsgVoteInboundResponse) ProtoMethods() *protoiface.Metho } marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { - x := input.Message.Interface().(*MsgVoteInboundResponse) + x := input.Message.Interface().(*MsgVoteOutboundResponse) if x == nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -3649,7 +3634,7 @@ func (x *fastReflection_MsgVoteInboundResponse) ProtoMethods() *protoiface.Metho }, nil } unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { - x := input.Message.Interface().(*MsgVoteInboundResponse) + x := input.Message.Interface().(*MsgVoteOutboundResponse) if x == nil { return protoiface.UnmarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -3681,10 +3666,10 @@ func (x *fastReflection_MsgVoteInboundResponse) ProtoMethods() *protoiface.Metho fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteInboundResponse: wiretype end group for non-group") + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteOutboundResponse: wiretype end group for non-group") } if fieldNum <= 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteInboundResponse: illegal tag %d (wire type %d)", fieldNum, wire) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteOutboundResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { default: @@ -3723,31 +3708,31 @@ func (x *fastReflection_MsgVoteInboundResponse) ProtoMethods() *protoiface.Metho } var ( - md_MsgVoteOutbound protoreflect.MessageDescriptor - fd_MsgVoteOutbound_signer protoreflect.FieldDescriptor - fd_MsgVoteOutbound_tx_id protoreflect.FieldDescriptor - fd_MsgVoteOutbound_utx_id protoreflect.FieldDescriptor - fd_MsgVoteOutbound_observed_tx protoreflect.FieldDescriptor + md_MsgVoteChainMeta protoreflect.MessageDescriptor + fd_MsgVoteChainMeta_signer protoreflect.FieldDescriptor + fd_MsgVoteChainMeta_observed_chain_id protoreflect.FieldDescriptor + fd_MsgVoteChainMeta_price protoreflect.FieldDescriptor + fd_MsgVoteChainMeta_chain_height protoreflect.FieldDescriptor ) func init() { file_uexecutor_v1_tx_proto_init() - md_MsgVoteOutbound = File_uexecutor_v1_tx_proto.Messages().ByName("MsgVoteOutbound") - fd_MsgVoteOutbound_signer = md_MsgVoteOutbound.Fields().ByName("signer") - fd_MsgVoteOutbound_tx_id = md_MsgVoteOutbound.Fields().ByName("tx_id") - fd_MsgVoteOutbound_utx_id = md_MsgVoteOutbound.Fields().ByName("utx_id") - fd_MsgVoteOutbound_observed_tx = md_MsgVoteOutbound.Fields().ByName("observed_tx") + md_MsgVoteChainMeta = File_uexecutor_v1_tx_proto.Messages().ByName("MsgVoteChainMeta") + fd_MsgVoteChainMeta_signer = md_MsgVoteChainMeta.Fields().ByName("signer") + fd_MsgVoteChainMeta_observed_chain_id = md_MsgVoteChainMeta.Fields().ByName("observed_chain_id") + fd_MsgVoteChainMeta_price = md_MsgVoteChainMeta.Fields().ByName("price") + fd_MsgVoteChainMeta_chain_height = md_MsgVoteChainMeta.Fields().ByName("chain_height") } -var _ protoreflect.Message = (*fastReflection_MsgVoteOutbound)(nil) +var _ protoreflect.Message = (*fastReflection_MsgVoteChainMeta)(nil) -type fastReflection_MsgVoteOutbound MsgVoteOutbound +type fastReflection_MsgVoteChainMeta MsgVoteChainMeta -func (x *MsgVoteOutbound) ProtoReflect() protoreflect.Message { - return (*fastReflection_MsgVoteOutbound)(x) +func (x *MsgVoteChainMeta) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgVoteChainMeta)(x) } -func (x *MsgVoteOutbound) slowProtoReflect() protoreflect.Message { +func (x *MsgVoteChainMeta) slowProtoReflect() protoreflect.Message { mi := &file_uexecutor_v1_tx_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3759,43 +3744,43 @@ func (x *MsgVoteOutbound) slowProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -var _fastReflection_MsgVoteOutbound_messageType fastReflection_MsgVoteOutbound_messageType -var _ protoreflect.MessageType = fastReflection_MsgVoteOutbound_messageType{} +var _fastReflection_MsgVoteChainMeta_messageType fastReflection_MsgVoteChainMeta_messageType +var _ protoreflect.MessageType = fastReflection_MsgVoteChainMeta_messageType{} -type fastReflection_MsgVoteOutbound_messageType struct{} +type fastReflection_MsgVoteChainMeta_messageType struct{} -func (x fastReflection_MsgVoteOutbound_messageType) Zero() protoreflect.Message { - return (*fastReflection_MsgVoteOutbound)(nil) +func (x fastReflection_MsgVoteChainMeta_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgVoteChainMeta)(nil) } -func (x fastReflection_MsgVoteOutbound_messageType) New() protoreflect.Message { - return new(fastReflection_MsgVoteOutbound) +func (x fastReflection_MsgVoteChainMeta_messageType) New() protoreflect.Message { + return new(fastReflection_MsgVoteChainMeta) } -func (x fastReflection_MsgVoteOutbound_messageType) Descriptor() protoreflect.MessageDescriptor { - return md_MsgVoteOutbound +func (x fastReflection_MsgVoteChainMeta_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgVoteChainMeta } // Descriptor returns message descriptor, which contains only the protobuf // type information for the message. -func (x *fastReflection_MsgVoteOutbound) Descriptor() protoreflect.MessageDescriptor { - return md_MsgVoteOutbound +func (x *fastReflection_MsgVoteChainMeta) Descriptor() protoreflect.MessageDescriptor { + return md_MsgVoteChainMeta } // Type returns the message type, which encapsulates both Go and protobuf // type information. If the Go type information is not needed, // it is recommended that the message descriptor be used instead. -func (x *fastReflection_MsgVoteOutbound) Type() protoreflect.MessageType { - return _fastReflection_MsgVoteOutbound_messageType +func (x *fastReflection_MsgVoteChainMeta) Type() protoreflect.MessageType { + return _fastReflection_MsgVoteChainMeta_messageType } // New returns a newly allocated and mutable empty message. -func (x *fastReflection_MsgVoteOutbound) New() protoreflect.Message { - return new(fastReflection_MsgVoteOutbound) +func (x *fastReflection_MsgVoteChainMeta) New() protoreflect.Message { + return new(fastReflection_MsgVoteChainMeta) } // Interface unwraps the message reflection interface and // returns the underlying ProtoMessage interface. -func (x *fastReflection_MsgVoteOutbound) Interface() protoreflect.ProtoMessage { - return (*MsgVoteOutbound)(x) +func (x *fastReflection_MsgVoteChainMeta) Interface() protoreflect.ProtoMessage { + return (*MsgVoteChainMeta)(x) } // Range iterates over every populated field in an undefined order, @@ -3803,28 +3788,28 @@ func (x *fastReflection_MsgVoteOutbound) Interface() protoreflect.ProtoMessage { // Range returns immediately if f returns false. // While iterating, mutating operations may only be performed // on the current field descriptor. -func (x *fastReflection_MsgVoteOutbound) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { +func (x *fastReflection_MsgVoteChainMeta) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { if x.Signer != "" { value := protoreflect.ValueOfString(x.Signer) - if !f(fd_MsgVoteOutbound_signer, value) { + if !f(fd_MsgVoteChainMeta_signer, value) { return } } - if x.TxId != "" { - value := protoreflect.ValueOfString(x.TxId) - if !f(fd_MsgVoteOutbound_tx_id, value) { + if x.ObservedChainId != "" { + value := protoreflect.ValueOfString(x.ObservedChainId) + if !f(fd_MsgVoteChainMeta_observed_chain_id, value) { return } } - if x.UtxId != "" { - value := protoreflect.ValueOfString(x.UtxId) - if !f(fd_MsgVoteOutbound_utx_id, value) { - return - } + if x.Price != uint64(0) { + value := protoreflect.ValueOfUint64(x.Price) + if !f(fd_MsgVoteChainMeta_price, value) { + return + } } - if x.ObservedTx != nil { - value := protoreflect.ValueOfMessage(x.ObservedTx.ProtoReflect()) - if !f(fd_MsgVoteOutbound_observed_tx, value) { + if x.ChainHeight != uint64(0) { + value := protoreflect.ValueOfUint64(x.ChainHeight) + if !f(fd_MsgVoteChainMeta_chain_height, value) { return } } @@ -3841,21 +3826,21 @@ func (x *fastReflection_MsgVoteOutbound) Range(f func(protoreflect.FieldDescript // In other cases (aside from the nullable cases above), // a proto3 scalar field is populated if it contains a non-zero value, and // a repeated field is populated if it is non-empty. -func (x *fastReflection_MsgVoteOutbound) Has(fd protoreflect.FieldDescriptor) bool { +func (x *fastReflection_MsgVoteChainMeta) Has(fd protoreflect.FieldDescriptor) bool { switch fd.FullName() { - case "uexecutor.v1.MsgVoteOutbound.signer": + case "uexecutor.v1.MsgVoteChainMeta.signer": return x.Signer != "" - case "uexecutor.v1.MsgVoteOutbound.tx_id": - return x.TxId != "" - case "uexecutor.v1.MsgVoteOutbound.utx_id": - return x.UtxId != "" - case "uexecutor.v1.MsgVoteOutbound.observed_tx": - return x.ObservedTx != nil + case "uexecutor.v1.MsgVoteChainMeta.observed_chain_id": + return x.ObservedChainId != "" + case "uexecutor.v1.MsgVoteChainMeta.price": + return x.Price != uint64(0) + case "uexecutor.v1.MsgVoteChainMeta.chain_height": + return x.ChainHeight != uint64(0) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMeta")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMeta does not contain field %s", fd.FullName())) } } @@ -3865,21 +3850,21 @@ func (x *fastReflection_MsgVoteOutbound) Has(fd protoreflect.FieldDescriptor) bo // associated with the given field number. // // Clear is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteOutbound) Clear(fd protoreflect.FieldDescriptor) { +func (x *fastReflection_MsgVoteChainMeta) Clear(fd protoreflect.FieldDescriptor) { switch fd.FullName() { - case "uexecutor.v1.MsgVoteOutbound.signer": + case "uexecutor.v1.MsgVoteChainMeta.signer": x.Signer = "" - case "uexecutor.v1.MsgVoteOutbound.tx_id": - x.TxId = "" - case "uexecutor.v1.MsgVoteOutbound.utx_id": - x.UtxId = "" - case "uexecutor.v1.MsgVoteOutbound.observed_tx": - x.ObservedTx = nil + case "uexecutor.v1.MsgVoteChainMeta.observed_chain_id": + x.ObservedChainId = "" + case "uexecutor.v1.MsgVoteChainMeta.price": + x.Price = uint64(0) + case "uexecutor.v1.MsgVoteChainMeta.chain_height": + x.ChainHeight = uint64(0) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMeta")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMeta does not contain field %s", fd.FullName())) } } @@ -3889,25 +3874,25 @@ func (x *fastReflection_MsgVoteOutbound) Clear(fd protoreflect.FieldDescriptor) // the default value of a bytes scalar is guaranteed to be a copy. // For unpopulated composite types, it returns an empty, read-only view // of the value; to obtain a mutable reference, use Mutable. -func (x *fastReflection_MsgVoteOutbound) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteChainMeta) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { switch descriptor.FullName() { - case "uexecutor.v1.MsgVoteOutbound.signer": + case "uexecutor.v1.MsgVoteChainMeta.signer": value := x.Signer return protoreflect.ValueOfString(value) - case "uexecutor.v1.MsgVoteOutbound.tx_id": - value := x.TxId - return protoreflect.ValueOfString(value) - case "uexecutor.v1.MsgVoteOutbound.utx_id": - value := x.UtxId + case "uexecutor.v1.MsgVoteChainMeta.observed_chain_id": + value := x.ObservedChainId return protoreflect.ValueOfString(value) - case "uexecutor.v1.MsgVoteOutbound.observed_tx": - value := x.ObservedTx - return protoreflect.ValueOfMessage(value.ProtoReflect()) + case "uexecutor.v1.MsgVoteChainMeta.price": + value := x.Price + return protoreflect.ValueOfUint64(value) + case "uexecutor.v1.MsgVoteChainMeta.chain_height": + value := x.ChainHeight + return protoreflect.ValueOfUint64(value) default: if descriptor.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMeta")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutbound does not contain field %s", descriptor.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMeta does not contain field %s", descriptor.FullName())) } } @@ -3921,21 +3906,21 @@ func (x *fastReflection_MsgVoteOutbound) Get(descriptor protoreflect.FieldDescri // empty, read-only value, then it panics. // // Set is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteOutbound) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { +func (x *fastReflection_MsgVoteChainMeta) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { switch fd.FullName() { - case "uexecutor.v1.MsgVoteOutbound.signer": + case "uexecutor.v1.MsgVoteChainMeta.signer": x.Signer = value.Interface().(string) - case "uexecutor.v1.MsgVoteOutbound.tx_id": - x.TxId = value.Interface().(string) - case "uexecutor.v1.MsgVoteOutbound.utx_id": - x.UtxId = value.Interface().(string) - case "uexecutor.v1.MsgVoteOutbound.observed_tx": - x.ObservedTx = value.Message().Interface().(*OutboundObservation) + case "uexecutor.v1.MsgVoteChainMeta.observed_chain_id": + x.ObservedChainId = value.Interface().(string) + case "uexecutor.v1.MsgVoteChainMeta.price": + x.Price = value.Uint() + case "uexecutor.v1.MsgVoteChainMeta.chain_height": + x.ChainHeight = value.Uint() default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMeta")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMeta does not contain field %s", fd.FullName())) } } @@ -3949,56 +3934,52 @@ func (x *fastReflection_MsgVoteOutbound) Set(fd protoreflect.FieldDescriptor, va // It panics if the field does not contain a composite type. // // Mutable is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteOutbound) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteChainMeta) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { - case "uexecutor.v1.MsgVoteOutbound.observed_tx": - if x.ObservedTx == nil { - x.ObservedTx = new(OutboundObservation) - } - return protoreflect.ValueOfMessage(x.ObservedTx.ProtoReflect()) - case "uexecutor.v1.MsgVoteOutbound.signer": - panic(fmt.Errorf("field signer of message uexecutor.v1.MsgVoteOutbound is not mutable")) - case "uexecutor.v1.MsgVoteOutbound.tx_id": - panic(fmt.Errorf("field tx_id of message uexecutor.v1.MsgVoteOutbound is not mutable")) - case "uexecutor.v1.MsgVoteOutbound.utx_id": - panic(fmt.Errorf("field utx_id of message uexecutor.v1.MsgVoteOutbound is not mutable")) + case "uexecutor.v1.MsgVoteChainMeta.signer": + panic(fmt.Errorf("field signer of message uexecutor.v1.MsgVoteChainMeta is not mutable")) + case "uexecutor.v1.MsgVoteChainMeta.observed_chain_id": + panic(fmt.Errorf("field observed_chain_id of message uexecutor.v1.MsgVoteChainMeta is not mutable")) + case "uexecutor.v1.MsgVoteChainMeta.price": + panic(fmt.Errorf("field price of message uexecutor.v1.MsgVoteChainMeta is not mutable")) + case "uexecutor.v1.MsgVoteChainMeta.chain_height": + panic(fmt.Errorf("field chain_height of message uexecutor.v1.MsgVoteChainMeta is not mutable")) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMeta")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMeta does not contain field %s", fd.FullName())) } } // NewField returns a new value that is assignable to the field // for the given descriptor. For scalars, this returns the default value. // For lists, maps, and messages, this returns a new, empty, mutable value. -func (x *fastReflection_MsgVoteOutbound) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteChainMeta) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { - case "uexecutor.v1.MsgVoteOutbound.signer": - return protoreflect.ValueOfString("") - case "uexecutor.v1.MsgVoteOutbound.tx_id": + case "uexecutor.v1.MsgVoteChainMeta.signer": return protoreflect.ValueOfString("") - case "uexecutor.v1.MsgVoteOutbound.utx_id": + case "uexecutor.v1.MsgVoteChainMeta.observed_chain_id": return protoreflect.ValueOfString("") - case "uexecutor.v1.MsgVoteOutbound.observed_tx": - m := new(OutboundObservation) - return protoreflect.ValueOfMessage(m.ProtoReflect()) + case "uexecutor.v1.MsgVoteChainMeta.price": + return protoreflect.ValueOfUint64(uint64(0)) + case "uexecutor.v1.MsgVoteChainMeta.chain_height": + return protoreflect.ValueOfUint64(uint64(0)) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMeta")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMeta does not contain field %s", fd.FullName())) } } // WhichOneof reports which field within the oneof is populated, // returning nil if none are populated. // It panics if the oneof descriptor does not belong to this message. -func (x *fastReflection_MsgVoteOutbound) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { +func (x *fastReflection_MsgVoteChainMeta) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { switch d.FullName() { default: - panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgVoteOutbound", d.FullName())) + panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgVoteChainMeta", d.FullName())) } panic("unreachable") } @@ -4006,7 +3987,7 @@ func (x *fastReflection_MsgVoteOutbound) WhichOneof(d protoreflect.OneofDescript // GetUnknown retrieves the entire list of unknown fields. // The caller may only mutate the contents of the RawFields // if the mutated bytes are stored back into the message with SetUnknown. -func (x *fastReflection_MsgVoteOutbound) GetUnknown() protoreflect.RawFields { +func (x *fastReflection_MsgVoteChainMeta) GetUnknown() protoreflect.RawFields { return x.unknownFields } @@ -4017,7 +3998,7 @@ func (x *fastReflection_MsgVoteOutbound) GetUnknown() protoreflect.RawFields { // An empty RawFields may be passed to clear the fields. // // SetUnknown is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteOutbound) SetUnknown(fields protoreflect.RawFields) { +func (x *fastReflection_MsgVoteChainMeta) SetUnknown(fields protoreflect.RawFields) { x.unknownFields = fields } @@ -4029,7 +4010,7 @@ func (x *fastReflection_MsgVoteOutbound) SetUnknown(fields protoreflect.RawField // message type, but the details are implementation dependent. // Validity is not part of the protobuf data model, and may not // be preserved in marshaling or other operations. -func (x *fastReflection_MsgVoteOutbound) IsValid() bool { +func (x *fastReflection_MsgVoteChainMeta) IsValid() bool { return x != nil } @@ -4039,9 +4020,9 @@ func (x *fastReflection_MsgVoteOutbound) IsValid() bool { // The returned methods type is identical to // "google.golang.org/protobuf/runtime/protoiface".Methods. // Consult the protoiface package documentation for details. -func (x *fastReflection_MsgVoteOutbound) ProtoMethods() *protoiface.Methods { +func (x *fastReflection_MsgVoteChainMeta) ProtoMethods() *protoiface.Methods { size := func(input protoiface.SizeInput) protoiface.SizeOutput { - x := input.Message.Interface().(*MsgVoteOutbound) + x := input.Message.Interface().(*MsgVoteChainMeta) if x == nil { return protoiface.SizeOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -4057,17 +4038,15 @@ func (x *fastReflection_MsgVoteOutbound) ProtoMethods() *protoiface.Methods { if l > 0 { n += 1 + l + runtime.Sov(uint64(l)) } - l = len(x.TxId) + l = len(x.ObservedChainId) if l > 0 { n += 1 + l + runtime.Sov(uint64(l)) } - l = len(x.UtxId) - if l > 0 { - n += 1 + l + runtime.Sov(uint64(l)) + if x.Price != 0 { + n += 1 + runtime.Sov(uint64(x.Price)) } - if x.ObservedTx != nil { - l = options.Size(x.ObservedTx) - n += 1 + l + runtime.Sov(uint64(l)) + if x.ChainHeight != 0 { + n += 1 + runtime.Sov(uint64(x.ChainHeight)) } if x.unknownFields != nil { n += len(x.unknownFields) @@ -4079,7 +4058,7 @@ func (x *fastReflection_MsgVoteOutbound) ProtoMethods() *protoiface.Methods { } marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { - x := input.Message.Interface().(*MsgVoteOutbound) + x := input.Message.Interface().(*MsgVoteChainMeta) if x == nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -4098,31 +4077,20 @@ func (x *fastReflection_MsgVoteOutbound) ProtoMethods() *protoiface.Methods { i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } - if x.ObservedTx != nil { - encoded, err := options.Marshal(x.ObservedTx) - if err != nil { - return protoiface.MarshalOutput{ - NoUnkeyedLiterals: input.NoUnkeyedLiterals, - Buf: input.Buf, - }, err - } - i -= len(encoded) - copy(dAtA[i:], encoded) - i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) + if x.ChainHeight != 0 { + i = runtime.EncodeVarint(dAtA, i, uint64(x.ChainHeight)) i-- - dAtA[i] = 0x22 + dAtA[i] = 0x20 } - if len(x.UtxId) > 0 { - i -= len(x.UtxId) - copy(dAtA[i:], x.UtxId) - i = runtime.EncodeVarint(dAtA, i, uint64(len(x.UtxId))) + if x.Price != 0 { + i = runtime.EncodeVarint(dAtA, i, uint64(x.Price)) i-- - dAtA[i] = 0x1a + dAtA[i] = 0x18 } - if len(x.TxId) > 0 { - i -= len(x.TxId) - copy(dAtA[i:], x.TxId) - i = runtime.EncodeVarint(dAtA, i, uint64(len(x.TxId))) + if len(x.ObservedChainId) > 0 { + i -= len(x.ObservedChainId) + copy(dAtA[i:], x.ObservedChainId) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.ObservedChainId))) i-- dAtA[i] = 0x12 } @@ -4144,7 +4112,7 @@ func (x *fastReflection_MsgVoteOutbound) ProtoMethods() *protoiface.Methods { }, nil } unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { - x := input.Message.Interface().(*MsgVoteOutbound) + x := input.Message.Interface().(*MsgVoteChainMeta) if x == nil { return protoiface.UnmarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -4176,10 +4144,10 @@ func (x *fastReflection_MsgVoteOutbound) ProtoMethods() *protoiface.Methods { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteOutbound: wiretype end group for non-group") + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteChainMeta: wiretype end group for non-group") } if fieldNum <= 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteOutbound: illegal tag %d (wire type %d)", fieldNum, wire) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteChainMeta: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: @@ -4216,7 +4184,7 @@ func (x *fastReflection_MsgVoteOutbound) ProtoMethods() *protoiface.Methods { iNdEx = postIndex case 2: if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field TxId", wireType) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ObservedChainId", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -4244,13 +4212,13 @@ func (x *fastReflection_MsgVoteOutbound) ProtoMethods() *protoiface.Methods { if postIndex > l { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF } - x.TxId = string(dAtA[iNdEx:postIndex]) + x.ObservedChainId = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: - if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field UtxId", wireType) + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Price", wireType) } - var stringLen uint64 + x.Price = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow @@ -4260,29 +4228,16 @@ func (x *fastReflection_MsgVoteOutbound) ProtoMethods() *protoiface.Methods { } b := dAtA[iNdEx] iNdEx++ - stringLen |= uint64(b&0x7F) << shift + x.Price |= uint64(b&0x7F) << shift if b < 0x80 { break } } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - if postIndex > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - x.UtxId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex case 4: - if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ObservedTx", wireType) + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ChainHeight", wireType) } - var msglen int + x.ChainHeight = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow @@ -4292,28 +4247,11 @@ func (x *fastReflection_MsgVoteOutbound) ProtoMethods() *protoiface.Methods { } b := dAtA[iNdEx] iNdEx++ - msglen |= int(b&0x7F) << shift + x.ChainHeight |= uint64(b&0x7F) << shift if b < 0x80 { break } } - if msglen < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - if postIndex > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - if x.ObservedTx == nil { - x.ObservedTx = &OutboundObservation{} - } - if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.ObservedTx); err != nil { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err - } - iNdEx = postIndex default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -4350,23 +4288,23 @@ func (x *fastReflection_MsgVoteOutbound) ProtoMethods() *protoiface.Methods { } var ( - md_MsgVoteOutboundResponse protoreflect.MessageDescriptor + md_MsgVoteChainMetaResponse protoreflect.MessageDescriptor ) func init() { file_uexecutor_v1_tx_proto_init() - md_MsgVoteOutboundResponse = File_uexecutor_v1_tx_proto.Messages().ByName("MsgVoteOutboundResponse") + md_MsgVoteChainMetaResponse = File_uexecutor_v1_tx_proto.Messages().ByName("MsgVoteChainMetaResponse") } -var _ protoreflect.Message = (*fastReflection_MsgVoteOutboundResponse)(nil) +var _ protoreflect.Message = (*fastReflection_MsgVoteChainMetaResponse)(nil) -type fastReflection_MsgVoteOutboundResponse MsgVoteOutboundResponse +type fastReflection_MsgVoteChainMetaResponse MsgVoteChainMetaResponse -func (x *MsgVoteOutboundResponse) ProtoReflect() protoreflect.Message { - return (*fastReflection_MsgVoteOutboundResponse)(x) +func (x *MsgVoteChainMetaResponse) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgVoteChainMetaResponse)(x) } -func (x *MsgVoteOutboundResponse) slowProtoReflect() protoreflect.Message { +func (x *MsgVoteChainMetaResponse) slowProtoReflect() protoreflect.Message { mi := &file_uexecutor_v1_tx_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -4378,43 +4316,43 @@ func (x *MsgVoteOutboundResponse) slowProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -var _fastReflection_MsgVoteOutboundResponse_messageType fastReflection_MsgVoteOutboundResponse_messageType -var _ protoreflect.MessageType = fastReflection_MsgVoteOutboundResponse_messageType{} +var _fastReflection_MsgVoteChainMetaResponse_messageType fastReflection_MsgVoteChainMetaResponse_messageType +var _ protoreflect.MessageType = fastReflection_MsgVoteChainMetaResponse_messageType{} -type fastReflection_MsgVoteOutboundResponse_messageType struct{} +type fastReflection_MsgVoteChainMetaResponse_messageType struct{} -func (x fastReflection_MsgVoteOutboundResponse_messageType) Zero() protoreflect.Message { - return (*fastReflection_MsgVoteOutboundResponse)(nil) +func (x fastReflection_MsgVoteChainMetaResponse_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgVoteChainMetaResponse)(nil) } -func (x fastReflection_MsgVoteOutboundResponse_messageType) New() protoreflect.Message { - return new(fastReflection_MsgVoteOutboundResponse) +func (x fastReflection_MsgVoteChainMetaResponse_messageType) New() protoreflect.Message { + return new(fastReflection_MsgVoteChainMetaResponse) } -func (x fastReflection_MsgVoteOutboundResponse_messageType) Descriptor() protoreflect.MessageDescriptor { - return md_MsgVoteOutboundResponse +func (x fastReflection_MsgVoteChainMetaResponse_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgVoteChainMetaResponse } // Descriptor returns message descriptor, which contains only the protobuf // type information for the message. -func (x *fastReflection_MsgVoteOutboundResponse) Descriptor() protoreflect.MessageDescriptor { - return md_MsgVoteOutboundResponse +func (x *fastReflection_MsgVoteChainMetaResponse) Descriptor() protoreflect.MessageDescriptor { + return md_MsgVoteChainMetaResponse } // Type returns the message type, which encapsulates both Go and protobuf // type information. If the Go type information is not needed, // it is recommended that the message descriptor be used instead. -func (x *fastReflection_MsgVoteOutboundResponse) Type() protoreflect.MessageType { - return _fastReflection_MsgVoteOutboundResponse_messageType +func (x *fastReflection_MsgVoteChainMetaResponse) Type() protoreflect.MessageType { + return _fastReflection_MsgVoteChainMetaResponse_messageType } // New returns a newly allocated and mutable empty message. -func (x *fastReflection_MsgVoteOutboundResponse) New() protoreflect.Message { - return new(fastReflection_MsgVoteOutboundResponse) +func (x *fastReflection_MsgVoteChainMetaResponse) New() protoreflect.Message { + return new(fastReflection_MsgVoteChainMetaResponse) } // Interface unwraps the message reflection interface and // returns the underlying ProtoMessage interface. -func (x *fastReflection_MsgVoteOutboundResponse) Interface() protoreflect.ProtoMessage { - return (*MsgVoteOutboundResponse)(x) +func (x *fastReflection_MsgVoteChainMetaResponse) Interface() protoreflect.ProtoMessage { + return (*MsgVoteChainMetaResponse)(x) } // Range iterates over every populated field in an undefined order, @@ -4422,7 +4360,7 @@ func (x *fastReflection_MsgVoteOutboundResponse) Interface() protoreflect.ProtoM // Range returns immediately if f returns false. // While iterating, mutating operations may only be performed // on the current field descriptor. -func (x *fastReflection_MsgVoteOutboundResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { +func (x *fastReflection_MsgVoteChainMetaResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { } // Has reports whether a field is populated. @@ -4436,13 +4374,13 @@ func (x *fastReflection_MsgVoteOutboundResponse) Range(f func(protoreflect.Field // In other cases (aside from the nullable cases above), // a proto3 scalar field is populated if it contains a non-zero value, and // a repeated field is populated if it is non-empty. -func (x *fastReflection_MsgVoteOutboundResponse) Has(fd protoreflect.FieldDescriptor) bool { +func (x *fastReflection_MsgVoteChainMetaResponse) Has(fd protoreflect.FieldDescriptor) bool { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMetaResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMetaResponse does not contain field %s", fd.FullName())) } } @@ -4452,13 +4390,13 @@ func (x *fastReflection_MsgVoteOutboundResponse) Has(fd protoreflect.FieldDescri // associated with the given field number. // // Clear is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteOutboundResponse) Clear(fd protoreflect.FieldDescriptor) { +func (x *fastReflection_MsgVoteChainMetaResponse) Clear(fd protoreflect.FieldDescriptor) { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMetaResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMetaResponse does not contain field %s", fd.FullName())) } } @@ -4468,13 +4406,13 @@ func (x *fastReflection_MsgVoteOutboundResponse) Clear(fd protoreflect.FieldDesc // the default value of a bytes scalar is guaranteed to be a copy. // For unpopulated composite types, it returns an empty, read-only view // of the value; to obtain a mutable reference, use Mutable. -func (x *fastReflection_MsgVoteOutboundResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteChainMetaResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { switch descriptor.FullName() { default: if descriptor.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMetaResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutboundResponse does not contain field %s", descriptor.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMetaResponse does not contain field %s", descriptor.FullName())) } } @@ -4488,13 +4426,13 @@ func (x *fastReflection_MsgVoteOutboundResponse) Get(descriptor protoreflect.Fie // empty, read-only value, then it panics. // // Set is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteOutboundResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { +func (x *fastReflection_MsgVoteChainMetaResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMetaResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMetaResponse does not contain field %s", fd.FullName())) } } @@ -4508,36 +4446,36 @@ func (x *fastReflection_MsgVoteOutboundResponse) Set(fd protoreflect.FieldDescri // It panics if the field does not contain a composite type. // // Mutable is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteOutboundResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteChainMetaResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMetaResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMetaResponse does not contain field %s", fd.FullName())) } } // NewField returns a new value that is assignable to the field // for the given descriptor. For scalars, this returns the default value. // For lists, maps, and messages, this returns a new, empty, mutable value. -func (x *fastReflection_MsgVoteOutboundResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgVoteChainMetaResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteOutboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMetaResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteOutboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMetaResponse does not contain field %s", fd.FullName())) } } // WhichOneof reports which field within the oneof is populated, // returning nil if none are populated. // It panics if the oneof descriptor does not belong to this message. -func (x *fastReflection_MsgVoteOutboundResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { +func (x *fastReflection_MsgVoteChainMetaResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { switch d.FullName() { default: - panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgVoteOutboundResponse", d.FullName())) + panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgVoteChainMetaResponse", d.FullName())) } panic("unreachable") } @@ -4545,7 +4483,7 @@ func (x *fastReflection_MsgVoteOutboundResponse) WhichOneof(d protoreflect.Oneof // GetUnknown retrieves the entire list of unknown fields. // The caller may only mutate the contents of the RawFields // if the mutated bytes are stored back into the message with SetUnknown. -func (x *fastReflection_MsgVoteOutboundResponse) GetUnknown() protoreflect.RawFields { +func (x *fastReflection_MsgVoteChainMetaResponse) GetUnknown() protoreflect.RawFields { return x.unknownFields } @@ -4556,7 +4494,7 @@ func (x *fastReflection_MsgVoteOutboundResponse) GetUnknown() protoreflect.RawFi // An empty RawFields may be passed to clear the fields. // // SetUnknown is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteOutboundResponse) SetUnknown(fields protoreflect.RawFields) { +func (x *fastReflection_MsgVoteChainMetaResponse) SetUnknown(fields protoreflect.RawFields) { x.unknownFields = fields } @@ -4568,7 +4506,7 @@ func (x *fastReflection_MsgVoteOutboundResponse) SetUnknown(fields protoreflect. // message type, but the details are implementation dependent. // Validity is not part of the protobuf data model, and may not // be preserved in marshaling or other operations. -func (x *fastReflection_MsgVoteOutboundResponse) IsValid() bool { +func (x *fastReflection_MsgVoteChainMetaResponse) IsValid() bool { return x != nil } @@ -4578,9 +4516,9 @@ func (x *fastReflection_MsgVoteOutboundResponse) IsValid() bool { // The returned methods type is identical to // "google.golang.org/protobuf/runtime/protoiface".Methods. // Consult the protoiface package documentation for details. -func (x *fastReflection_MsgVoteOutboundResponse) ProtoMethods() *protoiface.Methods { +func (x *fastReflection_MsgVoteChainMetaResponse) ProtoMethods() *protoiface.Methods { size := func(input protoiface.SizeInput) protoiface.SizeOutput { - x := input.Message.Interface().(*MsgVoteOutboundResponse) + x := input.Message.Interface().(*MsgVoteChainMetaResponse) if x == nil { return protoiface.SizeOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -4602,7 +4540,7 @@ func (x *fastReflection_MsgVoteOutboundResponse) ProtoMethods() *protoiface.Meth } marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { - x := input.Message.Interface().(*MsgVoteOutboundResponse) + x := input.Message.Interface().(*MsgVoteChainMetaResponse) if x == nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -4632,7 +4570,7 @@ func (x *fastReflection_MsgVoteOutboundResponse) ProtoMethods() *protoiface.Meth }, nil } unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { - x := input.Message.Interface().(*MsgVoteOutboundResponse) + x := input.Message.Interface().(*MsgVoteChainMetaResponse) if x == nil { return protoiface.UnmarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -4664,10 +4602,10 @@ func (x *fastReflection_MsgVoteOutboundResponse) ProtoMethods() *protoiface.Meth fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteOutboundResponse: wiretype end group for non-group") + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteChainMetaResponse: wiretype end group for non-group") } if fieldNum <= 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteOutboundResponse: illegal tag %d (wire type %d)", fieldNum, wire) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteChainMetaResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { default: @@ -4706,31 +4644,27 @@ func (x *fastReflection_MsgVoteOutboundResponse) ProtoMethods() *protoiface.Meth } var ( - md_MsgVoteChainMeta protoreflect.MessageDescriptor - fd_MsgVoteChainMeta_signer protoreflect.FieldDescriptor - fd_MsgVoteChainMeta_observed_chain_id protoreflect.FieldDescriptor - fd_MsgVoteChainMeta_price protoreflect.FieldDescriptor - fd_MsgVoteChainMeta_chain_height protoreflect.FieldDescriptor + md_MsgRevertStuckInbound protoreflect.MessageDescriptor + fd_MsgRevertStuckInbound_signer protoreflect.FieldDescriptor + fd_MsgRevertStuckInbound_inbound protoreflect.FieldDescriptor ) func init() { file_uexecutor_v1_tx_proto_init() - md_MsgVoteChainMeta = File_uexecutor_v1_tx_proto.Messages().ByName("MsgVoteChainMeta") - fd_MsgVoteChainMeta_signer = md_MsgVoteChainMeta.Fields().ByName("signer") - fd_MsgVoteChainMeta_observed_chain_id = md_MsgVoteChainMeta.Fields().ByName("observed_chain_id") - fd_MsgVoteChainMeta_price = md_MsgVoteChainMeta.Fields().ByName("price") - fd_MsgVoteChainMeta_chain_height = md_MsgVoteChainMeta.Fields().ByName("chain_height") + md_MsgRevertStuckInbound = File_uexecutor_v1_tx_proto.Messages().ByName("MsgRevertStuckInbound") + fd_MsgRevertStuckInbound_signer = md_MsgRevertStuckInbound.Fields().ByName("signer") + fd_MsgRevertStuckInbound_inbound = md_MsgRevertStuckInbound.Fields().ByName("inbound") } -var _ protoreflect.Message = (*fastReflection_MsgVoteChainMeta)(nil) +var _ protoreflect.Message = (*fastReflection_MsgRevertStuckInbound)(nil) -type fastReflection_MsgVoteChainMeta MsgVoteChainMeta +type fastReflection_MsgRevertStuckInbound MsgRevertStuckInbound -func (x *MsgVoteChainMeta) ProtoReflect() protoreflect.Message { - return (*fastReflection_MsgVoteChainMeta)(x) +func (x *MsgRevertStuckInbound) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgRevertStuckInbound)(x) } -func (x *MsgVoteChainMeta) slowProtoReflect() protoreflect.Message { +func (x *MsgRevertStuckInbound) slowProtoReflect() protoreflect.Message { mi := &file_uexecutor_v1_tx_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -4742,43 +4676,43 @@ func (x *MsgVoteChainMeta) slowProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -var _fastReflection_MsgVoteChainMeta_messageType fastReflection_MsgVoteChainMeta_messageType -var _ protoreflect.MessageType = fastReflection_MsgVoteChainMeta_messageType{} +var _fastReflection_MsgRevertStuckInbound_messageType fastReflection_MsgRevertStuckInbound_messageType +var _ protoreflect.MessageType = fastReflection_MsgRevertStuckInbound_messageType{} -type fastReflection_MsgVoteChainMeta_messageType struct{} +type fastReflection_MsgRevertStuckInbound_messageType struct{} -func (x fastReflection_MsgVoteChainMeta_messageType) Zero() protoreflect.Message { - return (*fastReflection_MsgVoteChainMeta)(nil) +func (x fastReflection_MsgRevertStuckInbound_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgRevertStuckInbound)(nil) } -func (x fastReflection_MsgVoteChainMeta_messageType) New() protoreflect.Message { - return new(fastReflection_MsgVoteChainMeta) +func (x fastReflection_MsgRevertStuckInbound_messageType) New() protoreflect.Message { + return new(fastReflection_MsgRevertStuckInbound) } -func (x fastReflection_MsgVoteChainMeta_messageType) Descriptor() protoreflect.MessageDescriptor { - return md_MsgVoteChainMeta +func (x fastReflection_MsgRevertStuckInbound_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgRevertStuckInbound } // Descriptor returns message descriptor, which contains only the protobuf // type information for the message. -func (x *fastReflection_MsgVoteChainMeta) Descriptor() protoreflect.MessageDescriptor { - return md_MsgVoteChainMeta +func (x *fastReflection_MsgRevertStuckInbound) Descriptor() protoreflect.MessageDescriptor { + return md_MsgRevertStuckInbound } // Type returns the message type, which encapsulates both Go and protobuf // type information. If the Go type information is not needed, // it is recommended that the message descriptor be used instead. -func (x *fastReflection_MsgVoteChainMeta) Type() protoreflect.MessageType { - return _fastReflection_MsgVoteChainMeta_messageType +func (x *fastReflection_MsgRevertStuckInbound) Type() protoreflect.MessageType { + return _fastReflection_MsgRevertStuckInbound_messageType } // New returns a newly allocated and mutable empty message. -func (x *fastReflection_MsgVoteChainMeta) New() protoreflect.Message { - return new(fastReflection_MsgVoteChainMeta) +func (x *fastReflection_MsgRevertStuckInbound) New() protoreflect.Message { + return new(fastReflection_MsgRevertStuckInbound) } // Interface unwraps the message reflection interface and // returns the underlying ProtoMessage interface. -func (x *fastReflection_MsgVoteChainMeta) Interface() protoreflect.ProtoMessage { - return (*MsgVoteChainMeta)(x) +func (x *fastReflection_MsgRevertStuckInbound) Interface() protoreflect.ProtoMessage { + return (*MsgRevertStuckInbound)(x) } // Range iterates over every populated field in an undefined order, @@ -4786,28 +4720,999 @@ func (x *fastReflection_MsgVoteChainMeta) Interface() protoreflect.ProtoMessage // Range returns immediately if f returns false. // While iterating, mutating operations may only be performed // on the current field descriptor. -func (x *fastReflection_MsgVoteChainMeta) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { +func (x *fastReflection_MsgRevertStuckInbound) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { if x.Signer != "" { value := protoreflect.ValueOfString(x.Signer) - if !f(fd_MsgVoteChainMeta_signer, value) { + if !f(fd_MsgRevertStuckInbound_signer, value) { return } } - if x.ObservedChainId != "" { - value := protoreflect.ValueOfString(x.ObservedChainId) - if !f(fd_MsgVoteChainMeta_observed_chain_id, value) { + if x.Inbound != nil { + value := protoreflect.ValueOfMessage(x.Inbound.ProtoReflect()) + if !f(fd_MsgRevertStuckInbound_inbound, value) { return } } - if x.Price != uint64(0) { - value := protoreflect.ValueOfUint64(x.Price) - if !f(fd_MsgVoteChainMeta_price, value) { +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_MsgRevertStuckInbound) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "uexecutor.v1.MsgRevertStuckInbound.signer": + return x.Signer != "" + case "uexecutor.v1.MsgRevertStuckInbound.inbound": + return x.Inbound != nil + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInbound")) + } + panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInbound does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MsgRevertStuckInbound) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "uexecutor.v1.MsgRevertStuckInbound.signer": + x.Signer = "" + case "uexecutor.v1.MsgRevertStuckInbound.inbound": + x.Inbound = nil + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInbound")) + } + panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInbound does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_MsgRevertStuckInbound) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "uexecutor.v1.MsgRevertStuckInbound.signer": + value := x.Signer + return protoreflect.ValueOfString(value) + case "uexecutor.v1.MsgRevertStuckInbound.inbound": + value := x.Inbound + return protoreflect.ValueOfMessage(value.ProtoReflect()) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInbound")) + } + panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInbound does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MsgRevertStuckInbound) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "uexecutor.v1.MsgRevertStuckInbound.signer": + x.Signer = value.Interface().(string) + case "uexecutor.v1.MsgRevertStuckInbound.inbound": + x.Inbound = value.Message().Interface().(*Inbound) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInbound")) + } + panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInbound does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MsgRevertStuckInbound) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "uexecutor.v1.MsgRevertStuckInbound.inbound": + if x.Inbound == nil { + x.Inbound = new(Inbound) + } + return protoreflect.ValueOfMessage(x.Inbound.ProtoReflect()) + case "uexecutor.v1.MsgRevertStuckInbound.signer": + panic(fmt.Errorf("field signer of message uexecutor.v1.MsgRevertStuckInbound is not mutable")) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInbound")) + } + panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInbound does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_MsgRevertStuckInbound) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "uexecutor.v1.MsgRevertStuckInbound.signer": + return protoreflect.ValueOfString("") + case "uexecutor.v1.MsgRevertStuckInbound.inbound": + m := new(Inbound) + return protoreflect.ValueOfMessage(m.ProtoReflect()) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInbound")) + } + panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInbound does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_MsgRevertStuckInbound) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgRevertStuckInbound", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_MsgRevertStuckInbound) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MsgRevertStuckInbound) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_MsgRevertStuckInbound) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_MsgRevertStuckInbound) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*MsgRevertStuckInbound) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + l = len(x.Signer) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + if x.Inbound != nil { + l = options.Size(x.Inbound) + n += 1 + l + runtime.Sov(uint64(l)) + } + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*MsgRevertStuckInbound) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if x.Inbound != nil { + encoded, err := options.Marshal(x.Inbound) + if err != nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) + i-- + dAtA[i] = 0x12 + } + if len(x.Signer) > 0 { + i -= len(x.Signer) + copy(dAtA[i:], x.Signer) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.Signer))) + i-- + dAtA[i] = 0xa + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*MsgRevertStuckInbound) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + 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++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgRevertStuckInbound: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgRevertStuckInbound: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Signer", wireType) + } + var stringLen uint64 + 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++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.Signer = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Inbound", wireType) + } + var msglen 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++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if x.Inbound == nil { + x.Inbound = &Inbound{} + } + if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.Inbound); err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +var ( + md_MsgRevertStuckInboundResponse protoreflect.MessageDescriptor + fd_MsgRevertStuckInboundResponse_utx_id protoreflect.FieldDescriptor + fd_MsgRevertStuckInboundResponse_outbound_id protoreflect.FieldDescriptor +) + +func init() { + file_uexecutor_v1_tx_proto_init() + md_MsgRevertStuckInboundResponse = File_uexecutor_v1_tx_proto.Messages().ByName("MsgRevertStuckInboundResponse") + fd_MsgRevertStuckInboundResponse_utx_id = md_MsgRevertStuckInboundResponse.Fields().ByName("utx_id") + fd_MsgRevertStuckInboundResponse_outbound_id = md_MsgRevertStuckInboundResponse.Fields().ByName("outbound_id") +} + +var _ protoreflect.Message = (*fastReflection_MsgRevertStuckInboundResponse)(nil) + +type fastReflection_MsgRevertStuckInboundResponse MsgRevertStuckInboundResponse + +func (x *MsgRevertStuckInboundResponse) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgRevertStuckInboundResponse)(x) +} + +func (x *MsgRevertStuckInboundResponse) slowProtoReflect() protoreflect.Message { + mi := &file_uexecutor_v1_tx_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_MsgRevertStuckInboundResponse_messageType fastReflection_MsgRevertStuckInboundResponse_messageType +var _ protoreflect.MessageType = fastReflection_MsgRevertStuckInboundResponse_messageType{} + +type fastReflection_MsgRevertStuckInboundResponse_messageType struct{} + +func (x fastReflection_MsgRevertStuckInboundResponse_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgRevertStuckInboundResponse)(nil) +} +func (x fastReflection_MsgRevertStuckInboundResponse_messageType) New() protoreflect.Message { + return new(fastReflection_MsgRevertStuckInboundResponse) +} +func (x fastReflection_MsgRevertStuckInboundResponse_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgRevertStuckInboundResponse +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_MsgRevertStuckInboundResponse) Descriptor() protoreflect.MessageDescriptor { + return md_MsgRevertStuckInboundResponse +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_MsgRevertStuckInboundResponse) Type() protoreflect.MessageType { + return _fastReflection_MsgRevertStuckInboundResponse_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_MsgRevertStuckInboundResponse) New() protoreflect.Message { + return new(fastReflection_MsgRevertStuckInboundResponse) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_MsgRevertStuckInboundResponse) Interface() protoreflect.ProtoMessage { + return (*MsgRevertStuckInboundResponse)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_MsgRevertStuckInboundResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if x.UtxId != "" { + value := protoreflect.ValueOfString(x.UtxId) + if !f(fd_MsgRevertStuckInboundResponse_utx_id, value) { + return + } + } + if x.OutboundId != "" { + value := protoreflect.ValueOfString(x.OutboundId) + if !f(fd_MsgRevertStuckInboundResponse_outbound_id, value) { + return + } + } +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_MsgRevertStuckInboundResponse) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "uexecutor.v1.MsgRevertStuckInboundResponse.utx_id": + return x.UtxId != "" + case "uexecutor.v1.MsgRevertStuckInboundResponse.outbound_id": + return x.OutboundId != "" + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInboundResponse")) + } + panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInboundResponse does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MsgRevertStuckInboundResponse) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "uexecutor.v1.MsgRevertStuckInboundResponse.utx_id": + x.UtxId = "" + case "uexecutor.v1.MsgRevertStuckInboundResponse.outbound_id": + x.OutboundId = "" + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInboundResponse")) + } + panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInboundResponse does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_MsgRevertStuckInboundResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "uexecutor.v1.MsgRevertStuckInboundResponse.utx_id": + value := x.UtxId + return protoreflect.ValueOfString(value) + case "uexecutor.v1.MsgRevertStuckInboundResponse.outbound_id": + value := x.OutboundId + return protoreflect.ValueOfString(value) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInboundResponse")) + } + panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInboundResponse does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MsgRevertStuckInboundResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "uexecutor.v1.MsgRevertStuckInboundResponse.utx_id": + x.UtxId = value.Interface().(string) + case "uexecutor.v1.MsgRevertStuckInboundResponse.outbound_id": + x.OutboundId = value.Interface().(string) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInboundResponse")) + } + panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInboundResponse does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MsgRevertStuckInboundResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "uexecutor.v1.MsgRevertStuckInboundResponse.utx_id": + panic(fmt.Errorf("field utx_id of message uexecutor.v1.MsgRevertStuckInboundResponse is not mutable")) + case "uexecutor.v1.MsgRevertStuckInboundResponse.outbound_id": + panic(fmt.Errorf("field outbound_id of message uexecutor.v1.MsgRevertStuckInboundResponse is not mutable")) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInboundResponse")) + } + panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInboundResponse does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_MsgRevertStuckInboundResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "uexecutor.v1.MsgRevertStuckInboundResponse.utx_id": + return protoreflect.ValueOfString("") + case "uexecutor.v1.MsgRevertStuckInboundResponse.outbound_id": + return protoreflect.ValueOfString("") + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInboundResponse")) + } + panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInboundResponse does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_MsgRevertStuckInboundResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgRevertStuckInboundResponse", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_MsgRevertStuckInboundResponse) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MsgRevertStuckInboundResponse) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_MsgRevertStuckInboundResponse) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_MsgRevertStuckInboundResponse) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*MsgRevertStuckInboundResponse) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + l = len(x.UtxId) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + l = len(x.OutboundId) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*MsgRevertStuckInboundResponse) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if len(x.OutboundId) > 0 { + i -= len(x.OutboundId) + copy(dAtA[i:], x.OutboundId) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.OutboundId))) + i-- + dAtA[i] = 0x12 + } + if len(x.UtxId) > 0 { + i -= len(x.UtxId) + copy(dAtA[i:], x.UtxId) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.UtxId))) + i-- + dAtA[i] = 0xa + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*MsgRevertStuckInboundResponse) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + 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++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgRevertStuckInboundResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgRevertStuckInboundResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field UtxId", wireType) + } + var stringLen uint64 + 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++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.UtxId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field OutboundId", wireType) + } + var stringLen uint64 + 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++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.OutboundId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +var ( + md_MsgExecuteStuckInbound protoreflect.MessageDescriptor + fd_MsgExecuteStuckInbound_signer protoreflect.FieldDescriptor + fd_MsgExecuteStuckInbound_inbound protoreflect.FieldDescriptor +) + +func init() { + file_uexecutor_v1_tx_proto_init() + md_MsgExecuteStuckInbound = File_uexecutor_v1_tx_proto.Messages().ByName("MsgExecuteStuckInbound") + fd_MsgExecuteStuckInbound_signer = md_MsgExecuteStuckInbound.Fields().ByName("signer") + fd_MsgExecuteStuckInbound_inbound = md_MsgExecuteStuckInbound.Fields().ByName("inbound") +} + +var _ protoreflect.Message = (*fastReflection_MsgExecuteStuckInbound)(nil) + +type fastReflection_MsgExecuteStuckInbound MsgExecuteStuckInbound + +func (x *MsgExecuteStuckInbound) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgExecuteStuckInbound)(x) +} + +func (x *MsgExecuteStuckInbound) slowProtoReflect() protoreflect.Message { + mi := &file_uexecutor_v1_tx_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_MsgExecuteStuckInbound_messageType fastReflection_MsgExecuteStuckInbound_messageType +var _ protoreflect.MessageType = fastReflection_MsgExecuteStuckInbound_messageType{} + +type fastReflection_MsgExecuteStuckInbound_messageType struct{} + +func (x fastReflection_MsgExecuteStuckInbound_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgExecuteStuckInbound)(nil) +} +func (x fastReflection_MsgExecuteStuckInbound_messageType) New() protoreflect.Message { + return new(fastReflection_MsgExecuteStuckInbound) +} +func (x fastReflection_MsgExecuteStuckInbound_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgExecuteStuckInbound +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_MsgExecuteStuckInbound) Descriptor() protoreflect.MessageDescriptor { + return md_MsgExecuteStuckInbound +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_MsgExecuteStuckInbound) Type() protoreflect.MessageType { + return _fastReflection_MsgExecuteStuckInbound_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_MsgExecuteStuckInbound) New() protoreflect.Message { + return new(fastReflection_MsgExecuteStuckInbound) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_MsgExecuteStuckInbound) Interface() protoreflect.ProtoMessage { + return (*MsgExecuteStuckInbound)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_MsgExecuteStuckInbound) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if x.Signer != "" { + value := protoreflect.ValueOfString(x.Signer) + if !f(fd_MsgExecuteStuckInbound_signer, value) { return } } - if x.ChainHeight != uint64(0) { - value := protoreflect.ValueOfUint64(x.ChainHeight) - if !f(fd_MsgVoteChainMeta_chain_height, value) { + if x.Inbound != nil { + value := protoreflect.ValueOfMessage(x.Inbound.ProtoReflect()) + if !f(fd_MsgExecuteStuckInbound_inbound, value) { return } } @@ -4824,21 +5729,17 @@ func (x *fastReflection_MsgVoteChainMeta) Range(f func(protoreflect.FieldDescrip // In other cases (aside from the nullable cases above), // a proto3 scalar field is populated if it contains a non-zero value, and // a repeated field is populated if it is non-empty. -func (x *fastReflection_MsgVoteChainMeta) Has(fd protoreflect.FieldDescriptor) bool { +func (x *fastReflection_MsgExecuteStuckInbound) Has(fd protoreflect.FieldDescriptor) bool { switch fd.FullName() { - case "uexecutor.v1.MsgVoteChainMeta.signer": + case "uexecutor.v1.MsgExecuteStuckInbound.signer": return x.Signer != "" - case "uexecutor.v1.MsgVoteChainMeta.observed_chain_id": - return x.ObservedChainId != "" - case "uexecutor.v1.MsgVoteChainMeta.price": - return x.Price != uint64(0) - case "uexecutor.v1.MsgVoteChainMeta.chain_height": - return x.ChainHeight != uint64(0) + case "uexecutor.v1.MsgExecuteStuckInbound.inbound": + return x.Inbound != nil default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMeta")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckInbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMeta does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckInbound does not contain field %s", fd.FullName())) } } @@ -4848,21 +5749,17 @@ func (x *fastReflection_MsgVoteChainMeta) Has(fd protoreflect.FieldDescriptor) b // associated with the given field number. // // Clear is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteChainMeta) Clear(fd protoreflect.FieldDescriptor) { +func (x *fastReflection_MsgExecuteStuckInbound) Clear(fd protoreflect.FieldDescriptor) { switch fd.FullName() { - case "uexecutor.v1.MsgVoteChainMeta.signer": + case "uexecutor.v1.MsgExecuteStuckInbound.signer": x.Signer = "" - case "uexecutor.v1.MsgVoteChainMeta.observed_chain_id": - x.ObservedChainId = "" - case "uexecutor.v1.MsgVoteChainMeta.price": - x.Price = uint64(0) - case "uexecutor.v1.MsgVoteChainMeta.chain_height": - x.ChainHeight = uint64(0) + case "uexecutor.v1.MsgExecuteStuckInbound.inbound": + x.Inbound = nil default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMeta")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckInbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMeta does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckInbound does not contain field %s", fd.FullName())) } } @@ -4872,25 +5769,19 @@ func (x *fastReflection_MsgVoteChainMeta) Clear(fd protoreflect.FieldDescriptor) // the default value of a bytes scalar is guaranteed to be a copy. // For unpopulated composite types, it returns an empty, read-only view // of the value; to obtain a mutable reference, use Mutable. -func (x *fastReflection_MsgVoteChainMeta) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgExecuteStuckInbound) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { switch descriptor.FullName() { - case "uexecutor.v1.MsgVoteChainMeta.signer": + case "uexecutor.v1.MsgExecuteStuckInbound.signer": value := x.Signer return protoreflect.ValueOfString(value) - case "uexecutor.v1.MsgVoteChainMeta.observed_chain_id": - value := x.ObservedChainId - return protoreflect.ValueOfString(value) - case "uexecutor.v1.MsgVoteChainMeta.price": - value := x.Price - return protoreflect.ValueOfUint64(value) - case "uexecutor.v1.MsgVoteChainMeta.chain_height": - value := x.ChainHeight - return protoreflect.ValueOfUint64(value) + case "uexecutor.v1.MsgExecuteStuckInbound.inbound": + value := x.Inbound + return protoreflect.ValueOfMessage(value.ProtoReflect()) default: if descriptor.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMeta")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckInbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMeta does not contain field %s", descriptor.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckInbound does not contain field %s", descriptor.FullName())) } } @@ -4904,21 +5795,17 @@ func (x *fastReflection_MsgVoteChainMeta) Get(descriptor protoreflect.FieldDescr // empty, read-only value, then it panics. // // Set is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteChainMeta) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { +func (x *fastReflection_MsgExecuteStuckInbound) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { switch fd.FullName() { - case "uexecutor.v1.MsgVoteChainMeta.signer": + case "uexecutor.v1.MsgExecuteStuckInbound.signer": x.Signer = value.Interface().(string) - case "uexecutor.v1.MsgVoteChainMeta.observed_chain_id": - x.ObservedChainId = value.Interface().(string) - case "uexecutor.v1.MsgVoteChainMeta.price": - x.Price = value.Uint() - case "uexecutor.v1.MsgVoteChainMeta.chain_height": - x.ChainHeight = value.Uint() + case "uexecutor.v1.MsgExecuteStuckInbound.inbound": + x.Inbound = value.Message().Interface().(*Inbound) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMeta")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckInbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMeta does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckInbound does not contain field %s", fd.FullName())) } } @@ -4932,52 +5819,48 @@ func (x *fastReflection_MsgVoteChainMeta) Set(fd protoreflect.FieldDescriptor, v // It panics if the field does not contain a composite type. // // Mutable is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteChainMeta) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgExecuteStuckInbound) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { - case "uexecutor.v1.MsgVoteChainMeta.signer": - panic(fmt.Errorf("field signer of message uexecutor.v1.MsgVoteChainMeta is not mutable")) - case "uexecutor.v1.MsgVoteChainMeta.observed_chain_id": - panic(fmt.Errorf("field observed_chain_id of message uexecutor.v1.MsgVoteChainMeta is not mutable")) - case "uexecutor.v1.MsgVoteChainMeta.price": - panic(fmt.Errorf("field price of message uexecutor.v1.MsgVoteChainMeta is not mutable")) - case "uexecutor.v1.MsgVoteChainMeta.chain_height": - panic(fmt.Errorf("field chain_height of message uexecutor.v1.MsgVoteChainMeta is not mutable")) + case "uexecutor.v1.MsgExecuteStuckInbound.inbound": + if x.Inbound == nil { + x.Inbound = new(Inbound) + } + return protoreflect.ValueOfMessage(x.Inbound.ProtoReflect()) + case "uexecutor.v1.MsgExecuteStuckInbound.signer": + panic(fmt.Errorf("field signer of message uexecutor.v1.MsgExecuteStuckInbound is not mutable")) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMeta")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckInbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMeta does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckInbound does not contain field %s", fd.FullName())) } } // NewField returns a new value that is assignable to the field // for the given descriptor. For scalars, this returns the default value. // For lists, maps, and messages, this returns a new, empty, mutable value. -func (x *fastReflection_MsgVoteChainMeta) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgExecuteStuckInbound) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { - case "uexecutor.v1.MsgVoteChainMeta.signer": - return protoreflect.ValueOfString("") - case "uexecutor.v1.MsgVoteChainMeta.observed_chain_id": + case "uexecutor.v1.MsgExecuteStuckInbound.signer": return protoreflect.ValueOfString("") - case "uexecutor.v1.MsgVoteChainMeta.price": - return protoreflect.ValueOfUint64(uint64(0)) - case "uexecutor.v1.MsgVoteChainMeta.chain_height": - return protoreflect.ValueOfUint64(uint64(0)) + case "uexecutor.v1.MsgExecuteStuckInbound.inbound": + m := new(Inbound) + return protoreflect.ValueOfMessage(m.ProtoReflect()) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMeta")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckInbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMeta does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckInbound does not contain field %s", fd.FullName())) } } // WhichOneof reports which field within the oneof is populated, // returning nil if none are populated. // It panics if the oneof descriptor does not belong to this message. -func (x *fastReflection_MsgVoteChainMeta) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { +func (x *fastReflection_MsgExecuteStuckInbound) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { switch d.FullName() { default: - panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgVoteChainMeta", d.FullName())) + panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgExecuteStuckInbound", d.FullName())) } panic("unreachable") } @@ -4985,7 +5868,7 @@ func (x *fastReflection_MsgVoteChainMeta) WhichOneof(d protoreflect.OneofDescrip // GetUnknown retrieves the entire list of unknown fields. // The caller may only mutate the contents of the RawFields // if the mutated bytes are stored back into the message with SetUnknown. -func (x *fastReflection_MsgVoteChainMeta) GetUnknown() protoreflect.RawFields { +func (x *fastReflection_MsgExecuteStuckInbound) GetUnknown() protoreflect.RawFields { return x.unknownFields } @@ -4996,7 +5879,7 @@ func (x *fastReflection_MsgVoteChainMeta) GetUnknown() protoreflect.RawFields { // An empty RawFields may be passed to clear the fields. // // SetUnknown is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteChainMeta) SetUnknown(fields protoreflect.RawFields) { +func (x *fastReflection_MsgExecuteStuckInbound) SetUnknown(fields protoreflect.RawFields) { x.unknownFields = fields } @@ -5008,7 +5891,7 @@ func (x *fastReflection_MsgVoteChainMeta) SetUnknown(fields protoreflect.RawFiel // message type, but the details are implementation dependent. // Validity is not part of the protobuf data model, and may not // be preserved in marshaling or other operations. -func (x *fastReflection_MsgVoteChainMeta) IsValid() bool { +func (x *fastReflection_MsgExecuteStuckInbound) IsValid() bool { return x != nil } @@ -5018,9 +5901,9 @@ func (x *fastReflection_MsgVoteChainMeta) IsValid() bool { // The returned methods type is identical to // "google.golang.org/protobuf/runtime/protoiface".Methods. // Consult the protoiface package documentation for details. -func (x *fastReflection_MsgVoteChainMeta) ProtoMethods() *protoiface.Methods { +func (x *fastReflection_MsgExecuteStuckInbound) ProtoMethods() *protoiface.Methods { size := func(input protoiface.SizeInput) protoiface.SizeOutput { - x := input.Message.Interface().(*MsgVoteChainMeta) + x := input.Message.Interface().(*MsgExecuteStuckInbound) if x == nil { return protoiface.SizeOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -5036,16 +5919,10 @@ func (x *fastReflection_MsgVoteChainMeta) ProtoMethods() *protoiface.Methods { if l > 0 { n += 1 + l + runtime.Sov(uint64(l)) } - l = len(x.ObservedChainId) - if l > 0 { + if x.Inbound != nil { + l = options.Size(x.Inbound) n += 1 + l + runtime.Sov(uint64(l)) } - if x.Price != 0 { - n += 1 + runtime.Sov(uint64(x.Price)) - } - if x.ChainHeight != 0 { - n += 1 + runtime.Sov(uint64(x.ChainHeight)) - } if x.unknownFields != nil { n += len(x.unknownFields) } @@ -5056,7 +5933,7 @@ func (x *fastReflection_MsgVoteChainMeta) ProtoMethods() *protoiface.Methods { } marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { - x := input.Message.Interface().(*MsgVoteChainMeta) + x := input.Message.Interface().(*MsgExecuteStuckInbound) if x == nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -5075,20 +5952,17 @@ func (x *fastReflection_MsgVoteChainMeta) ProtoMethods() *protoiface.Methods { i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } - if x.ChainHeight != 0 { - i = runtime.EncodeVarint(dAtA, i, uint64(x.ChainHeight)) - i-- - dAtA[i] = 0x20 - } - if x.Price != 0 { - i = runtime.EncodeVarint(dAtA, i, uint64(x.Price)) - i-- - dAtA[i] = 0x18 - } - if len(x.ObservedChainId) > 0 { - i -= len(x.ObservedChainId) - copy(dAtA[i:], x.ObservedChainId) - i = runtime.EncodeVarint(dAtA, i, uint64(len(x.ObservedChainId))) + if x.Inbound != nil { + encoded, err := options.Marshal(x.Inbound) + if err != nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) i-- dAtA[i] = 0x12 } @@ -5110,7 +5984,7 @@ func (x *fastReflection_MsgVoteChainMeta) ProtoMethods() *protoiface.Methods { }, nil } unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { - x := input.Message.Interface().(*MsgVoteChainMeta) + x := input.Message.Interface().(*MsgExecuteStuckInbound) if x == nil { return protoiface.UnmarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -5142,10 +6016,10 @@ func (x *fastReflection_MsgVoteChainMeta) ProtoMethods() *protoiface.Methods { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteChainMeta: wiretype end group for non-group") + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgExecuteStuckInbound: wiretype end group for non-group") } if fieldNum <= 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteChainMeta: illegal tag %d (wire type %d)", fieldNum, wire) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgExecuteStuckInbound: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: @@ -5182,9 +6056,9 @@ func (x *fastReflection_MsgVoteChainMeta) ProtoMethods() *protoiface.Methods { iNdEx = postIndex case 2: if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ObservedChainId", wireType) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Inbound", wireType) } - var stringLen uint64 + var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow @@ -5194,62 +6068,28 @@ func (x *fastReflection_MsgVoteChainMeta) ProtoMethods() *protoiface.Methods { } b := dAtA[iNdEx] iNdEx++ - stringLen |= uint64(b&0x7F) << shift + msglen |= int(b&0x7F) << shift if b < 0x80 { break } } - intStringLen := int(stringLen) - if intStringLen < 0 { + if msglen < 0 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength } - postIndex := iNdEx + intStringLen + postIndex := iNdEx + msglen if postIndex < 0 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength } if postIndex > l { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF } - x.ObservedChainId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Price", wireType) - } - x.Price = 0 - 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++ - x.Price |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 4: - if wireType != 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ChainHeight", wireType) + if x.Inbound == nil { + x.Inbound = &Inbound{} } - x.ChainHeight = 0 - 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++ - x.ChainHeight |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } + if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.Inbound); err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -5286,24 +6126,26 @@ func (x *fastReflection_MsgVoteChainMeta) ProtoMethods() *protoiface.Methods { } var ( - md_MsgVoteChainMetaResponse protoreflect.MessageDescriptor + md_MsgExecuteStuckInboundResponse protoreflect.MessageDescriptor + fd_MsgExecuteStuckInboundResponse_utx_id protoreflect.FieldDescriptor ) func init() { file_uexecutor_v1_tx_proto_init() - md_MsgVoteChainMetaResponse = File_uexecutor_v1_tx_proto.Messages().ByName("MsgVoteChainMetaResponse") + md_MsgExecuteStuckInboundResponse = File_uexecutor_v1_tx_proto.Messages().ByName("MsgExecuteStuckInboundResponse") + fd_MsgExecuteStuckInboundResponse_utx_id = md_MsgExecuteStuckInboundResponse.Fields().ByName("utx_id") } -var _ protoreflect.Message = (*fastReflection_MsgVoteChainMetaResponse)(nil) +var _ protoreflect.Message = (*fastReflection_MsgExecuteStuckInboundResponse)(nil) -type fastReflection_MsgVoteChainMetaResponse MsgVoteChainMetaResponse +type fastReflection_MsgExecuteStuckInboundResponse MsgExecuteStuckInboundResponse -func (x *MsgVoteChainMetaResponse) ProtoReflect() protoreflect.Message { - return (*fastReflection_MsgVoteChainMetaResponse)(x) +func (x *MsgExecuteStuckInboundResponse) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgExecuteStuckInboundResponse)(x) } -func (x *MsgVoteChainMetaResponse) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_tx_proto_msgTypes[11] +func (x *MsgExecuteStuckInboundResponse) slowProtoReflect() protoreflect.Message { + mi := &file_uexecutor_v1_tx_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5314,43 +6156,43 @@ func (x *MsgVoteChainMetaResponse) slowProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -var _fastReflection_MsgVoteChainMetaResponse_messageType fastReflection_MsgVoteChainMetaResponse_messageType -var _ protoreflect.MessageType = fastReflection_MsgVoteChainMetaResponse_messageType{} +var _fastReflection_MsgExecuteStuckInboundResponse_messageType fastReflection_MsgExecuteStuckInboundResponse_messageType +var _ protoreflect.MessageType = fastReflection_MsgExecuteStuckInboundResponse_messageType{} -type fastReflection_MsgVoteChainMetaResponse_messageType struct{} +type fastReflection_MsgExecuteStuckInboundResponse_messageType struct{} -func (x fastReflection_MsgVoteChainMetaResponse_messageType) Zero() protoreflect.Message { - return (*fastReflection_MsgVoteChainMetaResponse)(nil) +func (x fastReflection_MsgExecuteStuckInboundResponse_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgExecuteStuckInboundResponse)(nil) } -func (x fastReflection_MsgVoteChainMetaResponse_messageType) New() protoreflect.Message { - return new(fastReflection_MsgVoteChainMetaResponse) +func (x fastReflection_MsgExecuteStuckInboundResponse_messageType) New() protoreflect.Message { + return new(fastReflection_MsgExecuteStuckInboundResponse) } -func (x fastReflection_MsgVoteChainMetaResponse_messageType) Descriptor() protoreflect.MessageDescriptor { - return md_MsgVoteChainMetaResponse +func (x fastReflection_MsgExecuteStuckInboundResponse_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgExecuteStuckInboundResponse } // Descriptor returns message descriptor, which contains only the protobuf // type information for the message. -func (x *fastReflection_MsgVoteChainMetaResponse) Descriptor() protoreflect.MessageDescriptor { - return md_MsgVoteChainMetaResponse +func (x *fastReflection_MsgExecuteStuckInboundResponse) Descriptor() protoreflect.MessageDescriptor { + return md_MsgExecuteStuckInboundResponse } // Type returns the message type, which encapsulates both Go and protobuf // type information. If the Go type information is not needed, // it is recommended that the message descriptor be used instead. -func (x *fastReflection_MsgVoteChainMetaResponse) Type() protoreflect.MessageType { - return _fastReflection_MsgVoteChainMetaResponse_messageType +func (x *fastReflection_MsgExecuteStuckInboundResponse) Type() protoreflect.MessageType { + return _fastReflection_MsgExecuteStuckInboundResponse_messageType } // New returns a newly allocated and mutable empty message. -func (x *fastReflection_MsgVoteChainMetaResponse) New() protoreflect.Message { - return new(fastReflection_MsgVoteChainMetaResponse) +func (x *fastReflection_MsgExecuteStuckInboundResponse) New() protoreflect.Message { + return new(fastReflection_MsgExecuteStuckInboundResponse) } // Interface unwraps the message reflection interface and // returns the underlying ProtoMessage interface. -func (x *fastReflection_MsgVoteChainMetaResponse) Interface() protoreflect.ProtoMessage { - return (*MsgVoteChainMetaResponse)(x) +func (x *fastReflection_MsgExecuteStuckInboundResponse) Interface() protoreflect.ProtoMessage { + return (*MsgExecuteStuckInboundResponse)(x) } // Range iterates over every populated field in an undefined order, @@ -5358,7 +6200,13 @@ func (x *fastReflection_MsgVoteChainMetaResponse) Interface() protoreflect.Proto // Range returns immediately if f returns false. // While iterating, mutating operations may only be performed // on the current field descriptor. -func (x *fastReflection_MsgVoteChainMetaResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { +func (x *fastReflection_MsgExecuteStuckInboundResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if x.UtxId != "" { + value := protoreflect.ValueOfString(x.UtxId) + if !f(fd_MsgExecuteStuckInboundResponse_utx_id, value) { + return + } + } } // Has reports whether a field is populated. @@ -5372,13 +6220,15 @@ func (x *fastReflection_MsgVoteChainMetaResponse) Range(f func(protoreflect.Fiel // In other cases (aside from the nullable cases above), // a proto3 scalar field is populated if it contains a non-zero value, and // a repeated field is populated if it is non-empty. -func (x *fastReflection_MsgVoteChainMetaResponse) Has(fd protoreflect.FieldDescriptor) bool { +func (x *fastReflection_MsgExecuteStuckInboundResponse) Has(fd protoreflect.FieldDescriptor) bool { switch fd.FullName() { + case "uexecutor.v1.MsgExecuteStuckInboundResponse.utx_id": + return x.UtxId != "" default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMetaResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckInboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMetaResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckInboundResponse does not contain field %s", fd.FullName())) } } @@ -5388,13 +6238,15 @@ func (x *fastReflection_MsgVoteChainMetaResponse) Has(fd protoreflect.FieldDescr // associated with the given field number. // // Clear is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteChainMetaResponse) Clear(fd protoreflect.FieldDescriptor) { +func (x *fastReflection_MsgExecuteStuckInboundResponse) Clear(fd protoreflect.FieldDescriptor) { switch fd.FullName() { + case "uexecutor.v1.MsgExecuteStuckInboundResponse.utx_id": + x.UtxId = "" default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMetaResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckInboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMetaResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckInboundResponse does not contain field %s", fd.FullName())) } } @@ -5404,13 +6256,16 @@ func (x *fastReflection_MsgVoteChainMetaResponse) Clear(fd protoreflect.FieldDes // the default value of a bytes scalar is guaranteed to be a copy. // For unpopulated composite types, it returns an empty, read-only view // of the value; to obtain a mutable reference, use Mutable. -func (x *fastReflection_MsgVoteChainMetaResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgExecuteStuckInboundResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { switch descriptor.FullName() { + case "uexecutor.v1.MsgExecuteStuckInboundResponse.utx_id": + value := x.UtxId + return protoreflect.ValueOfString(value) default: if descriptor.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMetaResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckInboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMetaResponse does not contain field %s", descriptor.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckInboundResponse does not contain field %s", descriptor.FullName())) } } @@ -5424,13 +6279,15 @@ func (x *fastReflection_MsgVoteChainMetaResponse) Get(descriptor protoreflect.Fi // empty, read-only value, then it panics. // // Set is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteChainMetaResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { +func (x *fastReflection_MsgExecuteStuckInboundResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { switch fd.FullName() { + case "uexecutor.v1.MsgExecuteStuckInboundResponse.utx_id": + x.UtxId = value.Interface().(string) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMetaResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckInboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMetaResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckInboundResponse does not contain field %s", fd.FullName())) } } @@ -5444,36 +6301,40 @@ func (x *fastReflection_MsgVoteChainMetaResponse) Set(fd protoreflect.FieldDescr // It panics if the field does not contain a composite type. // // Mutable is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteChainMetaResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgExecuteStuckInboundResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { + case "uexecutor.v1.MsgExecuteStuckInboundResponse.utx_id": + panic(fmt.Errorf("field utx_id of message uexecutor.v1.MsgExecuteStuckInboundResponse is not mutable")) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMetaResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckInboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMetaResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckInboundResponse does not contain field %s", fd.FullName())) } } // NewField returns a new value that is assignable to the field // for the given descriptor. For scalars, this returns the default value. // For lists, maps, and messages, this returns a new, empty, mutable value. -func (x *fastReflection_MsgVoteChainMetaResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgExecuteStuckInboundResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { + case "uexecutor.v1.MsgExecuteStuckInboundResponse.utx_id": + return protoreflect.ValueOfString("") default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgVoteChainMetaResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckInboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgVoteChainMetaResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckInboundResponse does not contain field %s", fd.FullName())) } } // WhichOneof reports which field within the oneof is populated, // returning nil if none are populated. // It panics if the oneof descriptor does not belong to this message. -func (x *fastReflection_MsgVoteChainMetaResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { +func (x *fastReflection_MsgExecuteStuckInboundResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { switch d.FullName() { default: - panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgVoteChainMetaResponse", d.FullName())) + panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgExecuteStuckInboundResponse", d.FullName())) } panic("unreachable") } @@ -5481,7 +6342,7 @@ func (x *fastReflection_MsgVoteChainMetaResponse) WhichOneof(d protoreflect.Oneo // GetUnknown retrieves the entire list of unknown fields. // The caller may only mutate the contents of the RawFields // if the mutated bytes are stored back into the message with SetUnknown. -func (x *fastReflection_MsgVoteChainMetaResponse) GetUnknown() protoreflect.RawFields { +func (x *fastReflection_MsgExecuteStuckInboundResponse) GetUnknown() protoreflect.RawFields { return x.unknownFields } @@ -5492,7 +6353,7 @@ func (x *fastReflection_MsgVoteChainMetaResponse) GetUnknown() protoreflect.RawF // An empty RawFields may be passed to clear the fields. // // SetUnknown is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgVoteChainMetaResponse) SetUnknown(fields protoreflect.RawFields) { +func (x *fastReflection_MsgExecuteStuckInboundResponse) SetUnknown(fields protoreflect.RawFields) { x.unknownFields = fields } @@ -5504,7 +6365,7 @@ func (x *fastReflection_MsgVoteChainMetaResponse) SetUnknown(fields protoreflect // message type, but the details are implementation dependent. // Validity is not part of the protobuf data model, and may not // be preserved in marshaling or other operations. -func (x *fastReflection_MsgVoteChainMetaResponse) IsValid() bool { +func (x *fastReflection_MsgExecuteStuckInboundResponse) IsValid() bool { return x != nil } @@ -5514,9 +6375,9 @@ func (x *fastReflection_MsgVoteChainMetaResponse) IsValid() bool { // The returned methods type is identical to // "google.golang.org/protobuf/runtime/protoiface".Methods. // Consult the protoiface package documentation for details. -func (x *fastReflection_MsgVoteChainMetaResponse) ProtoMethods() *protoiface.Methods { +func (x *fastReflection_MsgExecuteStuckInboundResponse) ProtoMethods() *protoiface.Methods { size := func(input protoiface.SizeInput) protoiface.SizeOutput { - x := input.Message.Interface().(*MsgVoteChainMetaResponse) + x := input.Message.Interface().(*MsgExecuteStuckInboundResponse) if x == nil { return protoiface.SizeOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -5528,6 +6389,10 @@ func (x *fastReflection_MsgVoteChainMetaResponse) ProtoMethods() *protoiface.Met var n int var l int _ = l + l = len(x.UtxId) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } if x.unknownFields != nil { n += len(x.unknownFields) } @@ -5538,7 +6403,7 @@ func (x *fastReflection_MsgVoteChainMetaResponse) ProtoMethods() *protoiface.Met } marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { - x := input.Message.Interface().(*MsgVoteChainMetaResponse) + x := input.Message.Interface().(*MsgExecuteStuckInboundResponse) if x == nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -5557,6 +6422,13 @@ func (x *fastReflection_MsgVoteChainMetaResponse) ProtoMethods() *protoiface.Met i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } + if len(x.UtxId) > 0 { + i -= len(x.UtxId) + copy(dAtA[i:], x.UtxId) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.UtxId))) + i-- + dAtA[i] = 0xa + } if input.Buf != nil { input.Buf = append(input.Buf, dAtA...) } else { @@ -5568,7 +6440,7 @@ func (x *fastReflection_MsgVoteChainMetaResponse) ProtoMethods() *protoiface.Met }, nil } unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { - x := input.Message.Interface().(*MsgVoteChainMetaResponse) + x := input.Message.Interface().(*MsgExecuteStuckInboundResponse) if x == nil { return protoiface.UnmarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -5600,12 +6472,44 @@ func (x *fastReflection_MsgVoteChainMetaResponse) ProtoMethods() *protoiface.Met fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteChainMetaResponse: wiretype end group for non-group") + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgExecuteStuckInboundResponse: wiretype end group for non-group") } if fieldNum <= 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgVoteChainMetaResponse: illegal tag %d (wire type %d)", fieldNum, wire) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgExecuteStuckInboundResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { + case 1: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field UtxId", wireType) + } + var stringLen uint64 + 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++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.UtxId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -5642,28 +6546,32 @@ func (x *fastReflection_MsgVoteChainMetaResponse) ProtoMethods() *protoiface.Met } var ( - md_MsgRevertStuckInbound protoreflect.MessageDescriptor - fd_MsgRevertStuckInbound_signer protoreflect.FieldDescriptor - fd_MsgRevertStuckInbound_inbound protoreflect.FieldDescriptor + md_MsgExecuteStuckOutbound protoreflect.MessageDescriptor + fd_MsgExecuteStuckOutbound_signer protoreflect.FieldDescriptor + fd_MsgExecuteStuckOutbound_tx_id protoreflect.FieldDescriptor + fd_MsgExecuteStuckOutbound_utx_id protoreflect.FieldDescriptor + fd_MsgExecuteStuckOutbound_observed_tx protoreflect.FieldDescriptor ) func init() { file_uexecutor_v1_tx_proto_init() - md_MsgRevertStuckInbound = File_uexecutor_v1_tx_proto.Messages().ByName("MsgRevertStuckInbound") - fd_MsgRevertStuckInbound_signer = md_MsgRevertStuckInbound.Fields().ByName("signer") - fd_MsgRevertStuckInbound_inbound = md_MsgRevertStuckInbound.Fields().ByName("inbound") + md_MsgExecuteStuckOutbound = File_uexecutor_v1_tx_proto.Messages().ByName("MsgExecuteStuckOutbound") + fd_MsgExecuteStuckOutbound_signer = md_MsgExecuteStuckOutbound.Fields().ByName("signer") + fd_MsgExecuteStuckOutbound_tx_id = md_MsgExecuteStuckOutbound.Fields().ByName("tx_id") + fd_MsgExecuteStuckOutbound_utx_id = md_MsgExecuteStuckOutbound.Fields().ByName("utx_id") + fd_MsgExecuteStuckOutbound_observed_tx = md_MsgExecuteStuckOutbound.Fields().ByName("observed_tx") } -var _ protoreflect.Message = (*fastReflection_MsgRevertStuckInbound)(nil) +var _ protoreflect.Message = (*fastReflection_MsgExecuteStuckOutbound)(nil) -type fastReflection_MsgRevertStuckInbound MsgRevertStuckInbound +type fastReflection_MsgExecuteStuckOutbound MsgExecuteStuckOutbound -func (x *MsgRevertStuckInbound) ProtoReflect() protoreflect.Message { - return (*fastReflection_MsgRevertStuckInbound)(x) +func (x *MsgExecuteStuckOutbound) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgExecuteStuckOutbound)(x) } -func (x *MsgRevertStuckInbound) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_tx_proto_msgTypes[12] +func (x *MsgExecuteStuckOutbound) slowProtoReflect() protoreflect.Message { + mi := &file_uexecutor_v1_tx_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5674,43 +6582,43 @@ func (x *MsgRevertStuckInbound) slowProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -var _fastReflection_MsgRevertStuckInbound_messageType fastReflection_MsgRevertStuckInbound_messageType -var _ protoreflect.MessageType = fastReflection_MsgRevertStuckInbound_messageType{} +var _fastReflection_MsgExecuteStuckOutbound_messageType fastReflection_MsgExecuteStuckOutbound_messageType +var _ protoreflect.MessageType = fastReflection_MsgExecuteStuckOutbound_messageType{} -type fastReflection_MsgRevertStuckInbound_messageType struct{} +type fastReflection_MsgExecuteStuckOutbound_messageType struct{} -func (x fastReflection_MsgRevertStuckInbound_messageType) Zero() protoreflect.Message { - return (*fastReflection_MsgRevertStuckInbound)(nil) +func (x fastReflection_MsgExecuteStuckOutbound_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgExecuteStuckOutbound)(nil) } -func (x fastReflection_MsgRevertStuckInbound_messageType) New() protoreflect.Message { - return new(fastReflection_MsgRevertStuckInbound) +func (x fastReflection_MsgExecuteStuckOutbound_messageType) New() protoreflect.Message { + return new(fastReflection_MsgExecuteStuckOutbound) } -func (x fastReflection_MsgRevertStuckInbound_messageType) Descriptor() protoreflect.MessageDescriptor { - return md_MsgRevertStuckInbound +func (x fastReflection_MsgExecuteStuckOutbound_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgExecuteStuckOutbound } // Descriptor returns message descriptor, which contains only the protobuf // type information for the message. -func (x *fastReflection_MsgRevertStuckInbound) Descriptor() protoreflect.MessageDescriptor { - return md_MsgRevertStuckInbound +func (x *fastReflection_MsgExecuteStuckOutbound) Descriptor() protoreflect.MessageDescriptor { + return md_MsgExecuteStuckOutbound } // Type returns the message type, which encapsulates both Go and protobuf // type information. If the Go type information is not needed, // it is recommended that the message descriptor be used instead. -func (x *fastReflection_MsgRevertStuckInbound) Type() protoreflect.MessageType { - return _fastReflection_MsgRevertStuckInbound_messageType +func (x *fastReflection_MsgExecuteStuckOutbound) Type() protoreflect.MessageType { + return _fastReflection_MsgExecuteStuckOutbound_messageType } // New returns a newly allocated and mutable empty message. -func (x *fastReflection_MsgRevertStuckInbound) New() protoreflect.Message { - return new(fastReflection_MsgRevertStuckInbound) +func (x *fastReflection_MsgExecuteStuckOutbound) New() protoreflect.Message { + return new(fastReflection_MsgExecuteStuckOutbound) } // Interface unwraps the message reflection interface and // returns the underlying ProtoMessage interface. -func (x *fastReflection_MsgRevertStuckInbound) Interface() protoreflect.ProtoMessage { - return (*MsgRevertStuckInbound)(x) +func (x *fastReflection_MsgExecuteStuckOutbound) Interface() protoreflect.ProtoMessage { + return (*MsgExecuteStuckOutbound)(x) } // Range iterates over every populated field in an undefined order, @@ -5718,16 +6626,28 @@ func (x *fastReflection_MsgRevertStuckInbound) Interface() protoreflect.ProtoMes // Range returns immediately if f returns false. // While iterating, mutating operations may only be performed // on the current field descriptor. -func (x *fastReflection_MsgRevertStuckInbound) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { +func (x *fastReflection_MsgExecuteStuckOutbound) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { if x.Signer != "" { value := protoreflect.ValueOfString(x.Signer) - if !f(fd_MsgRevertStuckInbound_signer, value) { + if !f(fd_MsgExecuteStuckOutbound_signer, value) { return } } - if x.Inbound != nil { - value := protoreflect.ValueOfMessage(x.Inbound.ProtoReflect()) - if !f(fd_MsgRevertStuckInbound_inbound, value) { + if x.TxId != "" { + value := protoreflect.ValueOfString(x.TxId) + if !f(fd_MsgExecuteStuckOutbound_tx_id, value) { + return + } + } + if x.UtxId != "" { + value := protoreflect.ValueOfString(x.UtxId) + if !f(fd_MsgExecuteStuckOutbound_utx_id, value) { + return + } + } + if x.ObservedTx != nil { + value := protoreflect.ValueOfMessage(x.ObservedTx.ProtoReflect()) + if !f(fd_MsgExecuteStuckOutbound_observed_tx, value) { return } } @@ -5744,17 +6664,21 @@ func (x *fastReflection_MsgRevertStuckInbound) Range(f func(protoreflect.FieldDe // In other cases (aside from the nullable cases above), // a proto3 scalar field is populated if it contains a non-zero value, and // a repeated field is populated if it is non-empty. -func (x *fastReflection_MsgRevertStuckInbound) Has(fd protoreflect.FieldDescriptor) bool { +func (x *fastReflection_MsgExecuteStuckOutbound) Has(fd protoreflect.FieldDescriptor) bool { switch fd.FullName() { - case "uexecutor.v1.MsgRevertStuckInbound.signer": + case "uexecutor.v1.MsgExecuteStuckOutbound.signer": return x.Signer != "" - case "uexecutor.v1.MsgRevertStuckInbound.inbound": - return x.Inbound != nil + case "uexecutor.v1.MsgExecuteStuckOutbound.tx_id": + return x.TxId != "" + case "uexecutor.v1.MsgExecuteStuckOutbound.utx_id": + return x.UtxId != "" + case "uexecutor.v1.MsgExecuteStuckOutbound.observed_tx": + return x.ObservedTx != nil default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckOutbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckOutbound does not contain field %s", fd.FullName())) } } @@ -5764,17 +6688,21 @@ func (x *fastReflection_MsgRevertStuckInbound) Has(fd protoreflect.FieldDescript // associated with the given field number. // // Clear is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgRevertStuckInbound) Clear(fd protoreflect.FieldDescriptor) { +func (x *fastReflection_MsgExecuteStuckOutbound) Clear(fd protoreflect.FieldDescriptor) { switch fd.FullName() { - case "uexecutor.v1.MsgRevertStuckInbound.signer": + case "uexecutor.v1.MsgExecuteStuckOutbound.signer": x.Signer = "" - case "uexecutor.v1.MsgRevertStuckInbound.inbound": - x.Inbound = nil + case "uexecutor.v1.MsgExecuteStuckOutbound.tx_id": + x.TxId = "" + case "uexecutor.v1.MsgExecuteStuckOutbound.utx_id": + x.UtxId = "" + case "uexecutor.v1.MsgExecuteStuckOutbound.observed_tx": + x.ObservedTx = nil default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckOutbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckOutbound does not contain field %s", fd.FullName())) } } @@ -5784,19 +6712,25 @@ func (x *fastReflection_MsgRevertStuckInbound) Clear(fd protoreflect.FieldDescri // the default value of a bytes scalar is guaranteed to be a copy. // For unpopulated composite types, it returns an empty, read-only view // of the value; to obtain a mutable reference, use Mutable. -func (x *fastReflection_MsgRevertStuckInbound) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgExecuteStuckOutbound) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { switch descriptor.FullName() { - case "uexecutor.v1.MsgRevertStuckInbound.signer": + case "uexecutor.v1.MsgExecuteStuckOutbound.signer": value := x.Signer return protoreflect.ValueOfString(value) - case "uexecutor.v1.MsgRevertStuckInbound.inbound": - value := x.Inbound + case "uexecutor.v1.MsgExecuteStuckOutbound.tx_id": + value := x.TxId + return protoreflect.ValueOfString(value) + case "uexecutor.v1.MsgExecuteStuckOutbound.utx_id": + value := x.UtxId + return protoreflect.ValueOfString(value) + case "uexecutor.v1.MsgExecuteStuckOutbound.observed_tx": + value := x.ObservedTx return protoreflect.ValueOfMessage(value.ProtoReflect()) default: if descriptor.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckOutbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInbound does not contain field %s", descriptor.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckOutbound does not contain field %s", descriptor.FullName())) } } @@ -5810,17 +6744,21 @@ func (x *fastReflection_MsgRevertStuckInbound) Get(descriptor protoreflect.Field // empty, read-only value, then it panics. // // Set is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgRevertStuckInbound) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { +func (x *fastReflection_MsgExecuteStuckOutbound) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { switch fd.FullName() { - case "uexecutor.v1.MsgRevertStuckInbound.signer": + case "uexecutor.v1.MsgExecuteStuckOutbound.signer": x.Signer = value.Interface().(string) - case "uexecutor.v1.MsgRevertStuckInbound.inbound": - x.Inbound = value.Message().Interface().(*Inbound) + case "uexecutor.v1.MsgExecuteStuckOutbound.tx_id": + x.TxId = value.Interface().(string) + case "uexecutor.v1.MsgExecuteStuckOutbound.utx_id": + x.UtxId = value.Interface().(string) + case "uexecutor.v1.MsgExecuteStuckOutbound.observed_tx": + x.ObservedTx = value.Message().Interface().(*OutboundObservation) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckOutbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckOutbound does not contain field %s", fd.FullName())) } } @@ -5834,48 +6772,56 @@ func (x *fastReflection_MsgRevertStuckInbound) Set(fd protoreflect.FieldDescript // It panics if the field does not contain a composite type. // // Mutable is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgRevertStuckInbound) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgExecuteStuckOutbound) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { - case "uexecutor.v1.MsgRevertStuckInbound.inbound": - if x.Inbound == nil { - x.Inbound = new(Inbound) + case "uexecutor.v1.MsgExecuteStuckOutbound.observed_tx": + if x.ObservedTx == nil { + x.ObservedTx = new(OutboundObservation) } - return protoreflect.ValueOfMessage(x.Inbound.ProtoReflect()) - case "uexecutor.v1.MsgRevertStuckInbound.signer": - panic(fmt.Errorf("field signer of message uexecutor.v1.MsgRevertStuckInbound is not mutable")) + return protoreflect.ValueOfMessage(x.ObservedTx.ProtoReflect()) + case "uexecutor.v1.MsgExecuteStuckOutbound.signer": + panic(fmt.Errorf("field signer of message uexecutor.v1.MsgExecuteStuckOutbound is not mutable")) + case "uexecutor.v1.MsgExecuteStuckOutbound.tx_id": + panic(fmt.Errorf("field tx_id of message uexecutor.v1.MsgExecuteStuckOutbound is not mutable")) + case "uexecutor.v1.MsgExecuteStuckOutbound.utx_id": + panic(fmt.Errorf("field utx_id of message uexecutor.v1.MsgExecuteStuckOutbound is not mutable")) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckOutbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckOutbound does not contain field %s", fd.FullName())) } } // NewField returns a new value that is assignable to the field // for the given descriptor. For scalars, this returns the default value. // For lists, maps, and messages, this returns a new, empty, mutable value. -func (x *fastReflection_MsgRevertStuckInbound) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgExecuteStuckOutbound) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { - case "uexecutor.v1.MsgRevertStuckInbound.signer": + case "uexecutor.v1.MsgExecuteStuckOutbound.signer": return protoreflect.ValueOfString("") - case "uexecutor.v1.MsgRevertStuckInbound.inbound": - m := new(Inbound) + case "uexecutor.v1.MsgExecuteStuckOutbound.tx_id": + return protoreflect.ValueOfString("") + case "uexecutor.v1.MsgExecuteStuckOutbound.utx_id": + return protoreflect.ValueOfString("") + case "uexecutor.v1.MsgExecuteStuckOutbound.observed_tx": + m := new(OutboundObservation) return protoreflect.ValueOfMessage(m.ProtoReflect()) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInbound")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckOutbound")) } - panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInbound does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckOutbound does not contain field %s", fd.FullName())) } } // WhichOneof reports which field within the oneof is populated, // returning nil if none are populated. // It panics if the oneof descriptor does not belong to this message. -func (x *fastReflection_MsgRevertStuckInbound) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { +func (x *fastReflection_MsgExecuteStuckOutbound) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { switch d.FullName() { default: - panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgRevertStuckInbound", d.FullName())) + panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgExecuteStuckOutbound", d.FullName())) } panic("unreachable") } @@ -5883,7 +6829,7 @@ func (x *fastReflection_MsgRevertStuckInbound) WhichOneof(d protoreflect.OneofDe // GetUnknown retrieves the entire list of unknown fields. // The caller may only mutate the contents of the RawFields // if the mutated bytes are stored back into the message with SetUnknown. -func (x *fastReflection_MsgRevertStuckInbound) GetUnknown() protoreflect.RawFields { +func (x *fastReflection_MsgExecuteStuckOutbound) GetUnknown() protoreflect.RawFields { return x.unknownFields } @@ -5894,7 +6840,7 @@ func (x *fastReflection_MsgRevertStuckInbound) GetUnknown() protoreflect.RawFiel // An empty RawFields may be passed to clear the fields. // // SetUnknown is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgRevertStuckInbound) SetUnknown(fields protoreflect.RawFields) { +func (x *fastReflection_MsgExecuteStuckOutbound) SetUnknown(fields protoreflect.RawFields) { x.unknownFields = fields } @@ -5906,7 +6852,7 @@ func (x *fastReflection_MsgRevertStuckInbound) SetUnknown(fields protoreflect.Ra // message type, but the details are implementation dependent. // Validity is not part of the protobuf data model, and may not // be preserved in marshaling or other operations. -func (x *fastReflection_MsgRevertStuckInbound) IsValid() bool { +func (x *fastReflection_MsgExecuteStuckOutbound) IsValid() bool { return x != nil } @@ -5916,9 +6862,9 @@ func (x *fastReflection_MsgRevertStuckInbound) IsValid() bool { // The returned methods type is identical to // "google.golang.org/protobuf/runtime/protoiface".Methods. // Consult the protoiface package documentation for details. -func (x *fastReflection_MsgRevertStuckInbound) ProtoMethods() *protoiface.Methods { +func (x *fastReflection_MsgExecuteStuckOutbound) ProtoMethods() *protoiface.Methods { size := func(input protoiface.SizeInput) protoiface.SizeOutput { - x := input.Message.Interface().(*MsgRevertStuckInbound) + x := input.Message.Interface().(*MsgExecuteStuckOutbound) if x == nil { return protoiface.SizeOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -5934,8 +6880,16 @@ func (x *fastReflection_MsgRevertStuckInbound) ProtoMethods() *protoiface.Method if l > 0 { n += 1 + l + runtime.Sov(uint64(l)) } - if x.Inbound != nil { - l = options.Size(x.Inbound) + l = len(x.TxId) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + l = len(x.UtxId) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + if x.ObservedTx != nil { + l = options.Size(x.ObservedTx) n += 1 + l + runtime.Sov(uint64(l)) } if x.unknownFields != nil { @@ -5948,7 +6902,7 @@ func (x *fastReflection_MsgRevertStuckInbound) ProtoMethods() *protoiface.Method } marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { - x := input.Message.Interface().(*MsgRevertStuckInbound) + x := input.Message.Interface().(*MsgExecuteStuckOutbound) if x == nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -5967,8 +6921,8 @@ func (x *fastReflection_MsgRevertStuckInbound) ProtoMethods() *protoiface.Method i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } - if x.Inbound != nil { - encoded, err := options.Marshal(x.Inbound) + if x.ObservedTx != nil { + encoded, err := options.Marshal(x.ObservedTx) if err != nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -5979,6 +6933,20 @@ func (x *fastReflection_MsgRevertStuckInbound) ProtoMethods() *protoiface.Method copy(dAtA[i:], encoded) i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) i-- + dAtA[i] = 0x22 + } + if len(x.UtxId) > 0 { + i -= len(x.UtxId) + copy(dAtA[i:], x.UtxId) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.UtxId))) + i-- + dAtA[i] = 0x1a + } + if len(x.TxId) > 0 { + i -= len(x.TxId) + copy(dAtA[i:], x.TxId) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.TxId))) + i-- dAtA[i] = 0x12 } if len(x.Signer) > 0 { @@ -5999,7 +6967,7 @@ func (x *fastReflection_MsgRevertStuckInbound) ProtoMethods() *protoiface.Method }, nil } unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { - x := input.Message.Interface().(*MsgRevertStuckInbound) + x := input.Message.Interface().(*MsgExecuteStuckOutbound) if x == nil { return protoiface.UnmarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -6027,19 +6995,83 @@ func (x *fastReflection_MsgRevertStuckInbound) ProtoMethods() *protoiface.Method if b < 0x80 { break } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgRevertStuckInbound: wiretype end group for non-group") - } - if fieldNum <= 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgRevertStuckInbound: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgExecuteStuckOutbound: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgExecuteStuckOutbound: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Signer", wireType) + } + var stringLen uint64 + 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++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.Signer = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field TxId", wireType) + } + var stringLen uint64 + 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++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.TxId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Signer", wireType) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field UtxId", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -6067,11 +7099,11 @@ func (x *fastReflection_MsgRevertStuckInbound) ProtoMethods() *protoiface.Method if postIndex > l { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF } - x.Signer = string(dAtA[iNdEx:postIndex]) + x.UtxId = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex - case 2: + case 4: if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Inbound", wireType) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ObservedTx", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -6098,10 +7130,10 @@ func (x *fastReflection_MsgRevertStuckInbound) ProtoMethods() *protoiface.Method if postIndex > l { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF } - if x.Inbound == nil { - x.Inbound = &Inbound{} + if x.ObservedTx == nil { + x.ObservedTx = &OutboundObservation{} } - if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.Inbound); err != nil { + if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.ObservedTx); err != nil { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err } iNdEx = postIndex @@ -6141,28 +7173,26 @@ func (x *fastReflection_MsgRevertStuckInbound) ProtoMethods() *protoiface.Method } var ( - md_MsgRevertStuckInboundResponse protoreflect.MessageDescriptor - fd_MsgRevertStuckInboundResponse_utx_id protoreflect.FieldDescriptor - fd_MsgRevertStuckInboundResponse_outbound_id protoreflect.FieldDescriptor + md_MsgExecuteStuckOutboundResponse protoreflect.MessageDescriptor + fd_MsgExecuteStuckOutboundResponse_outbound_id protoreflect.FieldDescriptor ) func init() { file_uexecutor_v1_tx_proto_init() - md_MsgRevertStuckInboundResponse = File_uexecutor_v1_tx_proto.Messages().ByName("MsgRevertStuckInboundResponse") - fd_MsgRevertStuckInboundResponse_utx_id = md_MsgRevertStuckInboundResponse.Fields().ByName("utx_id") - fd_MsgRevertStuckInboundResponse_outbound_id = md_MsgRevertStuckInboundResponse.Fields().ByName("outbound_id") + md_MsgExecuteStuckOutboundResponse = File_uexecutor_v1_tx_proto.Messages().ByName("MsgExecuteStuckOutboundResponse") + fd_MsgExecuteStuckOutboundResponse_outbound_id = md_MsgExecuteStuckOutboundResponse.Fields().ByName("outbound_id") } -var _ protoreflect.Message = (*fastReflection_MsgRevertStuckInboundResponse)(nil) +var _ protoreflect.Message = (*fastReflection_MsgExecuteStuckOutboundResponse)(nil) -type fastReflection_MsgRevertStuckInboundResponse MsgRevertStuckInboundResponse +type fastReflection_MsgExecuteStuckOutboundResponse MsgExecuteStuckOutboundResponse -func (x *MsgRevertStuckInboundResponse) ProtoReflect() protoreflect.Message { - return (*fastReflection_MsgRevertStuckInboundResponse)(x) +func (x *MsgExecuteStuckOutboundResponse) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgExecuteStuckOutboundResponse)(x) } -func (x *MsgRevertStuckInboundResponse) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_tx_proto_msgTypes[13] +func (x *MsgExecuteStuckOutboundResponse) slowProtoReflect() protoreflect.Message { + mi := &file_uexecutor_v1_tx_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6173,43 +7203,43 @@ func (x *MsgRevertStuckInboundResponse) slowProtoReflect() protoreflect.Message return mi.MessageOf(x) } -var _fastReflection_MsgRevertStuckInboundResponse_messageType fastReflection_MsgRevertStuckInboundResponse_messageType -var _ protoreflect.MessageType = fastReflection_MsgRevertStuckInboundResponse_messageType{} +var _fastReflection_MsgExecuteStuckOutboundResponse_messageType fastReflection_MsgExecuteStuckOutboundResponse_messageType +var _ protoreflect.MessageType = fastReflection_MsgExecuteStuckOutboundResponse_messageType{} -type fastReflection_MsgRevertStuckInboundResponse_messageType struct{} +type fastReflection_MsgExecuteStuckOutboundResponse_messageType struct{} -func (x fastReflection_MsgRevertStuckInboundResponse_messageType) Zero() protoreflect.Message { - return (*fastReflection_MsgRevertStuckInboundResponse)(nil) +func (x fastReflection_MsgExecuteStuckOutboundResponse_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgExecuteStuckOutboundResponse)(nil) } -func (x fastReflection_MsgRevertStuckInboundResponse_messageType) New() protoreflect.Message { - return new(fastReflection_MsgRevertStuckInboundResponse) +func (x fastReflection_MsgExecuteStuckOutboundResponse_messageType) New() protoreflect.Message { + return new(fastReflection_MsgExecuteStuckOutboundResponse) } -func (x fastReflection_MsgRevertStuckInboundResponse_messageType) Descriptor() protoreflect.MessageDescriptor { - return md_MsgRevertStuckInboundResponse +func (x fastReflection_MsgExecuteStuckOutboundResponse_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgExecuteStuckOutboundResponse } // Descriptor returns message descriptor, which contains only the protobuf // type information for the message. -func (x *fastReflection_MsgRevertStuckInboundResponse) Descriptor() protoreflect.MessageDescriptor { - return md_MsgRevertStuckInboundResponse +func (x *fastReflection_MsgExecuteStuckOutboundResponse) Descriptor() protoreflect.MessageDescriptor { + return md_MsgExecuteStuckOutboundResponse } // Type returns the message type, which encapsulates both Go and protobuf // type information. If the Go type information is not needed, // it is recommended that the message descriptor be used instead. -func (x *fastReflection_MsgRevertStuckInboundResponse) Type() protoreflect.MessageType { - return _fastReflection_MsgRevertStuckInboundResponse_messageType +func (x *fastReflection_MsgExecuteStuckOutboundResponse) Type() protoreflect.MessageType { + return _fastReflection_MsgExecuteStuckOutboundResponse_messageType } // New returns a newly allocated and mutable empty message. -func (x *fastReflection_MsgRevertStuckInboundResponse) New() protoreflect.Message { - return new(fastReflection_MsgRevertStuckInboundResponse) +func (x *fastReflection_MsgExecuteStuckOutboundResponse) New() protoreflect.Message { + return new(fastReflection_MsgExecuteStuckOutboundResponse) } // Interface unwraps the message reflection interface and // returns the underlying ProtoMessage interface. -func (x *fastReflection_MsgRevertStuckInboundResponse) Interface() protoreflect.ProtoMessage { - return (*MsgRevertStuckInboundResponse)(x) +func (x *fastReflection_MsgExecuteStuckOutboundResponse) Interface() protoreflect.ProtoMessage { + return (*MsgExecuteStuckOutboundResponse)(x) } // Range iterates over every populated field in an undefined order, @@ -6217,16 +7247,10 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) Interface() protoreflect. // Range returns immediately if f returns false. // While iterating, mutating operations may only be performed // on the current field descriptor. -func (x *fastReflection_MsgRevertStuckInboundResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { - if x.UtxId != "" { - value := protoreflect.ValueOfString(x.UtxId) - if !f(fd_MsgRevertStuckInboundResponse_utx_id, value) { - return - } - } +func (x *fastReflection_MsgExecuteStuckOutboundResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { if x.OutboundId != "" { value := protoreflect.ValueOfString(x.OutboundId) - if !f(fd_MsgRevertStuckInboundResponse_outbound_id, value) { + if !f(fd_MsgExecuteStuckOutboundResponse_outbound_id, value) { return } } @@ -6243,17 +7267,15 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) Range(f func(protoreflect // In other cases (aside from the nullable cases above), // a proto3 scalar field is populated if it contains a non-zero value, and // a repeated field is populated if it is non-empty. -func (x *fastReflection_MsgRevertStuckInboundResponse) Has(fd protoreflect.FieldDescriptor) bool { +func (x *fastReflection_MsgExecuteStuckOutboundResponse) Has(fd protoreflect.FieldDescriptor) bool { switch fd.FullName() { - case "uexecutor.v1.MsgRevertStuckInboundResponse.utx_id": - return x.UtxId != "" - case "uexecutor.v1.MsgRevertStuckInboundResponse.outbound_id": + case "uexecutor.v1.MsgExecuteStuckOutboundResponse.outbound_id": return x.OutboundId != "" default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckOutboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckOutboundResponse does not contain field %s", fd.FullName())) } } @@ -6263,17 +7285,15 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) Has(fd protoreflect.Field // associated with the given field number. // // Clear is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgRevertStuckInboundResponse) Clear(fd protoreflect.FieldDescriptor) { +func (x *fastReflection_MsgExecuteStuckOutboundResponse) Clear(fd protoreflect.FieldDescriptor) { switch fd.FullName() { - case "uexecutor.v1.MsgRevertStuckInboundResponse.utx_id": - x.UtxId = "" - case "uexecutor.v1.MsgRevertStuckInboundResponse.outbound_id": + case "uexecutor.v1.MsgExecuteStuckOutboundResponse.outbound_id": x.OutboundId = "" default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckOutboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckOutboundResponse does not contain field %s", fd.FullName())) } } @@ -6283,19 +7303,16 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) Clear(fd protoreflect.Fie // the default value of a bytes scalar is guaranteed to be a copy. // For unpopulated composite types, it returns an empty, read-only view // of the value; to obtain a mutable reference, use Mutable. -func (x *fastReflection_MsgRevertStuckInboundResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgExecuteStuckOutboundResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { switch descriptor.FullName() { - case "uexecutor.v1.MsgRevertStuckInboundResponse.utx_id": - value := x.UtxId - return protoreflect.ValueOfString(value) - case "uexecutor.v1.MsgRevertStuckInboundResponse.outbound_id": + case "uexecutor.v1.MsgExecuteStuckOutboundResponse.outbound_id": value := x.OutboundId return protoreflect.ValueOfString(value) default: if descriptor.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckOutboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInboundResponse does not contain field %s", descriptor.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckOutboundResponse does not contain field %s", descriptor.FullName())) } } @@ -6309,17 +7326,15 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) Get(descriptor protorefle // empty, read-only value, then it panics. // // Set is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgRevertStuckInboundResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { +func (x *fastReflection_MsgExecuteStuckOutboundResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { switch fd.FullName() { - case "uexecutor.v1.MsgRevertStuckInboundResponse.utx_id": - x.UtxId = value.Interface().(string) - case "uexecutor.v1.MsgRevertStuckInboundResponse.outbound_id": + case "uexecutor.v1.MsgExecuteStuckOutboundResponse.outbound_id": x.OutboundId = value.Interface().(string) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckOutboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckOutboundResponse does not contain field %s", fd.FullName())) } } @@ -6333,44 +7348,40 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) Set(fd protoreflect.Field // It panics if the field does not contain a composite type. // // Mutable is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgRevertStuckInboundResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgExecuteStuckOutboundResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { - case "uexecutor.v1.MsgRevertStuckInboundResponse.utx_id": - panic(fmt.Errorf("field utx_id of message uexecutor.v1.MsgRevertStuckInboundResponse is not mutable")) - case "uexecutor.v1.MsgRevertStuckInboundResponse.outbound_id": - panic(fmt.Errorf("field outbound_id of message uexecutor.v1.MsgRevertStuckInboundResponse is not mutable")) + case "uexecutor.v1.MsgExecuteStuckOutboundResponse.outbound_id": + panic(fmt.Errorf("field outbound_id of message uexecutor.v1.MsgExecuteStuckOutboundResponse is not mutable")) default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckOutboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckOutboundResponse does not contain field %s", fd.FullName())) } } // NewField returns a new value that is assignable to the field // for the given descriptor. For scalars, this returns the default value. // For lists, maps, and messages, this returns a new, empty, mutable value. -func (x *fastReflection_MsgRevertStuckInboundResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { +func (x *fastReflection_MsgExecuteStuckOutboundResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { switch fd.FullName() { - case "uexecutor.v1.MsgRevertStuckInboundResponse.utx_id": - return protoreflect.ValueOfString("") - case "uexecutor.v1.MsgRevertStuckInboundResponse.outbound_id": + case "uexecutor.v1.MsgExecuteStuckOutboundResponse.outbound_id": return protoreflect.ValueOfString("") default: if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgRevertStuckInboundResponse")) + panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MsgExecuteStuckOutboundResponse")) } - panic(fmt.Errorf("message uexecutor.v1.MsgRevertStuckInboundResponse does not contain field %s", fd.FullName())) + panic(fmt.Errorf("message uexecutor.v1.MsgExecuteStuckOutboundResponse does not contain field %s", fd.FullName())) } } // WhichOneof reports which field within the oneof is populated, // returning nil if none are populated. // It panics if the oneof descriptor does not belong to this message. -func (x *fastReflection_MsgRevertStuckInboundResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { +func (x *fastReflection_MsgExecuteStuckOutboundResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { switch d.FullName() { default: - panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgRevertStuckInboundResponse", d.FullName())) + panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MsgExecuteStuckOutboundResponse", d.FullName())) } panic("unreachable") } @@ -6378,7 +7389,7 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) WhichOneof(d protoreflect // GetUnknown retrieves the entire list of unknown fields. // The caller may only mutate the contents of the RawFields // if the mutated bytes are stored back into the message with SetUnknown. -func (x *fastReflection_MsgRevertStuckInboundResponse) GetUnknown() protoreflect.RawFields { +func (x *fastReflection_MsgExecuteStuckOutboundResponse) GetUnknown() protoreflect.RawFields { return x.unknownFields } @@ -6389,7 +7400,7 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) GetUnknown() protoreflect // An empty RawFields may be passed to clear the fields. // // SetUnknown is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MsgRevertStuckInboundResponse) SetUnknown(fields protoreflect.RawFields) { +func (x *fastReflection_MsgExecuteStuckOutboundResponse) SetUnknown(fields protoreflect.RawFields) { x.unknownFields = fields } @@ -6401,7 +7412,7 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) SetUnknown(fields protore // message type, but the details are implementation dependent. // Validity is not part of the protobuf data model, and may not // be preserved in marshaling or other operations. -func (x *fastReflection_MsgRevertStuckInboundResponse) IsValid() bool { +func (x *fastReflection_MsgExecuteStuckOutboundResponse) IsValid() bool { return x != nil } @@ -6411,9 +7422,9 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) IsValid() bool { // The returned methods type is identical to // "google.golang.org/protobuf/runtime/protoiface".Methods. // Consult the protoiface package documentation for details. -func (x *fastReflection_MsgRevertStuckInboundResponse) ProtoMethods() *protoiface.Methods { +func (x *fastReflection_MsgExecuteStuckOutboundResponse) ProtoMethods() *protoiface.Methods { size := func(input protoiface.SizeInput) protoiface.SizeOutput { - x := input.Message.Interface().(*MsgRevertStuckInboundResponse) + x := input.Message.Interface().(*MsgExecuteStuckOutboundResponse) if x == nil { return protoiface.SizeOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -6425,10 +7436,6 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) ProtoMethods() *protoifac var n int var l int _ = l - l = len(x.UtxId) - if l > 0 { - n += 1 + l + runtime.Sov(uint64(l)) - } l = len(x.OutboundId) if l > 0 { n += 1 + l + runtime.Sov(uint64(l)) @@ -6443,7 +7450,7 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) ProtoMethods() *protoifac } marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { - x := input.Message.Interface().(*MsgRevertStuckInboundResponse) + x := input.Message.Interface().(*MsgExecuteStuckOutboundResponse) if x == nil { return protoiface.MarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -6467,13 +7474,6 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) ProtoMethods() *protoifac copy(dAtA[i:], x.OutboundId) i = runtime.EncodeVarint(dAtA, i, uint64(len(x.OutboundId))) i-- - dAtA[i] = 0x12 - } - if len(x.UtxId) > 0 { - i -= len(x.UtxId) - copy(dAtA[i:], x.UtxId) - i = runtime.EncodeVarint(dAtA, i, uint64(len(x.UtxId))) - i-- dAtA[i] = 0xa } if input.Buf != nil { @@ -6487,7 +7487,7 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) ProtoMethods() *protoifac }, nil } unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { - x := input.Message.Interface().(*MsgRevertStuckInboundResponse) + x := input.Message.Interface().(*MsgExecuteStuckOutboundResponse) if x == nil { return protoiface.UnmarshalOutput{ NoUnkeyedLiterals: input.NoUnkeyedLiterals, @@ -6519,45 +7519,13 @@ func (x *fastReflection_MsgRevertStuckInboundResponse) ProtoMethods() *protoifac fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgRevertStuckInboundResponse: wiretype end group for non-group") + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgExecuteStuckOutboundResponse: wiretype end group for non-group") } if fieldNum <= 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgRevertStuckInboundResponse: illegal tag %d (wire type %d)", fieldNum, wire) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgExecuteStuckOutboundResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: - if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field UtxId", wireType) - } - var stringLen uint64 - 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++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - if postIndex > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - x.UtxId = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: if wireType != 2 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field OutboundId", wireType) } @@ -6687,129 +7655,38 @@ func (x *MsgUpdateParams) GetParams() *Params { return nil } -// MsgUpdateParamsResponse defines the response structure for executing a -// MsgUpdateParams message. -// -// Since: cosmos-sdk 0.47 -type MsgUpdateParamsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields -} - -func (x *MsgUpdateParamsResponse) Reset() { - *x = MsgUpdateParamsResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *MsgUpdateParamsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MsgUpdateParamsResponse) ProtoMessage() {} - -// Deprecated: Use MsgUpdateParamsResponse.ProtoReflect.Descriptor instead. -func (*MsgUpdateParamsResponse) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{1} -} - -// MsgExecutePayload defines a message for executing a universal payload -type MsgExecutePayload struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // signer is the Cosmos address initiating the tx (used for tx signing) - Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"` - // universal_account_id is the identifier of the owner account - UniversalAccountId *UniversalAccountId `protobuf:"bytes,2,opt,name=universal_account_id,json=universalAccountId,proto3" json:"universal_account_id,omitempty"` - // payload is the universal payload to be executed - UniversalPayload *UniversalPayload `protobuf:"bytes,3,opt,name=universal_payload,json=universalPayload,proto3" json:"universal_payload,omitempty"` - // verification_data is the bytes passed as verifier data for the given payload. - VerificationData string `protobuf:"bytes,4,opt,name=verification_data,json=verificationData,proto3" json:"verification_data,omitempty"` -} - -func (x *MsgExecutePayload) Reset() { - *x = MsgExecutePayload{} - if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *MsgExecutePayload) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MsgExecutePayload) ProtoMessage() {} - -// Deprecated: Use MsgExecutePayload.ProtoReflect.Descriptor instead. -func (*MsgExecutePayload) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{2} -} - -func (x *MsgExecutePayload) GetSigner() string { - if x != nil { - return x.Signer - } - return "" -} - -func (x *MsgExecutePayload) GetUniversalAccountId() *UniversalAccountId { - if x != nil { - return x.UniversalAccountId - } - return nil -} - -func (x *MsgExecutePayload) GetUniversalPayload() *UniversalPayload { - if x != nil { - return x.UniversalPayload - } - return nil -} - -func (x *MsgExecutePayload) GetVerificationData() string { - if x != nil { - return x.VerificationData - } - return "" -} - -// MsgExecutePayloadResponse defines the response for MsgExecutePayload. -type MsgExecutePayloadResponse struct { +// MsgUpdateParamsResponse defines the response structure for executing a +// MsgUpdateParams message. +// +// Since: cosmos-sdk 0.47 +type MsgUpdateParamsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } -func (x *MsgExecutePayloadResponse) Reset() { - *x = MsgExecutePayloadResponse{} +func (x *MsgUpdateParamsResponse) Reset() { + *x = MsgUpdateParamsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[3] + mi := &file_uexecutor_v1_tx_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *MsgExecutePayloadResponse) String() string { +func (x *MsgUpdateParamsResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MsgExecutePayloadResponse) ProtoMessage() {} +func (*MsgUpdateParamsResponse) ProtoMessage() {} -// Deprecated: Use MsgExecutePayloadResponse.ProtoReflect.Descriptor instead. -func (*MsgExecutePayloadResponse) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{3} +// Deprecated: Use MsgUpdateParamsResponse.ProtoReflect.Descriptor instead. +func (*MsgUpdateParamsResponse) Descriptor() ([]byte, []int) { + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{1} } -// MsgMigrateUEA defines a message for migarting Universal Executor Account (UEA) -type MsgMigrateUEA struct { +// MsgExecutePayload defines a message for executing a universal payload +type MsgExecutePayload struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -6818,85 +7695,85 @@ type MsgMigrateUEA struct { Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"` // universal_account_id is the identifier of the owner account UniversalAccountId *UniversalAccountId `protobuf:"bytes,2,opt,name=universal_account_id,json=universalAccountId,proto3" json:"universal_account_id,omitempty"` - // payload is the migration payload to be executed - MigrationPayload *MigrationPayload `protobuf:"bytes,3,opt,name=migration_payload,json=migrationPayload,proto3" json:"migration_payload,omitempty"` - // signature is the bytes passed as verifier data for the given payload. - Signature string `protobuf:"bytes,4,opt,name=signature,proto3" json:"signature,omitempty"` + // payload is the universal payload to be executed + UniversalPayload *UniversalPayload `protobuf:"bytes,3,opt,name=universal_payload,json=universalPayload,proto3" json:"universal_payload,omitempty"` + // verification_data is the bytes passed as verifier data for the given payload. + VerificationData string `protobuf:"bytes,4,opt,name=verification_data,json=verificationData,proto3" json:"verification_data,omitempty"` } -func (x *MsgMigrateUEA) Reset() { - *x = MsgMigrateUEA{} +func (x *MsgExecutePayload) Reset() { + *x = MsgExecutePayload{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[4] + mi := &file_uexecutor_v1_tx_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *MsgMigrateUEA) String() string { +func (x *MsgExecutePayload) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MsgMigrateUEA) ProtoMessage() {} +func (*MsgExecutePayload) ProtoMessage() {} -// Deprecated: Use MsgMigrateUEA.ProtoReflect.Descriptor instead. -func (*MsgMigrateUEA) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{4} +// Deprecated: Use MsgExecutePayload.ProtoReflect.Descriptor instead. +func (*MsgExecutePayload) Descriptor() ([]byte, []int) { + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{2} } -func (x *MsgMigrateUEA) GetSigner() string { +func (x *MsgExecutePayload) GetSigner() string { if x != nil { return x.Signer } return "" } -func (x *MsgMigrateUEA) GetUniversalAccountId() *UniversalAccountId { +func (x *MsgExecutePayload) GetUniversalAccountId() *UniversalAccountId { if x != nil { return x.UniversalAccountId } return nil } -func (x *MsgMigrateUEA) GetMigrationPayload() *MigrationPayload { +func (x *MsgExecutePayload) GetUniversalPayload() *UniversalPayload { if x != nil { - return x.MigrationPayload + return x.UniversalPayload } return nil } -func (x *MsgMigrateUEA) GetSignature() string { +func (x *MsgExecutePayload) GetVerificationData() string { if x != nil { - return x.Signature + return x.VerificationData } return "" } -// MsgMigrateUEAResponse defines the response for MsgMigrateUEA. -type MsgMigrateUEAResponse struct { +// MsgExecutePayloadResponse defines the response for MsgExecutePayload. +type MsgExecutePayloadResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } -func (x *MsgMigrateUEAResponse) Reset() { - *x = MsgMigrateUEAResponse{} +func (x *MsgExecutePayloadResponse) Reset() { + *x = MsgExecutePayloadResponse{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[5] + mi := &file_uexecutor_v1_tx_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *MsgMigrateUEAResponse) String() string { +func (x *MsgExecutePayloadResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MsgMigrateUEAResponse) ProtoMessage() {} +func (*MsgExecutePayloadResponse) ProtoMessage() {} -// Deprecated: Use MsgMigrateUEAResponse.ProtoReflect.Descriptor instead. -func (*MsgMigrateUEAResponse) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{5} +// Deprecated: Use MsgExecutePayloadResponse.ProtoReflect.Descriptor instead. +func (*MsgExecutePayloadResponse) Descriptor() ([]byte, []int) { + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{3} } // MsgVoteInbound allows a universal validator to vote on an inbound transfer. @@ -6913,7 +7790,7 @@ type MsgVoteInbound struct { func (x *MsgVoteInbound) Reset() { *x = MsgVoteInbound{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[6] + mi := &file_uexecutor_v1_tx_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6927,7 +7804,7 @@ func (*MsgVoteInbound) ProtoMessage() {} // Deprecated: Use MsgVoteInbound.ProtoReflect.Descriptor instead. func (*MsgVoteInbound) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{6} + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{4} } func (x *MsgVoteInbound) GetSigner() string { @@ -6954,7 +7831,7 @@ type MsgVoteInboundResponse struct { func (x *MsgVoteInboundResponse) Reset() { *x = MsgVoteInboundResponse{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[7] + mi := &file_uexecutor_v1_tx_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6968,7 +7845,7 @@ func (*MsgVoteInboundResponse) ProtoMessage() {} // Deprecated: Use MsgVoteInboundResponse.ProtoReflect.Descriptor instead. func (*MsgVoteInboundResponse) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{7} + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{5} } // MsgVoteOutbound allows a universal validator to vote on an outbound tx observation. @@ -6987,7 +7864,7 @@ type MsgVoteOutbound struct { func (x *MsgVoteOutbound) Reset() { *x = MsgVoteOutbound{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[8] + mi := &file_uexecutor_v1_tx_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7001,7 +7878,7 @@ func (*MsgVoteOutbound) ProtoMessage() {} // Deprecated: Use MsgVoteOutbound.ProtoReflect.Descriptor instead. func (*MsgVoteOutbound) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{8} + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{6} } func (x *MsgVoteOutbound) GetSigner() string { @@ -7042,7 +7919,7 @@ type MsgVoteOutboundResponse struct { func (x *MsgVoteOutboundResponse) Reset() { *x = MsgVoteOutboundResponse{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[9] + mi := &file_uexecutor_v1_tx_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7056,7 +7933,7 @@ func (*MsgVoteOutboundResponse) ProtoMessage() {} // Deprecated: Use MsgVoteOutboundResponse.ProtoReflect.Descriptor instead. func (*MsgVoteOutboundResponse) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{9} + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{7} } // MsgVoteChainMeta is broadcasted by Universal Validators to submit observed chain metadata (gas price + block height) @@ -7074,7 +7951,7 @@ type MsgVoteChainMeta struct { func (x *MsgVoteChainMeta) Reset() { *x = MsgVoteChainMeta{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[10] + mi := &file_uexecutor_v1_tx_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7088,7 +7965,7 @@ func (*MsgVoteChainMeta) ProtoMessage() {} // Deprecated: Use MsgVoteChainMeta.ProtoReflect.Descriptor instead. func (*MsgVoteChainMeta) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{10} + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{8} } func (x *MsgVoteChainMeta) GetSigner() string { @@ -7129,7 +8006,7 @@ type MsgVoteChainMetaResponse struct { func (x *MsgVoteChainMetaResponse) Reset() { *x = MsgVoteChainMetaResponse{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[11] + mi := &file_uexecutor_v1_tx_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7143,7 +8020,7 @@ func (*MsgVoteChainMetaResponse) ProtoMessage() {} // Deprecated: Use MsgVoteChainMetaResponse.ProtoReflect.Descriptor instead. func (*MsgVoteChainMetaResponse) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{11} + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{9} } // MsgRevertStuckInbound is an admin escape hatch. For an inbound whose ballot @@ -7165,7 +8042,7 @@ type MsgRevertStuckInbound struct { func (x *MsgRevertStuckInbound) Reset() { *x = MsgRevertStuckInbound{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[12] + mi := &file_uexecutor_v1_tx_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7179,7 +8056,7 @@ func (*MsgRevertStuckInbound) ProtoMessage() {} // Deprecated: Use MsgRevertStuckInbound.ProtoReflect.Descriptor instead. func (*MsgRevertStuckInbound) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{12} + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{10} } func (x *MsgRevertStuckInbound) GetSigner() string { @@ -7208,7 +8085,7 @@ type MsgRevertStuckInboundResponse struct { func (x *MsgRevertStuckInboundResponse) Reset() { *x = MsgRevertStuckInboundResponse{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_tx_proto_msgTypes[13] + mi := &file_uexecutor_v1_tx_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7222,7 +8099,7 @@ func (*MsgRevertStuckInboundResponse) ProtoMessage() {} // Deprecated: Use MsgRevertStuckInboundResponse.ProtoReflect.Descriptor instead. func (*MsgRevertStuckInboundResponse) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{13} + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{11} } func (x *MsgRevertStuckInboundResponse) GetUtxId() string { @@ -7239,6 +8116,199 @@ func (x *MsgRevertStuckInboundResponse) GetOutboundId() string { return "" } +// MsgExecuteStuckInbound is an admin escape hatch and the sibling of +// MsgRevertStuckInbound. For an inbound whose ballot is stored PENDING but can +// never finalize on its own, and whose YES votes already meet the recomputed +// threshold, this marks the ballot PASSED and runs the same post-finalization +// pipeline a finalizing vote would have run - so the user receives the bridged +// funds on Push instead of a source-chain refund. +type MsgExecuteStuckInbound struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // signer must equal uvalidator Params.Admin + Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"` + // inbound is the original payload the stuck ballot was voting on. Admin + // supplies this from off-chain UV observation logs since the chain does not + // persist ballot payloads. + Inbound *Inbound `protobuf:"bytes,2,opt,name=inbound,proto3" json:"inbound,omitempty"` +} + +func (x *MsgExecuteStuckInbound) Reset() { + *x = MsgExecuteStuckInbound{} + if protoimpl.UnsafeEnabled { + mi := &file_uexecutor_v1_tx_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MsgExecuteStuckInbound) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MsgExecuteStuckInbound) ProtoMessage() {} + +// Deprecated: Use MsgExecuteStuckInbound.ProtoReflect.Descriptor instead. +func (*MsgExecuteStuckInbound) Descriptor() ([]byte, []int) { + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{12} +} + +func (x *MsgExecuteStuckInbound) GetSigner() string { + if x != nil { + return x.Signer + } + return "" +} + +func (x *MsgExecuteStuckInbound) GetInbound() *Inbound { + if x != nil { + return x.Inbound + } + return nil +} + +type MsgExecuteStuckInboundResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UtxId string `protobuf:"bytes,1,opt,name=utx_id,json=utxId,proto3" json:"utx_id,omitempty"` // ID of the UTX created for the executed inbound +} + +func (x *MsgExecuteStuckInboundResponse) Reset() { + *x = MsgExecuteStuckInboundResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_uexecutor_v1_tx_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MsgExecuteStuckInboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MsgExecuteStuckInboundResponse) ProtoMessage() {} + +// Deprecated: Use MsgExecuteStuckInboundResponse.ProtoReflect.Descriptor instead. +func (*MsgExecuteStuckInboundResponse) Descriptor() ([]byte, []int) { + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{13} +} + +func (x *MsgExecuteStuckInboundResponse) GetUtxId() string { + if x != nil { + return x.UtxId + } + return "" +} + +// MsgExecuteStuckOutbound is an admin escape hatch for an outbound whose ballot +// can no longer reach a terminal-and-settled state — EXPIRED, or PENDING with +// every eligible voter already voted and the YES votes at the stored threshold. +// It runs the same settlement pipeline a finalizing vote would have run, so the +// outcome follows observed_tx.success: a success settles, a failure mints the +// bridged tokens back to the revert recipient and refunds the excess gas. +type MsgExecuteStuckOutbound struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // signer must equal uvalidator Params.Admin + Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"` + TxId string `protobuf:"bytes,2,opt,name=tx_id,json=txId,proto3" json:"tx_id,omitempty"` // txId of outbound tx + UtxId string `protobuf:"bytes,3,opt,name=utx_id,json=utxId,proto3" json:"utx_id,omitempty"` // UniversalTx Id + // observed_tx is the destination-chain observation the stuck ballot was voting + // on. Admin supplies this from off-chain UV observation logs since the chain + // does not persist ballot payloads; it must match field-for-field or the + // derived ballot key finds no ballot. + ObservedTx *OutboundObservation `protobuf:"bytes,4,opt,name=observed_tx,json=observedTx,proto3" json:"observed_tx,omitempty"` +} + +func (x *MsgExecuteStuckOutbound) Reset() { + *x = MsgExecuteStuckOutbound{} + if protoimpl.UnsafeEnabled { + mi := &file_uexecutor_v1_tx_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MsgExecuteStuckOutbound) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MsgExecuteStuckOutbound) ProtoMessage() {} + +// Deprecated: Use MsgExecuteStuckOutbound.ProtoReflect.Descriptor instead. +func (*MsgExecuteStuckOutbound) Descriptor() ([]byte, []int) { + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{14} +} + +func (x *MsgExecuteStuckOutbound) GetSigner() string { + if x != nil { + return x.Signer + } + return "" +} + +func (x *MsgExecuteStuckOutbound) GetTxId() string { + if x != nil { + return x.TxId + } + return "" +} + +func (x *MsgExecuteStuckOutbound) GetUtxId() string { + if x != nil { + return x.UtxId + } + return "" +} + +func (x *MsgExecuteStuckOutbound) GetObservedTx() *OutboundObservation { + if x != nil { + return x.ObservedTx + } + return nil +} + +type MsgExecuteStuckOutboundResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + OutboundId string `protobuf:"bytes,1,opt,name=outbound_id,json=outboundId,proto3" json:"outbound_id,omitempty"` // ID of the outbound that was settled +} + +func (x *MsgExecuteStuckOutboundResponse) Reset() { + *x = MsgExecuteStuckOutboundResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_uexecutor_v1_tx_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MsgExecuteStuckOutboundResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MsgExecuteStuckOutboundResponse) ProtoMessage() {} + +// Deprecated: Use MsgExecuteStuckOutboundResponse.ProtoReflect.Descriptor instead. +func (*MsgExecuteStuckOutboundResponse) Descriptor() ([]byte, []int) { + return file_uexecutor_v1_tx_proto_rawDescGZIP(), []int{15} +} + +func (x *MsgExecuteStuckOutboundResponse) GetOutboundId() string { + if x != nil { + return x.OutboundId + } + return "" +} + var File_uexecutor_v1_tx_proto protoreflect.FileDescriptor var file_uexecutor_v1_tx_proto_rawDesc = []byte{ @@ -7288,137 +8358,159 @@ var file_uexecutor_v1_tx_proto_rawDesc = []byte{ 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x4d, 0x73, 0x67, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x1b, 0x0a, 0x19, 0x4d, 0x73, 0x67, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa9, 0x02, 0x0a, 0x0d, 0x4d, 0x73, 0x67, 0x4d, 0x69, 0x67, 0x72, - 0x61, 0x74, 0x65, 0x55, 0x45, 0x41, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, - 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, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x52, 0x0a, 0x14, 0x75, 0x6e, 0x69, 0x76, - 0x65, 0x72, 0x73, 0x61, 0x6c, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x41, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x52, 0x12, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, - 0x73, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x11, - 0x6d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, - 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x10, 0x6d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, - 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69, - 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x3a, 0x27, 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x69, - 0x67, 0x6e, 0x65, 0x72, 0x8a, 0xe7, 0xb0, 0x2a, 0x17, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x6f, 0x72, 0x2f, 0x4d, 0x73, 0x67, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x65, 0x55, 0x45, 0x41, - 0x22, 0x17, 0x0a, 0x15, 0x4d, 0x73, 0x67, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x65, 0x55, 0x45, - 0x41, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x96, 0x01, 0x0a, 0x0e, 0x4d, 0x73, - 0x67, 0x56, 0x6f, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x30, 0x0a, 0x06, - 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 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, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x2f, - 0x0a, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, - 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x3a, - 0x21, 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x8a, 0xe7, 0xb0, 0x2a, - 0x11, 0x75, 0x65, 0x2f, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, - 0x6e, 0x64, 0x22, 0x18, 0x0a, 0x16, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x49, 0x6e, 0x62, - 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xde, 0x01, 0x0a, - 0x0f, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x12, 0x30, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 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, 0x06, 0x73, 0x69, 0x67, 0x6e, - 0x65, 0x72, 0x12, 0x13, 0x0a, 0x05, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x74, 0x78, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x75, 0x74, 0x78, 0x5f, 0x69, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x75, 0x74, 0x78, 0x49, 0x64, 0x12, 0x42, - 0x0a, 0x0b, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x74, 0x78, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, - 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, - 0x54, 0x78, 0x3a, 0x29, 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x8a, - 0xe7, 0xb0, 0x2a, 0x19, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x4d, 0x73, - 0x67, 0x56, 0x6f, 0x74, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x19, 0x0a, - 0x17, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xd5, 0x01, 0x0a, 0x10, 0x4d, 0x73, 0x67, - 0x56, 0x6f, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x30, 0x0a, - 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 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, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, - 0x2a, 0x0a, 0x11, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x63, 0x68, 0x61, 0x69, - 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x62, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x70, - 0x72, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, - 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x48, 0x65, - 0x69, 0x67, 0x68, 0x74, 0x3a, 0x2a, 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, - 0x72, 0x8a, 0xe7, 0xb0, 0x2a, 0x1a, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x96, 0x01, 0x0a, 0x0e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, + 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, + 0x72, 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, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x2f, 0x0a, 0x07, 0x69, 0x6e, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x75, 0x65, 0x78, + 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x52, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x3a, 0x21, 0x82, 0xe7, 0xb0, 0x2a, + 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x8a, 0xe7, 0xb0, 0x2a, 0x11, 0x75, 0x65, 0x2f, 0x4d, + 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x18, 0x0a, + 0x16, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xde, 0x01, 0x0a, 0x0f, 0x4d, 0x73, 0x67, 0x56, + 0x6f, 0x74, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x30, 0x0a, 0x06, 0x73, + 0x69, 0x67, 0x6e, 0x65, 0x72, 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, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x13, 0x0a, + 0x05, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, + 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x75, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x75, 0x74, 0x78, 0x49, 0x64, 0x12, 0x42, 0x0a, 0x0b, 0x6f, 0x62, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x74, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, + 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, + 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x0a, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x54, 0x78, 0x3a, 0x29, 0x82, + 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x8a, 0xe7, 0xb0, 0x2a, 0x19, 0x75, + 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, + 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x19, 0x0a, 0x17, 0x4d, 0x73, 0x67, 0x56, + 0x6f, 0x74, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0xd5, 0x01, 0x0a, 0x10, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x43, + 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, + 0x65, 0x72, 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, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x11, 0x6f, 0x62, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x43, + 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x12, 0x21, 0x0a, 0x0c, + 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3a, + 0x2a, 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x8a, 0xe7, 0xb0, 0x2a, + 0x1a, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x4d, 0x73, 0x67, 0x56, 0x6f, + 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x22, 0x1a, 0x0a, 0x18, 0x4d, + 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x15, 0x4d, 0x73, 0x67, 0x52, + 0x65, 0x76, 0x65, 0x72, 0x74, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 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, 0x06, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x72, 0x12, 0x2f, 0x0a, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x07, 0x69, 0x6e, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x3a, 0x2f, 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, + 0x72, 0x8a, 0xe7, 0xb0, 0x2a, 0x1f, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, + 0x4d, 0x73, 0x67, 0x52, 0x65, 0x76, 0x65, 0x72, 0x74, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x49, 0x6e, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x57, 0x0a, 0x1d, 0x4d, 0x73, 0x67, 0x52, 0x65, 0x76, 0x65, + 0x72, 0x74, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x75, 0x74, 0x78, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x75, 0x74, 0x78, 0x49, 0x64, 0x12, 0x1f, 0x0a, + 0x0b, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x49, 0x64, 0x22, 0xad, + 0x01, 0x0a, 0x16, 0x4d, 0x73, 0x67, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x75, + 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x72, 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, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x2f, 0x0a, 0x07, 0x69, + 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x75, + 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x52, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x3a, 0x30, 0x82, 0xe7, + 0xb0, 0x2a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x8a, 0xe7, 0xb0, 0x2a, 0x20, 0x75, 0x65, + 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x4d, 0x73, 0x67, 0x45, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x65, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x37, + 0x0a, 0x1e, 0x4d, 0x73, 0x67, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x75, 0x63, + 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x15, 0x0a, 0x06, 0x75, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x75, 0x74, 0x78, 0x49, 0x64, 0x22, 0xee, 0x01, 0x0a, 0x17, 0x4d, 0x73, 0x67, 0x45, + 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x4f, 0x75, 0x74, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 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, 0x06, 0x73, + 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x13, 0x0a, 0x05, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x75, 0x74, + 0x78, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x75, 0x74, 0x78, 0x49, + 0x64, 0x12, 0x42, 0x0a, 0x0b, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x74, 0x78, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, + 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4f, 0x62, + 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x62, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x64, 0x54, 0x78, 0x3a, 0x31, 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x69, 0x67, 0x6e, + 0x65, 0x72, 0x8a, 0xe7, 0xb0, 0x2a, 0x21, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, + 0x2f, 0x4d, 0x73, 0x67, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x75, 0x63, 0x6b, + 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x42, 0x0a, 0x1f, 0x4d, 0x73, 0x67, 0x45, + 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x4f, 0x75, 0x74, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6f, + 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x49, 0x64, 0x32, 0x81, 0x06, 0x0a, + 0x03, 0x4d, 0x73, 0x67, 0x12, 0x54, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, + 0x72, 0x61, 0x6d, 0x73, 0x12, 0x1d, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, + 0x61, 0x6d, 0x73, 0x1a, 0x25, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, + 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0e, 0x45, 0x78, + 0x65, 0x63, 0x75, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x1f, 0x2e, 0x75, + 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x45, + 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x1a, 0x27, 0x2e, + 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, + 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0b, 0x56, 0x6f, 0x74, 0x65, 0x49, 0x6e, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x1c, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, + 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x1a, 0x24, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0c, 0x56, 0x6f, 0x74, + 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x1d, 0x2e, 0x75, 0x65, 0x78, 0x65, + 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, + 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x1a, 0x25, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x4f, + 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x57, 0x0a, 0x0d, 0x56, 0x6f, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, + 0x12, 0x1e, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, + 0x1a, 0x26, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, - 0x22, 0x1a, 0x0a, 0x18, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, - 0x4d, 0x65, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xab, 0x01, 0x0a, - 0x15, 0x4d, 0x73, 0x67, 0x52, 0x65, 0x76, 0x65, 0x72, 0x74, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x49, - 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, - 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, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x2f, 0x0a, 0x07, 0x69, 0x6e, 0x62, 0x6f, - 0x75, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x75, 0x65, 0x78, 0x65, - 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x52, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x3a, 0x2f, 0x82, 0xe7, 0xb0, 0x2a, 0x06, - 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x8a, 0xe7, 0xb0, 0x2a, 0x1f, 0x75, 0x65, 0x78, 0x65, 0x63, - 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x4d, 0x73, 0x67, 0x52, 0x65, 0x76, 0x65, 0x72, 0x74, 0x53, 0x74, - 0x75, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x57, 0x0a, 0x1d, 0x4d, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x12, 0x52, 0x65, 0x76, 0x65, + 0x72, 0x74, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x23, + 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x52, 0x65, 0x76, 0x65, 0x72, 0x74, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, - 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x75, - 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x75, 0x74, 0x78, - 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, - 0x64, 0x49, 0x64, 0x32, 0xf8, 0x04, 0x0a, 0x03, 0x4d, 0x73, 0x67, 0x12, 0x54, 0x0a, 0x0c, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x1d, 0x2e, 0x75, 0x65, - 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x25, 0x2e, 0x75, 0x65, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x5a, 0x0a, 0x0e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6c, - 0x6f, 0x61, 0x64, 0x12, 0x1f, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x50, 0x61, 0x79, - 0x6c, 0x6f, 0x61, 0x64, 0x1a, 0x27, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, - 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x50, 0x61, - 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, - 0x0a, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x65, 0x55, 0x45, 0x41, 0x12, 0x1b, 0x2e, 0x75, 0x65, - 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x4d, 0x69, - 0x67, 0x72, 0x61, 0x74, 0x65, 0x55, 0x45, 0x41, 0x1a, 0x23, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, - 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x4d, 0x69, 0x67, 0x72, 0x61, - 0x74, 0x65, 0x55, 0x45, 0x41, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, - 0x0b, 0x56, 0x6f, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x1c, 0x2e, 0x75, - 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, - 0x6f, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x1a, 0x24, 0x2e, 0x75, 0x65, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, - 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x54, 0x0a, 0x0c, 0x56, 0x6f, 0x74, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x12, 0x1d, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x1a, - 0x25, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, - 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0d, 0x56, 0x6f, 0x74, 0x65, 0x43, 0x68, - 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1e, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, - 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x43, 0x68, - 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x26, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, - 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x43, 0x68, - 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x66, 0x0a, 0x12, 0x52, 0x65, 0x76, 0x65, 0x72, 0x74, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x49, 0x6e, - 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x23, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, - 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x52, 0x65, 0x76, 0x65, 0x72, 0x74, 0x53, 0x74, - 0x75, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x1a, 0x2b, 0x2e, 0x75, 0x65, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x52, 0x65, 0x76, - 0x65, 0x72, 0x74, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x05, 0x80, 0xe7, 0xb0, 0x2a, 0x01, 0x42, 0xaf, - 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, - 0x2e, 0x76, 0x31, 0x42, 0x07, 0x54, 0x78, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x41, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x75, 0x73, 0x68, 0x63, - 0x68, 0x61, 0x69, 0x6e, 0x2f, 0x70, 0x75, 0x73, 0x68, 0x2d, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2d, - 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x6f, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x76, - 0x31, 0xa2, 0x02, 0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x0c, 0x55, 0x65, 0x78, 0x65, 0x63, 0x75, - 0x74, 0x6f, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0c, 0x55, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x6f, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x18, 0x55, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, - 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0xea, 0x02, 0x0d, 0x55, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x3a, 0x3a, 0x56, 0x31, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x75, 0x6e, 0x64, 0x1a, 0x2b, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x52, 0x65, 0x76, 0x65, 0x72, 0x74, 0x53, 0x74, 0x75, 0x63, + 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x69, 0x0a, 0x13, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x75, 0x63, 0x6b, + 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x24, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, + 0x65, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x1a, 0x2c, 0x2e, + 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, + 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x14, 0x45, + 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x4f, 0x75, 0x74, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x12, 0x25, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x75, + 0x63, 0x6b, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x1a, 0x2d, 0x2e, 0x75, 0x65, 0x78, + 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x45, 0x78, 0x65, + 0x63, 0x75, 0x74, 0x65, 0x53, 0x74, 0x75, 0x63, 0x6b, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x05, 0x80, 0xe7, 0xb0, 0x2a, 0x01, + 0x42, 0xaf, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, + 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x42, 0x07, 0x54, 0x78, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x75, 0x73, + 0x68, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2f, 0x70, 0x75, 0x73, 0x68, 0x2d, 0x63, 0x68, 0x61, 0x69, + 0x6e, 0x2d, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x65, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, + 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x0c, 0x55, 0x65, 0x78, 0x65, + 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0c, 0x55, 0x65, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x6f, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x18, 0x55, 0x65, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x6f, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0xea, 0x02, 0x0d, 0x55, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x3a, 0x3a, + 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -7433,54 +8525,57 @@ func file_uexecutor_v1_tx_proto_rawDescGZIP() []byte { return file_uexecutor_v1_tx_proto_rawDescData } -var file_uexecutor_v1_tx_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_uexecutor_v1_tx_proto_msgTypes = make([]protoimpl.MessageInfo, 16) var file_uexecutor_v1_tx_proto_goTypes = []interface{}{ - (*MsgUpdateParams)(nil), // 0: uexecutor.v1.MsgUpdateParams - (*MsgUpdateParamsResponse)(nil), // 1: uexecutor.v1.MsgUpdateParamsResponse - (*MsgExecutePayload)(nil), // 2: uexecutor.v1.MsgExecutePayload - (*MsgExecutePayloadResponse)(nil), // 3: uexecutor.v1.MsgExecutePayloadResponse - (*MsgMigrateUEA)(nil), // 4: uexecutor.v1.MsgMigrateUEA - (*MsgMigrateUEAResponse)(nil), // 5: uexecutor.v1.MsgMigrateUEAResponse - (*MsgVoteInbound)(nil), // 6: uexecutor.v1.MsgVoteInbound - (*MsgVoteInboundResponse)(nil), // 7: uexecutor.v1.MsgVoteInboundResponse - (*MsgVoteOutbound)(nil), // 8: uexecutor.v1.MsgVoteOutbound - (*MsgVoteOutboundResponse)(nil), // 9: uexecutor.v1.MsgVoteOutboundResponse - (*MsgVoteChainMeta)(nil), // 10: uexecutor.v1.MsgVoteChainMeta - (*MsgVoteChainMetaResponse)(nil), // 11: uexecutor.v1.MsgVoteChainMetaResponse - (*MsgRevertStuckInbound)(nil), // 12: uexecutor.v1.MsgRevertStuckInbound - (*MsgRevertStuckInboundResponse)(nil), // 13: uexecutor.v1.MsgRevertStuckInboundResponse - (*Params)(nil), // 14: uexecutor.v1.Params - (*UniversalAccountId)(nil), // 15: uexecutor.v1.UniversalAccountId - (*UniversalPayload)(nil), // 16: uexecutor.v1.UniversalPayload - (*MigrationPayload)(nil), // 17: uexecutor.v1.MigrationPayload - (*Inbound)(nil), // 18: uexecutor.v1.Inbound - (*OutboundObservation)(nil), // 19: uexecutor.v1.OutboundObservation + (*MsgUpdateParams)(nil), // 0: uexecutor.v1.MsgUpdateParams + (*MsgUpdateParamsResponse)(nil), // 1: uexecutor.v1.MsgUpdateParamsResponse + (*MsgExecutePayload)(nil), // 2: uexecutor.v1.MsgExecutePayload + (*MsgExecutePayloadResponse)(nil), // 3: uexecutor.v1.MsgExecutePayloadResponse + (*MsgVoteInbound)(nil), // 4: uexecutor.v1.MsgVoteInbound + (*MsgVoteInboundResponse)(nil), // 5: uexecutor.v1.MsgVoteInboundResponse + (*MsgVoteOutbound)(nil), // 6: uexecutor.v1.MsgVoteOutbound + (*MsgVoteOutboundResponse)(nil), // 7: uexecutor.v1.MsgVoteOutboundResponse + (*MsgVoteChainMeta)(nil), // 8: uexecutor.v1.MsgVoteChainMeta + (*MsgVoteChainMetaResponse)(nil), // 9: uexecutor.v1.MsgVoteChainMetaResponse + (*MsgRevertStuckInbound)(nil), // 10: uexecutor.v1.MsgRevertStuckInbound + (*MsgRevertStuckInboundResponse)(nil), // 11: uexecutor.v1.MsgRevertStuckInboundResponse + (*MsgExecuteStuckInbound)(nil), // 12: uexecutor.v1.MsgExecuteStuckInbound + (*MsgExecuteStuckInboundResponse)(nil), // 13: uexecutor.v1.MsgExecuteStuckInboundResponse + (*MsgExecuteStuckOutbound)(nil), // 14: uexecutor.v1.MsgExecuteStuckOutbound + (*MsgExecuteStuckOutboundResponse)(nil), // 15: uexecutor.v1.MsgExecuteStuckOutboundResponse + (*Params)(nil), // 16: uexecutor.v1.Params + (*UniversalAccountId)(nil), // 17: uexecutor.v1.UniversalAccountId + (*UniversalPayload)(nil), // 18: uexecutor.v1.UniversalPayload + (*Inbound)(nil), // 19: uexecutor.v1.Inbound + (*OutboundObservation)(nil), // 20: uexecutor.v1.OutboundObservation } var file_uexecutor_v1_tx_proto_depIdxs = []int32{ - 14, // 0: uexecutor.v1.MsgUpdateParams.params:type_name -> uexecutor.v1.Params - 15, // 1: uexecutor.v1.MsgExecutePayload.universal_account_id:type_name -> uexecutor.v1.UniversalAccountId - 16, // 2: uexecutor.v1.MsgExecutePayload.universal_payload:type_name -> uexecutor.v1.UniversalPayload - 15, // 3: uexecutor.v1.MsgMigrateUEA.universal_account_id:type_name -> uexecutor.v1.UniversalAccountId - 17, // 4: uexecutor.v1.MsgMigrateUEA.migration_payload:type_name -> uexecutor.v1.MigrationPayload - 18, // 5: uexecutor.v1.MsgVoteInbound.inbound:type_name -> uexecutor.v1.Inbound - 19, // 6: uexecutor.v1.MsgVoteOutbound.observed_tx:type_name -> uexecutor.v1.OutboundObservation - 18, // 7: uexecutor.v1.MsgRevertStuckInbound.inbound:type_name -> uexecutor.v1.Inbound + 16, // 0: uexecutor.v1.MsgUpdateParams.params:type_name -> uexecutor.v1.Params + 17, // 1: uexecutor.v1.MsgExecutePayload.universal_account_id:type_name -> uexecutor.v1.UniversalAccountId + 18, // 2: uexecutor.v1.MsgExecutePayload.universal_payload:type_name -> uexecutor.v1.UniversalPayload + 19, // 3: uexecutor.v1.MsgVoteInbound.inbound:type_name -> uexecutor.v1.Inbound + 20, // 4: uexecutor.v1.MsgVoteOutbound.observed_tx:type_name -> uexecutor.v1.OutboundObservation + 19, // 5: uexecutor.v1.MsgRevertStuckInbound.inbound:type_name -> uexecutor.v1.Inbound + 19, // 6: uexecutor.v1.MsgExecuteStuckInbound.inbound:type_name -> uexecutor.v1.Inbound + 20, // 7: uexecutor.v1.MsgExecuteStuckOutbound.observed_tx:type_name -> uexecutor.v1.OutboundObservation 0, // 8: uexecutor.v1.Msg.UpdateParams:input_type -> uexecutor.v1.MsgUpdateParams 2, // 9: uexecutor.v1.Msg.ExecutePayload:input_type -> uexecutor.v1.MsgExecutePayload - 4, // 10: uexecutor.v1.Msg.MigrateUEA:input_type -> uexecutor.v1.MsgMigrateUEA - 6, // 11: uexecutor.v1.Msg.VoteInbound:input_type -> uexecutor.v1.MsgVoteInbound - 8, // 12: uexecutor.v1.Msg.VoteOutbound:input_type -> uexecutor.v1.MsgVoteOutbound - 10, // 13: uexecutor.v1.Msg.VoteChainMeta:input_type -> uexecutor.v1.MsgVoteChainMeta - 12, // 14: uexecutor.v1.Msg.RevertStuckInbound:input_type -> uexecutor.v1.MsgRevertStuckInbound - 1, // 15: uexecutor.v1.Msg.UpdateParams:output_type -> uexecutor.v1.MsgUpdateParamsResponse - 3, // 16: uexecutor.v1.Msg.ExecutePayload:output_type -> uexecutor.v1.MsgExecutePayloadResponse - 5, // 17: uexecutor.v1.Msg.MigrateUEA:output_type -> uexecutor.v1.MsgMigrateUEAResponse - 7, // 18: uexecutor.v1.Msg.VoteInbound:output_type -> uexecutor.v1.MsgVoteInboundResponse - 9, // 19: uexecutor.v1.Msg.VoteOutbound:output_type -> uexecutor.v1.MsgVoteOutboundResponse - 11, // 20: uexecutor.v1.Msg.VoteChainMeta:output_type -> uexecutor.v1.MsgVoteChainMetaResponse - 13, // 21: uexecutor.v1.Msg.RevertStuckInbound:output_type -> uexecutor.v1.MsgRevertStuckInboundResponse - 15, // [15:22] is the sub-list for method output_type - 8, // [8:15] is the sub-list for method input_type + 4, // 10: uexecutor.v1.Msg.VoteInbound:input_type -> uexecutor.v1.MsgVoteInbound + 6, // 11: uexecutor.v1.Msg.VoteOutbound:input_type -> uexecutor.v1.MsgVoteOutbound + 8, // 12: uexecutor.v1.Msg.VoteChainMeta:input_type -> uexecutor.v1.MsgVoteChainMeta + 10, // 13: uexecutor.v1.Msg.RevertStuckInbound:input_type -> uexecutor.v1.MsgRevertStuckInbound + 12, // 14: uexecutor.v1.Msg.ExecuteStuckInbound:input_type -> uexecutor.v1.MsgExecuteStuckInbound + 14, // 15: uexecutor.v1.Msg.ExecuteStuckOutbound:input_type -> uexecutor.v1.MsgExecuteStuckOutbound + 1, // 16: uexecutor.v1.Msg.UpdateParams:output_type -> uexecutor.v1.MsgUpdateParamsResponse + 3, // 17: uexecutor.v1.Msg.ExecutePayload:output_type -> uexecutor.v1.MsgExecutePayloadResponse + 5, // 18: uexecutor.v1.Msg.VoteInbound:output_type -> uexecutor.v1.MsgVoteInboundResponse + 7, // 19: uexecutor.v1.Msg.VoteOutbound:output_type -> uexecutor.v1.MsgVoteOutboundResponse + 9, // 20: uexecutor.v1.Msg.VoteChainMeta:output_type -> uexecutor.v1.MsgVoteChainMetaResponse + 11, // 21: uexecutor.v1.Msg.RevertStuckInbound:output_type -> uexecutor.v1.MsgRevertStuckInboundResponse + 13, // 22: uexecutor.v1.Msg.ExecuteStuckInbound:output_type -> uexecutor.v1.MsgExecuteStuckInboundResponse + 15, // 23: uexecutor.v1.Msg.ExecuteStuckOutbound:output_type -> uexecutor.v1.MsgExecuteStuckOutboundResponse + 16, // [16:24] is the sub-list for method output_type + 8, // [8:16] is the sub-list for method input_type 8, // [8:8] is the sub-list for extension type_name 8, // [8:8] is the sub-list for extension extendee 0, // [0:8] is the sub-list for field type_name @@ -7543,7 +8638,7 @@ func file_uexecutor_v1_tx_proto_init() { } } file_uexecutor_v1_tx_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MsgMigrateUEA); i { + switch v := v.(*MsgVoteInbound); i { case 0: return &v.state case 1: @@ -7555,7 +8650,7 @@ func file_uexecutor_v1_tx_proto_init() { } } file_uexecutor_v1_tx_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MsgMigrateUEAResponse); i { + switch v := v.(*MsgVoteInboundResponse); i { case 0: return &v.state case 1: @@ -7567,7 +8662,7 @@ func file_uexecutor_v1_tx_proto_init() { } } file_uexecutor_v1_tx_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MsgVoteInbound); i { + switch v := v.(*MsgVoteOutbound); i { case 0: return &v.state case 1: @@ -7579,7 +8674,7 @@ func file_uexecutor_v1_tx_proto_init() { } } file_uexecutor_v1_tx_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MsgVoteInboundResponse); i { + switch v := v.(*MsgVoteOutboundResponse); i { case 0: return &v.state case 1: @@ -7591,7 +8686,7 @@ func file_uexecutor_v1_tx_proto_init() { } } file_uexecutor_v1_tx_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MsgVoteOutbound); i { + switch v := v.(*MsgVoteChainMeta); i { case 0: return &v.state case 1: @@ -7603,7 +8698,7 @@ func file_uexecutor_v1_tx_proto_init() { } } file_uexecutor_v1_tx_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MsgVoteOutboundResponse); i { + switch v := v.(*MsgVoteChainMetaResponse); i { case 0: return &v.state case 1: @@ -7615,7 +8710,7 @@ func file_uexecutor_v1_tx_proto_init() { } } file_uexecutor_v1_tx_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MsgVoteChainMeta); i { + switch v := v.(*MsgRevertStuckInbound); i { case 0: return &v.state case 1: @@ -7627,7 +8722,7 @@ func file_uexecutor_v1_tx_proto_init() { } } file_uexecutor_v1_tx_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MsgVoteChainMetaResponse); i { + switch v := v.(*MsgRevertStuckInboundResponse); i { case 0: return &v.state case 1: @@ -7639,7 +8734,7 @@ func file_uexecutor_v1_tx_proto_init() { } } file_uexecutor_v1_tx_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MsgRevertStuckInbound); i { + switch v := v.(*MsgExecuteStuckInbound); i { case 0: return &v.state case 1: @@ -7651,7 +8746,31 @@ func file_uexecutor_v1_tx_proto_init() { } } file_uexecutor_v1_tx_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MsgRevertStuckInboundResponse); i { + switch v := v.(*MsgExecuteStuckInboundResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_uexecutor_v1_tx_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MsgExecuteStuckOutbound); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_uexecutor_v1_tx_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MsgExecuteStuckOutboundResponse); i { case 0: return &v.state case 1: @@ -7669,7 +8788,7 @@ func file_uexecutor_v1_tx_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_uexecutor_v1_tx_proto_rawDesc, NumEnums: 0, - NumMessages: 14, + NumMessages: 16, NumExtensions: 0, NumServices: 1, }, diff --git a/api/uexecutor/v1/tx_grpc.pb.go b/api/uexecutor/v1/tx_grpc.pb.go index 9959cb728..cefd4ab8f 100644 --- a/api/uexecutor/v1/tx_grpc.pb.go +++ b/api/uexecutor/v1/tx_grpc.pb.go @@ -19,13 +19,14 @@ import ( const _ = grpc.SupportPackageIsVersion7 const ( - Msg_UpdateParams_FullMethodName = "/uexecutor.v1.Msg/UpdateParams" - Msg_ExecutePayload_FullMethodName = "/uexecutor.v1.Msg/ExecutePayload" - Msg_MigrateUEA_FullMethodName = "/uexecutor.v1.Msg/MigrateUEA" - Msg_VoteInbound_FullMethodName = "/uexecutor.v1.Msg/VoteInbound" - Msg_VoteOutbound_FullMethodName = "/uexecutor.v1.Msg/VoteOutbound" - Msg_VoteChainMeta_FullMethodName = "/uexecutor.v1.Msg/VoteChainMeta" - Msg_RevertStuckInbound_FullMethodName = "/uexecutor.v1.Msg/RevertStuckInbound" + Msg_UpdateParams_FullMethodName = "/uexecutor.v1.Msg/UpdateParams" + Msg_ExecutePayload_FullMethodName = "/uexecutor.v1.Msg/ExecutePayload" + Msg_VoteInbound_FullMethodName = "/uexecutor.v1.Msg/VoteInbound" + Msg_VoteOutbound_FullMethodName = "/uexecutor.v1.Msg/VoteOutbound" + Msg_VoteChainMeta_FullMethodName = "/uexecutor.v1.Msg/VoteChainMeta" + Msg_RevertStuckInbound_FullMethodName = "/uexecutor.v1.Msg/RevertStuckInbound" + Msg_ExecuteStuckInbound_FullMethodName = "/uexecutor.v1.Msg/ExecuteStuckInbound" + Msg_ExecuteStuckOutbound_FullMethodName = "/uexecutor.v1.Msg/ExecuteStuckOutbound" ) // MsgClient is the client API for Msg service. @@ -38,8 +39,6 @@ type MsgClient interface { UpdateParams(ctx context.Context, in *MsgUpdateParams, opts ...grpc.CallOption) (*MsgUpdateParamsResponse, error) // ExecutePayload defines a message for executing a universal payload ExecutePayload(ctx context.Context, in *MsgExecutePayload, opts ...grpc.CallOption) (*MsgExecutePayloadResponse, error) - // MigrateUEA defines a message for migrating UEA - MigrateUEA(ctx context.Context, in *MsgMigrateUEA, opts ...grpc.CallOption) (*MsgMigrateUEAResponse, error) // VoteInbound defines a message for voting on synthetic assets bridging from external chain to PC VoteInbound(ctx context.Context, in *MsgVoteInbound, opts ...grpc.CallOption) (*MsgVoteInboundResponse, error) // VoteOutbound defines a message for voting on a observed outbound tx on external chain @@ -50,6 +49,15 @@ type MsgClient interface { // ballot has expired without finalizing, refunding the user on the source // chain via the normal revert/outbound flow. Admin-only escape hatch. RevertStuckInbound(ctx context.Context, in *MsgRevertStuckInbound, opts ...grpc.CallOption) (*MsgRevertStuckInboundResponse, error) + // ExecuteStuckInbound finalizes an inbound ballot that is provably unable to + // finalize on its own yet already carries enough YES votes, then runs the + // normal post-finalization pipeline so the user receives funds on Push. + // Admin-only escape hatch, sibling of RevertStuckInbound. + ExecuteStuckInbound(ctx context.Context, in *MsgExecuteStuckInbound, opts ...grpc.CallOption) (*MsgExecuteStuckInboundResponse, error) + // ExecuteStuckOutbound settles an outbound whose ballot can no longer reach a + // terminal-and-settled state, running the same post-finalization pipeline a + // finalizing vote would have run. Admin-only escape hatch. + ExecuteStuckOutbound(ctx context.Context, in *MsgExecuteStuckOutbound, opts ...grpc.CallOption) (*MsgExecuteStuckOutboundResponse, error) } type msgClient struct { @@ -78,15 +86,6 @@ func (c *msgClient) ExecutePayload(ctx context.Context, in *MsgExecutePayload, o return out, nil } -func (c *msgClient) MigrateUEA(ctx context.Context, in *MsgMigrateUEA, opts ...grpc.CallOption) (*MsgMigrateUEAResponse, error) { - out := new(MsgMigrateUEAResponse) - err := c.cc.Invoke(ctx, Msg_MigrateUEA_FullMethodName, in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *msgClient) VoteInbound(ctx context.Context, in *MsgVoteInbound, opts ...grpc.CallOption) (*MsgVoteInboundResponse, error) { out := new(MsgVoteInboundResponse) err := c.cc.Invoke(ctx, Msg_VoteInbound_FullMethodName, in, out, opts...) @@ -123,6 +122,24 @@ func (c *msgClient) RevertStuckInbound(ctx context.Context, in *MsgRevertStuckIn return out, nil } +func (c *msgClient) ExecuteStuckInbound(ctx context.Context, in *MsgExecuteStuckInbound, opts ...grpc.CallOption) (*MsgExecuteStuckInboundResponse, error) { + out := new(MsgExecuteStuckInboundResponse) + err := c.cc.Invoke(ctx, Msg_ExecuteStuckInbound_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *msgClient) ExecuteStuckOutbound(ctx context.Context, in *MsgExecuteStuckOutbound, opts ...grpc.CallOption) (*MsgExecuteStuckOutboundResponse, error) { + out := new(MsgExecuteStuckOutboundResponse) + err := c.cc.Invoke(ctx, Msg_ExecuteStuckOutbound_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // MsgServer is the server API for Msg service. // All implementations must embed UnimplementedMsgServer // for forward compatibility @@ -133,8 +150,6 @@ type MsgServer interface { UpdateParams(context.Context, *MsgUpdateParams) (*MsgUpdateParamsResponse, error) // ExecutePayload defines a message for executing a universal payload ExecutePayload(context.Context, *MsgExecutePayload) (*MsgExecutePayloadResponse, error) - // MigrateUEA defines a message for migrating UEA - MigrateUEA(context.Context, *MsgMigrateUEA) (*MsgMigrateUEAResponse, error) // VoteInbound defines a message for voting on synthetic assets bridging from external chain to PC VoteInbound(context.Context, *MsgVoteInbound) (*MsgVoteInboundResponse, error) // VoteOutbound defines a message for voting on a observed outbound tx on external chain @@ -145,6 +160,15 @@ type MsgServer interface { // ballot has expired without finalizing, refunding the user on the source // chain via the normal revert/outbound flow. Admin-only escape hatch. RevertStuckInbound(context.Context, *MsgRevertStuckInbound) (*MsgRevertStuckInboundResponse, error) + // ExecuteStuckInbound finalizes an inbound ballot that is provably unable to + // finalize on its own yet already carries enough YES votes, then runs the + // normal post-finalization pipeline so the user receives funds on Push. + // Admin-only escape hatch, sibling of RevertStuckInbound. + ExecuteStuckInbound(context.Context, *MsgExecuteStuckInbound) (*MsgExecuteStuckInboundResponse, error) + // ExecuteStuckOutbound settles an outbound whose ballot can no longer reach a + // terminal-and-settled state, running the same post-finalization pipeline a + // finalizing vote would have run. Admin-only escape hatch. + ExecuteStuckOutbound(context.Context, *MsgExecuteStuckOutbound) (*MsgExecuteStuckOutboundResponse, error) mustEmbedUnimplementedMsgServer() } @@ -158,9 +182,6 @@ func (UnimplementedMsgServer) UpdateParams(context.Context, *MsgUpdateParams) (* func (UnimplementedMsgServer) ExecutePayload(context.Context, *MsgExecutePayload) (*MsgExecutePayloadResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ExecutePayload not implemented") } -func (UnimplementedMsgServer) MigrateUEA(context.Context, *MsgMigrateUEA) (*MsgMigrateUEAResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method MigrateUEA not implemented") -} func (UnimplementedMsgServer) VoteInbound(context.Context, *MsgVoteInbound) (*MsgVoteInboundResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method VoteInbound not implemented") } @@ -173,6 +194,12 @@ func (UnimplementedMsgServer) VoteChainMeta(context.Context, *MsgVoteChainMeta) func (UnimplementedMsgServer) RevertStuckInbound(context.Context, *MsgRevertStuckInbound) (*MsgRevertStuckInboundResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RevertStuckInbound not implemented") } +func (UnimplementedMsgServer) ExecuteStuckInbound(context.Context, *MsgExecuteStuckInbound) (*MsgExecuteStuckInboundResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ExecuteStuckInbound not implemented") +} +func (UnimplementedMsgServer) ExecuteStuckOutbound(context.Context, *MsgExecuteStuckOutbound) (*MsgExecuteStuckOutboundResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ExecuteStuckOutbound not implemented") +} func (UnimplementedMsgServer) mustEmbedUnimplementedMsgServer() {} // UnsafeMsgServer may be embedded to opt out of forward compatibility for this service. @@ -222,24 +249,6 @@ func _Msg_ExecutePayload_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } -func _Msg_MigrateUEA_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(MsgMigrateUEA) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(MsgServer).MigrateUEA(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Msg_MigrateUEA_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(MsgServer).MigrateUEA(ctx, req.(*MsgMigrateUEA)) - } - return interceptor(ctx, in, info, handler) -} - func _Msg_VoteInbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(MsgVoteInbound) if err := dec(in); err != nil { @@ -312,6 +321,42 @@ func _Msg_RevertStuckInbound_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _Msg_ExecuteStuckInbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgExecuteStuckInbound) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServer).ExecuteStuckInbound(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Msg_ExecuteStuckInbound_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServer).ExecuteStuckInbound(ctx, req.(*MsgExecuteStuckInbound)) + } + return interceptor(ctx, in, info, handler) +} + +func _Msg_ExecuteStuckOutbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgExecuteStuckOutbound) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServer).ExecuteStuckOutbound(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Msg_ExecuteStuckOutbound_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServer).ExecuteStuckOutbound(ctx, req.(*MsgExecuteStuckOutbound)) + } + return interceptor(ctx, in, info, handler) +} + // Msg_ServiceDesc is the grpc.ServiceDesc for Msg service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -327,10 +372,6 @@ var Msg_ServiceDesc = grpc.ServiceDesc{ MethodName: "ExecutePayload", Handler: _Msg_ExecutePayload_Handler, }, - { - MethodName: "MigrateUEA", - Handler: _Msg_MigrateUEA_Handler, - }, { MethodName: "VoteInbound", Handler: _Msg_VoteInbound_Handler, @@ -347,6 +388,14 @@ var Msg_ServiceDesc = grpc.ServiceDesc{ MethodName: "RevertStuckInbound", Handler: _Msg_RevertStuckInbound_Handler, }, + { + MethodName: "ExecuteStuckInbound", + Handler: _Msg_ExecuteStuckInbound_Handler, + }, + { + MethodName: "ExecuteStuckOutbound", + Handler: _Msg_ExecuteStuckOutbound_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "uexecutor/v1/tx.proto", diff --git a/api/uexecutor/v1/types.pulsar.go b/api/uexecutor/v1/types.pulsar.go index 197b2fb9c..f35e1bb9f 100644 --- a/api/uexecutor/v1/types.pulsar.go +++ b/api/uexecutor/v1/types.pulsar.go @@ -15,14 +15,16 @@ import ( ) var ( - md_Params protoreflect.MessageDescriptor - fd_Params_some_value protoreflect.FieldDescriptor + md_Params protoreflect.MessageDescriptor + fd_Params_some_value protoreflect.FieldDescriptor + fd_Params_max_gasless_tx_gas protoreflect.FieldDescriptor ) func init() { file_uexecutor_v1_types_proto_init() md_Params = File_uexecutor_v1_types_proto.Messages().ByName("Params") fd_Params_some_value = md_Params.Fields().ByName("some_value") + fd_Params_max_gasless_tx_gas = md_Params.Fields().ByName("max_gasless_tx_gas") } var _ protoreflect.Message = (*fastReflection_Params)(nil) @@ -96,6 +98,12 @@ func (x *fastReflection_Params) Range(f func(protoreflect.FieldDescriptor, proto return } } + if x.MaxGaslessTxGas != uint64(0) { + value := protoreflect.ValueOfUint64(x.MaxGaslessTxGas) + if !f(fd_Params_max_gasless_tx_gas, value) { + return + } + } } // Has reports whether a field is populated. @@ -113,6 +121,8 @@ func (x *fastReflection_Params) Has(fd protoreflect.FieldDescriptor) bool { switch fd.FullName() { case "uexecutor.v1.Params.some_value": return x.SomeValue != false + case "uexecutor.v1.Params.max_gasless_tx_gas": + return x.MaxGaslessTxGas != uint64(0) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.Params")) @@ -131,6 +141,8 @@ func (x *fastReflection_Params) Clear(fd protoreflect.FieldDescriptor) { switch fd.FullName() { case "uexecutor.v1.Params.some_value": x.SomeValue = false + case "uexecutor.v1.Params.max_gasless_tx_gas": + x.MaxGaslessTxGas = uint64(0) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.Params")) @@ -150,6 +162,9 @@ func (x *fastReflection_Params) Get(descriptor protoreflect.FieldDescriptor) pro case "uexecutor.v1.Params.some_value": value := x.SomeValue return protoreflect.ValueOfBool(value) + case "uexecutor.v1.Params.max_gasless_tx_gas": + value := x.MaxGaslessTxGas + return protoreflect.ValueOfUint64(value) default: if descriptor.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.Params")) @@ -172,6 +187,8 @@ func (x *fastReflection_Params) Set(fd protoreflect.FieldDescriptor, value proto switch fd.FullName() { case "uexecutor.v1.Params.some_value": x.SomeValue = value.Bool() + case "uexecutor.v1.Params.max_gasless_tx_gas": + x.MaxGaslessTxGas = value.Uint() default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.Params")) @@ -194,6 +211,8 @@ func (x *fastReflection_Params) Mutable(fd protoreflect.FieldDescriptor) protore switch fd.FullName() { case "uexecutor.v1.Params.some_value": panic(fmt.Errorf("field some_value of message uexecutor.v1.Params is not mutable")) + case "uexecutor.v1.Params.max_gasless_tx_gas": + panic(fmt.Errorf("field max_gasless_tx_gas of message uexecutor.v1.Params is not mutable")) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.Params")) @@ -209,6 +228,8 @@ func (x *fastReflection_Params) NewField(fd protoreflect.FieldDescriptor) protor switch fd.FullName() { case "uexecutor.v1.Params.some_value": return protoreflect.ValueOfBool(false) + case "uexecutor.v1.Params.max_gasless_tx_gas": + return protoreflect.ValueOfUint64(uint64(0)) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.Params")) @@ -281,6 +302,9 @@ func (x *fastReflection_Params) ProtoMethods() *protoiface.Methods { if x.SomeValue { n += 2 } + if x.MaxGaslessTxGas != 0 { + n += 1 + runtime.Sov(uint64(x.MaxGaslessTxGas)) + } if x.unknownFields != nil { n += len(x.unknownFields) } @@ -310,6 +334,11 @@ func (x *fastReflection_Params) ProtoMethods() *protoiface.Methods { i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } + if x.MaxGaslessTxGas != 0 { + i = runtime.EncodeVarint(dAtA, i, uint64(x.MaxGaslessTxGas)) + i-- + dAtA[i] = 0x18 + } if x.SomeValue { i-- if x.SomeValue { @@ -389,6 +418,25 @@ func (x *fastReflection_Params) ProtoMethods() *protoiface.Methods { } } x.SomeValue = bool(v != 0) + case 3: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field MaxGaslessTxGas", wireType) + } + x.MaxGaslessTxGas = 0 + 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++ + x.MaxGaslessTxGas |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -1106,660 +1154,61 @@ func (x *fastReflection_UniversalPayload) ProtoMethods() *protoiface.Methods { if iNdEx >= l { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - if postIndex > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - x.Data = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 4: - if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field GasLimit", wireType) - } - var stringLen uint64 - 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++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - if postIndex > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - x.GasLimit = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 5: - if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field MaxFeePerGas", wireType) - } - var stringLen uint64 - 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++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - if postIndex > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - x.MaxFeePerGas = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 6: - if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field MaxPriorityFeePerGas", wireType) - } - var stringLen uint64 - 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++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - if postIndex > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - x.MaxPriorityFeePerGas = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 7: - if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Nonce", wireType) - } - var stringLen uint64 - 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++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - if postIndex > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - x.Nonce = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 8: - if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Deadline", wireType) - } - var stringLen uint64 - 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++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - if postIndex > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - x.Deadline = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 9: - if wireType != 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field VType", wireType) - } - x.VType = 0 - 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++ - x.VType |= VerificationType(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := runtime.Skip(dAtA[iNdEx:]) - if err != nil { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength - } - if (iNdEx + skippy) > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - if !options.DiscardUnknown { - x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - } - iNdEx += skippy - } - } - - if iNdEx > l { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF - } - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil - } - return &protoiface.Methods{ - NoUnkeyedLiterals: struct{}{}, - Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, - Size: size, - Marshal: marshal, - Unmarshal: unmarshal, - Merge: nil, - CheckInitialized: nil, - } -} - -var ( - md_MigrationPayload protoreflect.MessageDescriptor - fd_MigrationPayload_migration protoreflect.FieldDescriptor - fd_MigrationPayload_nonce protoreflect.FieldDescriptor - fd_MigrationPayload_deadline protoreflect.FieldDescriptor -) - -func init() { - file_uexecutor_v1_types_proto_init() - md_MigrationPayload = File_uexecutor_v1_types_proto.Messages().ByName("MigrationPayload") - fd_MigrationPayload_migration = md_MigrationPayload.Fields().ByName("migration") - fd_MigrationPayload_nonce = md_MigrationPayload.Fields().ByName("nonce") - fd_MigrationPayload_deadline = md_MigrationPayload.Fields().ByName("deadline") -} - -var _ protoreflect.Message = (*fastReflection_MigrationPayload)(nil) - -type fastReflection_MigrationPayload MigrationPayload - -func (x *MigrationPayload) ProtoReflect() protoreflect.Message { - return (*fastReflection_MigrationPayload)(x) -} - -func (x *MigrationPayload) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_types_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -var _fastReflection_MigrationPayload_messageType fastReflection_MigrationPayload_messageType -var _ protoreflect.MessageType = fastReflection_MigrationPayload_messageType{} - -type fastReflection_MigrationPayload_messageType struct{} - -func (x fastReflection_MigrationPayload_messageType) Zero() protoreflect.Message { - return (*fastReflection_MigrationPayload)(nil) -} -func (x fastReflection_MigrationPayload_messageType) New() protoreflect.Message { - return new(fastReflection_MigrationPayload) -} -func (x fastReflection_MigrationPayload_messageType) Descriptor() protoreflect.MessageDescriptor { - return md_MigrationPayload -} - -// Descriptor returns message descriptor, which contains only the protobuf -// type information for the message. -func (x *fastReflection_MigrationPayload) Descriptor() protoreflect.MessageDescriptor { - return md_MigrationPayload -} - -// Type returns the message type, which encapsulates both Go and protobuf -// type information. If the Go type information is not needed, -// it is recommended that the message descriptor be used instead. -func (x *fastReflection_MigrationPayload) Type() protoreflect.MessageType { - return _fastReflection_MigrationPayload_messageType -} - -// New returns a newly allocated and mutable empty message. -func (x *fastReflection_MigrationPayload) New() protoreflect.Message { - return new(fastReflection_MigrationPayload) -} - -// Interface unwraps the message reflection interface and -// returns the underlying ProtoMessage interface. -func (x *fastReflection_MigrationPayload) Interface() protoreflect.ProtoMessage { - return (*MigrationPayload)(x) -} - -// Range iterates over every populated field in an undefined order, -// calling f for each field descriptor and value encountered. -// Range returns immediately if f returns false. -// While iterating, mutating operations may only be performed -// on the current field descriptor. -func (x *fastReflection_MigrationPayload) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { - if x.Migration != "" { - value := protoreflect.ValueOfString(x.Migration) - if !f(fd_MigrationPayload_migration, value) { - return - } - } - if x.Nonce != "" { - value := protoreflect.ValueOfString(x.Nonce) - if !f(fd_MigrationPayload_nonce, value) { - return - } - } - if x.Deadline != "" { - value := protoreflect.ValueOfString(x.Deadline) - if !f(fd_MigrationPayload_deadline, value) { - return - } - } -} - -// Has reports whether a field is populated. -// -// Some fields have the property of nullability where it is possible to -// distinguish between the default value of a field and whether the field -// was explicitly populated with the default value. Singular message fields, -// member fields of a oneof, and proto2 scalar fields are nullable. Such -// fields are populated only if explicitly set. -// -// In other cases (aside from the nullable cases above), -// a proto3 scalar field is populated if it contains a non-zero value, and -// a repeated field is populated if it is non-empty. -func (x *fastReflection_MigrationPayload) Has(fd protoreflect.FieldDescriptor) bool { - switch fd.FullName() { - case "uexecutor.v1.MigrationPayload.migration": - return x.Migration != "" - case "uexecutor.v1.MigrationPayload.nonce": - return x.Nonce != "" - case "uexecutor.v1.MigrationPayload.deadline": - return x.Deadline != "" - default: - if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MigrationPayload")) - } - panic(fmt.Errorf("message uexecutor.v1.MigrationPayload does not contain field %s", fd.FullName())) - } -} - -// Clear clears the field such that a subsequent Has call reports false. -// -// Clearing an extension field clears both the extension type and value -// associated with the given field number. -// -// Clear is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MigrationPayload) Clear(fd protoreflect.FieldDescriptor) { - switch fd.FullName() { - case "uexecutor.v1.MigrationPayload.migration": - x.Migration = "" - case "uexecutor.v1.MigrationPayload.nonce": - x.Nonce = "" - case "uexecutor.v1.MigrationPayload.deadline": - x.Deadline = "" - default: - if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MigrationPayload")) - } - panic(fmt.Errorf("message uexecutor.v1.MigrationPayload does not contain field %s", fd.FullName())) - } -} - -// Get retrieves the value for a field. -// -// For unpopulated scalars, it returns the default value, where -// the default value of a bytes scalar is guaranteed to be a copy. -// For unpopulated composite types, it returns an empty, read-only view -// of the value; to obtain a mutable reference, use Mutable. -func (x *fastReflection_MigrationPayload) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { - switch descriptor.FullName() { - case "uexecutor.v1.MigrationPayload.migration": - value := x.Migration - return protoreflect.ValueOfString(value) - case "uexecutor.v1.MigrationPayload.nonce": - value := x.Nonce - return protoreflect.ValueOfString(value) - case "uexecutor.v1.MigrationPayload.deadline": - value := x.Deadline - return protoreflect.ValueOfString(value) - default: - if descriptor.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MigrationPayload")) - } - panic(fmt.Errorf("message uexecutor.v1.MigrationPayload does not contain field %s", descriptor.FullName())) - } -} - -// Set stores the value for a field. -// -// For a field belonging to a oneof, it implicitly clears any other field -// that may be currently set within the same oneof. -// For extension fields, it implicitly stores the provided ExtensionType. -// When setting a composite type, it is unspecified whether the stored value -// aliases the source's memory in any way. If the composite value is an -// empty, read-only value, then it panics. -// -// Set is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MigrationPayload) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { - switch fd.FullName() { - case "uexecutor.v1.MigrationPayload.migration": - x.Migration = value.Interface().(string) - case "uexecutor.v1.MigrationPayload.nonce": - x.Nonce = value.Interface().(string) - case "uexecutor.v1.MigrationPayload.deadline": - x.Deadline = value.Interface().(string) - default: - if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MigrationPayload")) - } - panic(fmt.Errorf("message uexecutor.v1.MigrationPayload does not contain field %s", fd.FullName())) - } -} - -// Mutable returns a mutable reference to a composite type. -// -// If the field is unpopulated, it may allocate a composite value. -// For a field belonging to a oneof, it implicitly clears any other field -// that may be currently set within the same oneof. -// For extension fields, it implicitly stores the provided ExtensionType -// if not already stored. -// It panics if the field does not contain a composite type. -// -// Mutable is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MigrationPayload) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { - switch fd.FullName() { - case "uexecutor.v1.MigrationPayload.migration": - panic(fmt.Errorf("field migration of message uexecutor.v1.MigrationPayload is not mutable")) - case "uexecutor.v1.MigrationPayload.nonce": - panic(fmt.Errorf("field nonce of message uexecutor.v1.MigrationPayload is not mutable")) - case "uexecutor.v1.MigrationPayload.deadline": - panic(fmt.Errorf("field deadline of message uexecutor.v1.MigrationPayload is not mutable")) - default: - if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MigrationPayload")) - } - panic(fmt.Errorf("message uexecutor.v1.MigrationPayload does not contain field %s", fd.FullName())) - } -} - -// NewField returns a new value that is assignable to the field -// for the given descriptor. For scalars, this returns the default value. -// For lists, maps, and messages, this returns a new, empty, mutable value. -func (x *fastReflection_MigrationPayload) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { - switch fd.FullName() { - case "uexecutor.v1.MigrationPayload.migration": - return protoreflect.ValueOfString("") - case "uexecutor.v1.MigrationPayload.nonce": - return protoreflect.ValueOfString("") - case "uexecutor.v1.MigrationPayload.deadline": - return protoreflect.ValueOfString("") - default: - if fd.IsExtension() { - panic(fmt.Errorf("proto3 declared messages do not support extensions: uexecutor.v1.MigrationPayload")) - } - panic(fmt.Errorf("message uexecutor.v1.MigrationPayload does not contain field %s", fd.FullName())) - } -} - -// WhichOneof reports which field within the oneof is populated, -// returning nil if none are populated. -// It panics if the oneof descriptor does not belong to this message. -func (x *fastReflection_MigrationPayload) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { - switch d.FullName() { - default: - panic(fmt.Errorf("%s is not a oneof field in uexecutor.v1.MigrationPayload", d.FullName())) - } - panic("unreachable") -} - -// GetUnknown retrieves the entire list of unknown fields. -// The caller may only mutate the contents of the RawFields -// if the mutated bytes are stored back into the message with SetUnknown. -func (x *fastReflection_MigrationPayload) GetUnknown() protoreflect.RawFields { - return x.unknownFields -} - -// SetUnknown stores an entire list of unknown fields. -// The raw fields must be syntactically valid according to the wire format. -// An implementation may panic if this is not the case. -// Once stored, the caller must not mutate the content of the RawFields. -// An empty RawFields may be passed to clear the fields. -// -// SetUnknown is a mutating operation and unsafe for concurrent use. -func (x *fastReflection_MigrationPayload) SetUnknown(fields protoreflect.RawFields) { - x.unknownFields = fields -} - -// IsValid reports whether the message is valid. -// -// An invalid message is an empty, read-only value. -// -// An invalid message often corresponds to a nil pointer of the concrete -// message type, but the details are implementation dependent. -// Validity is not part of the protobuf data model, and may not -// be preserved in marshaling or other operations. -func (x *fastReflection_MigrationPayload) IsValid() bool { - return x != nil -} - -// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. -// This method may return nil. -// -// The returned methods type is identical to -// "google.golang.org/protobuf/runtime/protoiface".Methods. -// Consult the protoiface package documentation for details. -func (x *fastReflection_MigrationPayload) ProtoMethods() *protoiface.Methods { - size := func(input protoiface.SizeInput) protoiface.SizeOutput { - x := input.Message.Interface().(*MigrationPayload) - if x == nil { - return protoiface.SizeOutput{ - NoUnkeyedLiterals: input.NoUnkeyedLiterals, - Size: 0, - } - } - options := runtime.SizeInputToOptions(input) - _ = options - var n int - var l int - _ = l - l = len(x.Migration) - if l > 0 { - n += 1 + l + runtime.Sov(uint64(l)) - } - l = len(x.Nonce) - if l > 0 { - n += 1 + l + runtime.Sov(uint64(l)) - } - l = len(x.Deadline) - if l > 0 { - n += 1 + l + runtime.Sov(uint64(l)) - } - if x.unknownFields != nil { - n += len(x.unknownFields) - } - return protoiface.SizeOutput{ - NoUnkeyedLiterals: input.NoUnkeyedLiterals, - Size: n, - } - } - - marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { - x := input.Message.Interface().(*MigrationPayload) - if x == nil { - return protoiface.MarshalOutput{ - NoUnkeyedLiterals: input.NoUnkeyedLiterals, - Buf: input.Buf, - }, nil - } - options := runtime.MarshalInputToOptions(input) - _ = options - size := options.Size(x) - dAtA := make([]byte, size) - i := len(dAtA) - _ = i - var l int - _ = l - if x.unknownFields != nil { - i -= len(x.unknownFields) - copy(dAtA[i:], x.unknownFields) - } - if len(x.Deadline) > 0 { - i -= len(x.Deadline) - copy(dAtA[i:], x.Deadline) - i = runtime.EncodeVarint(dAtA, i, uint64(len(x.Deadline))) - i-- - dAtA[i] = 0x1a - } - if len(x.Nonce) > 0 { - i -= len(x.Nonce) - copy(dAtA[i:], x.Nonce) - i = runtime.EncodeVarint(dAtA, i, uint64(len(x.Nonce))) - i-- - dAtA[i] = 0x12 - } - if len(x.Migration) > 0 { - i -= len(x.Migration) - copy(dAtA[i:], x.Migration) - i = runtime.EncodeVarint(dAtA, i, uint64(len(x.Migration))) - i-- - dAtA[i] = 0xa - } - if input.Buf != nil { - input.Buf = append(input.Buf, dAtA...) - } else { - input.Buf = dAtA - } - return protoiface.MarshalOutput{ - NoUnkeyedLiterals: input.NoUnkeyedLiterals, - Buf: input.Buf, - }, nil - } - unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { - x := input.Message.Interface().(*MigrationPayload) - if x == nil { - return protoiface.UnmarshalOutput{ - NoUnkeyedLiterals: input.NoUnkeyedLiterals, - Flags: input.Flags, - }, nil - } - options := runtime.UnmarshalInputToOptions(input) - _ = options - dAtA := input.Buf - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } } - if iNdEx >= l { + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break + x.Data = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field GasLimit", wireType) } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MigrationPayload: wiretype end group for non-group") - } - if fieldNum <= 0 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MigrationPayload: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: + var stringLen uint64 + 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++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.GasLimit = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 5: if wireType != 2 { - return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Migration", wireType) + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field MaxFeePerGas", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -1787,9 +1236,41 @@ func (x *fastReflection_MigrationPayload) ProtoMethods() *protoiface.Methods { if postIndex > l { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF } - x.Migration = string(dAtA[iNdEx:postIndex]) + x.MaxFeePerGas = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex - case 2: + case 6: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field MaxPriorityFeePerGas", wireType) + } + var stringLen uint64 + 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++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.MaxPriorityFeePerGas = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 7: if wireType != 2 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Nonce", wireType) } @@ -1821,7 +1302,7 @@ func (x *fastReflection_MigrationPayload) ProtoMethods() *protoiface.Methods { } x.Nonce = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex - case 3: + case 8: if wireType != 2 { return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Deadline", wireType) } @@ -1853,6 +1334,25 @@ func (x *fastReflection_MigrationPayload) ProtoMethods() *protoiface.Methods { } x.Deadline = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 9: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field VType", wireType) + } + x.VType = 0 + 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++ + x.VType |= VerificationType(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -1912,7 +1412,7 @@ func (x *UniversalAccountId) ProtoReflect() protoreflect.Message { } func (x *UniversalAccountId) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_types_proto_msgTypes[3] + mi := &file_uexecutor_v1_types_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2456,7 +1956,7 @@ func (x *RevertInstructions) ProtoReflect() protoreflect.Message { } func (x *RevertInstructions) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_types_proto_msgTypes[4] + mi := &file_uexecutor_v1_types_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2902,7 +2402,7 @@ func (x *Inbound) ProtoReflect() protoreflect.Message { } func (x *Inbound) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_types_proto_msgTypes[5] + mi := &file_uexecutor_v1_types_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4132,7 +3632,7 @@ func (x *PCTx) ProtoReflect() protoreflect.Message { } func (x *PCTx) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_types_proto_msgTypes[6] + mi := &file_uexecutor_v1_types_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4840,7 +4340,7 @@ func (x *OutboundObservation) ProtoReflect() protoreflect.Message { } func (x *OutboundObservation) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_types_proto_msgTypes[7] + mi := &file_uexecutor_v1_types_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5546,7 +5046,7 @@ func (x *OriginatingPcTx) ProtoReflect() protoreflect.Message { } func (x *OriginatingPcTx) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_types_proto_msgTypes[8] + mi := &file_uexecutor_v1_types_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6072,7 +5572,7 @@ func (x *OutboundTx) ProtoReflect() protoreflect.Message { } func (x *OutboundTx) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_types_proto_msgTypes[9] + mi := &file_uexecutor_v1_types_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8015,7 +7515,7 @@ func (x *UniversalTx) ProtoReflect() protoreflect.Message { } func (x *UniversalTx) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_types_proto_msgTypes[10] + mi := &file_uexecutor_v1_types_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8762,7 +8262,7 @@ func (x *InboundLegacy) ProtoReflect() protoreflect.Message { } func (x *InboundLegacy) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_types_proto_msgTypes[11] + mi := &file_uexecutor_v1_types_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9747,7 +9247,7 @@ func (x *OutboundTxLegacy) ProtoReflect() protoreflect.Message { } func (x *OutboundTxLegacy) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_types_proto_msgTypes[12] + mi := &file_uexecutor_v1_types_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10472,7 +9972,7 @@ func (x *UniversalTxLegacy) ProtoReflect() protoreflect.Message { } func (x *UniversalTxLegacy) slowProtoReflect() protoreflect.Message { - mi := &file_uexecutor_v1_types_proto_msgTypes[13] + mi := &file_uexecutor_v1_types_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11407,6 +10907,12 @@ type Params struct { unknownFields protoimpl.UnknownFields SomeValue bool `protobuf:"varint,2,opt,name=some_value,json=someValue,proto3" json:"some_value,omitempty"` + // max_gasless_tx_gas is the maximum gas limit a fee-exempt (gasless) + // transaction is allowed to declare. Gasless transactions pay no fee, so + // their declared gas is not bounded by anything the sender has to spend; + // this cap is the only bound on how much a single gasless transaction can + // contribute to the block's cumulative gas wanted. + MaxGaslessTxGas uint64 `protobuf:"varint,3,opt,name=max_gasless_tx_gas,json=maxGaslessTxGas,proto3" json:"max_gasless_tx_gas,omitempty"` } func (x *Params) Reset() { @@ -11436,6 +10942,13 @@ func (x *Params) GetSomeValue() bool { return false } +func (x *Params) GetMaxGaslessTxGas() uint64 { + if x != nil { + return x.MaxGaslessTxGas + } + return 0 +} + // UniversalPayload mirrors the Solidity struct type UniversalPayload struct { state protoimpl.MessageState @@ -11536,58 +11049,6 @@ func (x *UniversalPayload) GetVType() VerificationType { return VerificationType_signedVerification } -// MigrationPayload mirrors the Solidity struct -type MigrationPayload struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Migration string `protobuf:"bytes,1,opt,name=migration,proto3" json:"migration,omitempty"` // Migration Address - Nonce string `protobuf:"bytes,2,opt,name=nonce,proto3" json:"nonce,omitempty"` // unit256 as string - Deadline string `protobuf:"bytes,3,opt,name=deadline,proto3" json:"deadline,omitempty"` // unit256 as string -} - -func (x *MigrationPayload) Reset() { - *x = MigrationPayload{} - if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_types_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *MigrationPayload) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MigrationPayload) ProtoMessage() {} - -// Deprecated: Use MigrationPayload.ProtoReflect.Descriptor instead. -func (*MigrationPayload) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{2} -} - -func (x *MigrationPayload) GetMigration() string { - if x != nil { - return x.Migration - } - return "" -} - -func (x *MigrationPayload) GetNonce() string { - if x != nil { - return x.Nonce - } - return "" -} - -func (x *MigrationPayload) GetDeadline() string { - if x != nil { - return x.Deadline - } - return "" -} - // UniversalAccountId is the identifier of a owner account type UniversalAccountId struct { state protoimpl.MessageState @@ -11602,7 +11063,7 @@ type UniversalAccountId struct { func (x *UniversalAccountId) Reset() { *x = UniversalAccountId{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_types_proto_msgTypes[3] + mi := &file_uexecutor_v1_types_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11616,7 +11077,7 @@ func (*UniversalAccountId) ProtoMessage() {} // Deprecated: Use UniversalAccountId.ProtoReflect.Descriptor instead. func (*UniversalAccountId) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{3} + return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{2} } func (x *UniversalAccountId) GetChainNamespace() string { @@ -11651,7 +11112,7 @@ type RevertInstructions struct { func (x *RevertInstructions) Reset() { *x = RevertInstructions{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_types_proto_msgTypes[4] + mi := &file_uexecutor_v1_types_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11665,7 +11126,7 @@ func (*RevertInstructions) ProtoMessage() {} // Deprecated: Use RevertInstructions.ProtoReflect.Descriptor instead. func (*RevertInstructions) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{4} + return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{3} } func (x *RevertInstructions) GetFundRecipient() string { @@ -11699,7 +11160,7 @@ type Inbound struct { func (x *Inbound) Reset() { *x = Inbound{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_types_proto_msgTypes[5] + mi := &file_uexecutor_v1_types_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11713,7 +11174,7 @@ func (*Inbound) ProtoMessage() {} // Deprecated: Use Inbound.ProtoReflect.Descriptor instead. func (*Inbound) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{5} + return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{4} } func (x *Inbound) GetSourceChain() string { @@ -11830,7 +11291,7 @@ type PCTx struct { func (x *PCTx) Reset() { *x = PCTx{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_types_proto_msgTypes[6] + mi := &file_uexecutor_v1_types_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11844,7 +11305,7 @@ func (*PCTx) ProtoMessage() {} // Deprecated: Use PCTx.ProtoReflect.Descriptor instead. func (*PCTx) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{6} + return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{5} } func (x *PCTx) GetTxHash() string { @@ -11905,7 +11366,7 @@ type OutboundObservation struct { func (x *OutboundObservation) Reset() { *x = OutboundObservation{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_types_proto_msgTypes[7] + mi := &file_uexecutor_v1_types_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11919,7 +11380,7 @@ func (*OutboundObservation) ProtoMessage() {} // Deprecated: Use OutboundObservation.ProtoReflect.Descriptor instead. func (*OutboundObservation) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{7} + return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{6} } func (x *OutboundObservation) GetSuccess() bool { @@ -11976,7 +11437,7 @@ type OriginatingPcTx struct { func (x *OriginatingPcTx) Reset() { *x = OriginatingPcTx{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_types_proto_msgTypes[8] + mi := &file_uexecutor_v1_types_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11990,7 +11451,7 @@ func (*OriginatingPcTx) ProtoMessage() {} // Deprecated: Use OriginatingPcTx.ProtoReflect.Descriptor instead. func (*OriginatingPcTx) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{8} + return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{7} } func (x *OriginatingPcTx) GetTxHash() string { @@ -12040,7 +11501,7 @@ type OutboundTx struct { func (x *OutboundTx) Reset() { *x = OutboundTx{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_types_proto_msgTypes[9] + mi := &file_uexecutor_v1_types_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12054,7 +11515,7 @@ func (*OutboundTx) ProtoMessage() {} // Deprecated: Use OutboundTx.ProtoReflect.Descriptor instead. func (*OutboundTx) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{9} + return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{8} } func (x *OutboundTx) GetDestinationChain() string { @@ -12233,7 +11694,7 @@ type UniversalTx struct { func (x *UniversalTx) Reset() { *x = UniversalTx{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_types_proto_msgTypes[10] + mi := &file_uexecutor_v1_types_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12247,7 +11708,7 @@ func (*UniversalTx) ProtoMessage() {} // Deprecated: Use UniversalTx.ProtoReflect.Descriptor instead. func (*UniversalTx) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{10} + return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{9} } func (x *UniversalTx) GetId() string { @@ -12305,7 +11766,7 @@ type InboundLegacy struct { func (x *InboundLegacy) Reset() { *x = InboundLegacy{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_types_proto_msgTypes[11] + mi := &file_uexecutor_v1_types_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12319,7 +11780,7 @@ func (*InboundLegacy) ProtoMessage() {} // Deprecated: Use InboundLegacy.ProtoReflect.Descriptor instead. func (*InboundLegacy) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{11} + return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{10} } func (x *InboundLegacy) GetSourceChain() string { @@ -12407,7 +11868,7 @@ type OutboundTxLegacy struct { func (x *OutboundTxLegacy) Reset() { *x = OutboundTxLegacy{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_types_proto_msgTypes[12] + mi := &file_uexecutor_v1_types_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12421,7 +11882,7 @@ func (*OutboundTxLegacy) ProtoMessage() {} // Deprecated: Use OutboundTxLegacy.ProtoReflect.Descriptor instead. func (*OutboundTxLegacy) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{12} + return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{11} } func (x *OutboundTxLegacy) GetDestinationChain() string { @@ -12473,7 +11934,7 @@ type UniversalTxLegacy struct { func (x *UniversalTxLegacy) Reset() { *x = UniversalTxLegacy{} if protoimpl.UnsafeEnabled { - mi := &file_uexecutor_v1_types_proto_msgTypes[13] + mi := &file_uexecutor_v1_types_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12487,7 +11948,7 @@ func (*UniversalTxLegacy) ProtoMessage() {} // Deprecated: Use UniversalTxLegacy.ProtoReflect.Descriptor instead. func (*UniversalTxLegacy) Descriptor() ([]byte, []int) { - return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{13} + return file_uexecutor_v1_types_proto_rawDescGZIP(), []int{12} } func (x *UniversalTxLegacy) GetInboundTx() *InboundLegacy { @@ -12526,324 +11987,318 @@ var file_uexecutor_v1_types_proto_rawDesc = []byte{ 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x1a, 0x14, 0x67, 0x6f, 0x67, 0x6f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x67, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x11, 0x61, 0x6d, 0x69, 0x6e, 0x6f, 0x2f, 0x61, 0x6d, 0x69, 0x6e, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0x46, 0x0a, 0x06, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, + 0x6f, 0x22, 0x73, 0x0a, 0x06, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x73, 0x6f, 0x6d, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x1d, 0x98, 0xa0, 0x1f, 0x00, - 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x10, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x6f, 0x72, 0x2f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x22, 0xdb, 0x02, 0x0a, 0x10, 0x55, 0x6e, - 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x0e, - 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, - 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x67, 0x61, 0x73, - 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x25, 0x0a, 0x0f, 0x6d, 0x61, 0x78, 0x5f, 0x66, 0x65, 0x65, - 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x67, 0x61, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x6d, 0x61, 0x78, 0x46, 0x65, 0x65, 0x50, 0x65, 0x72, 0x47, 0x61, 0x73, 0x12, 0x36, 0x0a, 0x18, - 0x6d, 0x61, 0x78, 0x5f, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x66, 0x65, 0x65, - 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x67, 0x61, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, - 0x6d, 0x61, 0x78, 0x50, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x46, 0x65, 0x65, 0x50, 0x65, - 0x72, 0x47, 0x61, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x65, - 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, - 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x35, 0x0a, 0x06, 0x76, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, 0x76, 0x54, 0x79, 0x70, 0x65, 0x3a, 0x28, 0x98, - 0xa0, 0x1f, 0x00, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x1b, 0x75, 0x65, 0x78, 0x65, - 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x5f, - 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x8c, 0x01, 0x0a, 0x10, 0x4d, 0x69, 0x67, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x1c, 0x0a, 0x09, - 0x6d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x6d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, - 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, - 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x3a, 0x28, 0x98, 0xa0, - 0x1f, 0x00, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x1b, 0x75, 0x65, 0x78, 0x65, 0x63, - 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x6d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, - 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x98, 0x01, 0x0a, 0x12, 0x55, 0x6e, 0x69, 0x76, 0x65, - 0x72, 0x73, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x27, 0x0a, - 0x0f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, - 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x3a, 0x28, 0x98, 0xa0, 0x1f, 0x00, 0xe8, 0xa0, 0x1f, - 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x1b, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, - 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x22, 0x63, 0x0a, 0x12, 0x52, 0x65, 0x76, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x72, - 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x75, 0x6e, 0x64, 0x5f, - 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x66, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x3a, 0x26, - 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x1d, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x6f, 0x72, 0x2f, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x75, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xa5, 0x04, 0x0a, 0x07, 0x49, 0x6e, 0x62, 0x6f, 0x75, - 0x6e, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x68, 0x61, - 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x09, 0x73, 0x6f, 0x6d, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2b, 0x0a, 0x12, 0x6d, 0x61, + 0x78, 0x5f, 0x67, 0x61, 0x73, 0x6c, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x78, 0x5f, 0x67, 0x61, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x6d, 0x61, 0x78, 0x47, 0x61, 0x73, 0x6c, 0x65, + 0x73, 0x73, 0x54, 0x78, 0x47, 0x61, 0x73, 0x3a, 0x1d, 0x98, 0xa0, 0x1f, 0x00, 0xe8, 0xa0, 0x1f, + 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x10, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x22, 0xdb, 0x02, 0x0a, 0x10, 0x55, 0x6e, 0x69, 0x76, 0x65, + 0x72, 0x73, 0x61, 0x6c, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x74, + 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, 0x6c, 0x69, 0x6d, + 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x12, 0x25, 0x0a, 0x0f, 0x6d, 0x61, 0x78, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x70, 0x65, + 0x72, 0x5f, 0x67, 0x61, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x78, + 0x46, 0x65, 0x65, 0x50, 0x65, 0x72, 0x47, 0x61, 0x73, 0x12, 0x36, 0x0a, 0x18, 0x6d, 0x61, 0x78, + 0x5f, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x70, 0x65, + 0x72, 0x5f, 0x67, 0x61, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x6d, 0x61, 0x78, + 0x50, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x46, 0x65, 0x65, 0x50, 0x65, 0x72, 0x47, 0x61, + 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x65, 0x61, 0x64, 0x6c, + 0x69, 0x6e, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x61, 0x64, 0x6c, + 0x69, 0x6e, 0x65, 0x12, 0x35, 0x0a, 0x06, 0x76, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x05, 0x76, 0x54, 0x79, 0x70, 0x65, 0x3a, 0x28, 0x98, 0xa0, 0x1f, 0x00, + 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x1b, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, + 0x6f, 0x72, 0x2f, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x79, + 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x98, 0x01, 0x0a, 0x12, 0x55, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, + 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, + 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x3a, 0x28, 0x98, 0xa0, 0x1f, 0x00, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, + 0xe7, 0xb0, 0x2a, 0x1b, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x75, 0x6e, + 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, + 0x63, 0x0a, 0x12, 0x52, 0x65, 0x76, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x75, 0x6e, 0x64, 0x5f, 0x72, 0x65, + 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x66, + 0x75, 0x6e, 0x64, 0x52, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x3a, 0x26, 0xe8, 0xa0, + 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x1d, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, + 0x2f, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xa5, 0x04, 0x0a, 0x07, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, + 0x61, 0x69, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, + 0x6e, 0x64, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, + 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x73, + 0x73, 0x65, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x6f, 0x67, + 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x6f, + 0x67, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x2d, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x78, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06, 0x74, + 0x78, 0x54, 0x79, 0x70, 0x65, 0x12, 0x4b, 0x0a, 0x11, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, + 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x55, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x52, 0x10, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x50, 0x61, 0x79, 0x6c, 0x6f, + 0x61, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x76, + 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, + 0x51, 0x0a, 0x13, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x75, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x75, + 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x65, + 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x12, + 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x73, 0x43, 0x45, 0x41, 0x18, 0x0c, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x05, 0x69, 0x73, 0x43, 0x45, 0x41, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x61, 0x77, 0x5f, + 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, + 0x61, 0x77, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, + 0x70, 0x63, 0x32, 0x30, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x50, 0x63, + 0x32, 0x30, 0x3a, 0x08, 0x98, 0xa0, 0x1f, 0x00, 0xe8, 0xa0, 0x1f, 0x01, 0x22, 0xc8, 0x01, 0x0a, + 0x04, 0x50, 0x43, 0x54, 0x78, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x61, 0x73, 0x5f, 0x75, 0x73, + 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x67, 0x61, 0x73, 0x55, 0x73, 0x65, + 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, + 0x69, 0x67, 0x68, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x09, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x73, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x73, 0x67, 0x3a, 0x1c, 0x98, 0xa0, 0x1f, 0x00, 0xe8, + 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x0f, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, + 0x72, 0x2f, 0x70, 0x63, 0x5f, 0x74, 0x78, 0x22, 0x85, 0x02, 0x0a, 0x13, 0x4f, 0x75, 0x74, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, + 0x63, 0x6b, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x17, 0x0a, 0x07, + 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, + 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, + 0x73, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, + 0x73, 0x67, 0x12, 0x20, 0x0a, 0x0c, 0x67, 0x61, 0x73, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x75, 0x73, + 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x67, 0x61, 0x73, 0x46, 0x65, 0x65, + 0x55, 0x73, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x70, 0x63, 0x32, 0x30, 0x5f, 0x77, 0x72, 0x61, + 0x70, 0x70, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x12, 0x70, 0x63, 0x32, 0x30, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x41, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x3a, 0x27, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, + 0x1e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x6f, 0x75, 0x74, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x6d, 0x0a, 0x0f, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x63, + 0x54, 0x78, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x6c, + 0x6f, 0x67, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x6c, 0x6f, 0x67, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x3a, 0x24, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, + 0xb0, 0x2a, 0x1b, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x6f, 0x72, 0x69, + 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x63, 0x5f, 0x74, 0x78, 0x22, 0xe2, + 0x07, 0x0a, 0x0a, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x78, 0x12, 0x2b, 0x0a, + 0x11, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x68, 0x61, + 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, + 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, + 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x2e, 0x0a, 0x13, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x73, 0x73, + 0x65, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, + 0x12, 0x28, 0x0a, 0x10, 0x70, 0x72, 0x63, 0x32, 0x30, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, + 0x61, 0x64, 0x64, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x72, 0x63, 0x32, + 0x30, 0x41, 0x73, 0x73, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, + 0x6e, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x64, + 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x1b, 0x0a, 0x09, + 0x67, 0x61, 0x73, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2d, 0x0a, 0x07, 0x74, 0x78, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x75, 0x65, 0x78, + 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x78, 0x54, 0x79, 0x70, 0x65, + 0x52, 0x06, 0x74, 0x78, 0x54, 0x79, 0x70, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x70, 0x63, 0x5f, 0x74, + 0x78, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6e, 0x67, 0x50, 0x63, 0x54, 0x78, 0x52, 0x04, 0x70, 0x63, 0x54, 0x78, 0x12, 0x42, 0x0a, 0x0b, + 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x74, 0x78, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x21, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x54, 0x78, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x3d, 0x0a, 0x0f, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x75, 0x65, 0x78, 0x65, + 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x0e, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x51, 0x0a, 0x13, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x75, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x75, + 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x65, + 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x12, + 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x13, 0x70, 0x63, 0x5f, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x5f, + 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, + 0x43, 0x54, 0x78, 0x52, 0x11, 0x70, 0x63, 0x52, 0x65, 0x76, 0x65, 0x72, 0x74, 0x45, 0x78, 0x65, + 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, 0x70, 0x72, + 0x69, 0x63, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x67, 0x61, 0x73, 0x50, 0x72, + 0x69, 0x63, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x61, 0x73, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x11, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x61, 0x73, 0x46, 0x65, 0x65, 0x12, 0x42, 0x0a, 0x13, + 0x70, 0x63, 0x5f, 0x72, 0x65, 0x66, 0x75, 0x6e, 0x64, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x75, 0x65, 0x78, 0x65, + 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x43, 0x54, 0x78, 0x52, 0x11, 0x70, + 0x63, 0x52, 0x65, 0x66, 0x75, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x66, 0x75, 0x6e, 0x64, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x5f, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x72, 0x65, 0x66, + 0x75, 0x6e, 0x64, 0x53, 0x77, 0x61, 0x70, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, + 0x67, 0x61, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x67, 0x61, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x62, 0x6f, + 0x72, 0x74, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x07, + 0x69, 0x73, 0x5f, 0x70, 0x63, 0x32, 0x30, 0x18, 0x16, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, + 0x73, 0x50, 0x63, 0x32, 0x30, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x63, 0x32, 0x30, 0x5f, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x17, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x63, 0x32, 0x30, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x61, + 0x63, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x3a, 0x08, 0x98, 0xa0, 0x1f, 0x00, 0xe8, + 0xa0, 0x1f, 0x01, 0x22, 0xff, 0x01, 0x0a, 0x0b, 0x55, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, + 0x6c, 0x54, 0x78, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x34, 0x0a, 0x0a, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x74, + 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x09, + 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x78, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x63, 0x5f, + 0x74, 0x78, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x43, 0x54, 0x78, 0x52, 0x04, 0x70, 0x63, + 0x54, 0x78, 0x12, 0x39, 0x0a, 0x0b, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x74, + 0x78, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, + 0x78, 0x52, 0x0a, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x78, 0x12, 0x21, 0x0a, + 0x0c, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x3a, 0x23, 0x98, 0xa0, 0x1f, 0x00, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x16, 0x75, + 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, + 0x61, 0x6c, 0x5f, 0x74, 0x78, 0x22, 0xab, 0x03, 0x0a, 0x0d, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, + 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, + 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x72, + 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, + 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x6f, 0x67, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x3a, 0x0a, + 0x07, 0x74, 0x78, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, + 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x78, 0x54, 0x79, 0x70, 0x65, 0x4c, 0x65, 0x67, 0x61, 0x63, + 0x79, 0x52, 0x06, 0x74, 0x78, 0x54, 0x79, 0x70, 0x65, 0x12, 0x4b, 0x0a, 0x11, 0x75, 0x6e, 0x69, + 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x50, 0x61, 0x79, + 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x10, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x50, + 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x10, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, + 0x61, 0x74, 0x61, 0x3a, 0x1e, 0x98, 0xa0, 0x1f, 0x01, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, + 0x2a, 0x11, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x69, 0x6e, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x22, 0xd1, 0x01, 0x0a, 0x10, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x54, 0x78, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x2b, 0x0a, 0x11, 0x64, 0x65, 0x73, 0x74, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, - 0x0a, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, - 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, - 0x69, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x6c, - 0x6f, 0x67, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x6c, 0x6f, 0x67, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x2d, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x75, 0x65, 0x78, 0x65, - 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x78, 0x54, 0x79, 0x70, 0x65, 0x52, - 0x06, 0x74, 0x78, 0x54, 0x79, 0x70, 0x65, 0x12, 0x4b, 0x0a, 0x11, 0x75, 0x6e, 0x69, 0x76, 0x65, - 0x72, 0x73, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x55, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x50, 0x61, 0x79, 0x6c, 0x6f, - 0x61, 0x64, 0x52, 0x10, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x50, 0x61, 0x79, - 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x10, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, - 0x61, 0x12, 0x51, 0x0a, 0x13, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, - 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, - 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x76, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x52, 0x12, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x73, 0x43, 0x45, 0x41, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x05, 0x69, 0x73, 0x43, 0x45, 0x41, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x61, - 0x77, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x72, 0x61, 0x77, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x69, - 0x73, 0x5f, 0x70, 0x63, 0x32, 0x30, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, - 0x50, 0x63, 0x32, 0x30, 0x3a, 0x08, 0x98, 0xa0, 0x1f, 0x00, 0xe8, 0xa0, 0x1f, 0x01, 0x22, 0xc8, - 0x01, 0x0a, 0x04, 0x50, 0x43, 0x54, 0x78, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, - 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, - 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x61, 0x73, 0x5f, - 0x75, 0x73, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x67, 0x61, 0x73, 0x55, - 0x73, 0x65, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x65, 0x69, - 0x67, 0x68, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, - 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, - 0x0a, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x73, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x73, 0x67, 0x3a, 0x1c, 0x98, 0xa0, 0x1f, - 0x00, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x0f, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, - 0x74, 0x6f, 0x72, 0x2f, 0x70, 0x63, 0x5f, 0x74, 0x78, 0x22, 0x85, 0x02, 0x0a, 0x13, 0x4f, 0x75, - 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x62, - 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x17, - 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x5f, 0x6d, 0x73, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x4d, 0x73, 0x67, 0x12, 0x20, 0x0a, 0x0c, 0x67, 0x61, 0x73, 0x5f, 0x66, 0x65, 0x65, 0x5f, - 0x75, 0x73, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x67, 0x61, 0x73, 0x46, - 0x65, 0x65, 0x55, 0x73, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x70, 0x63, 0x32, 0x30, 0x5f, 0x77, - 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x70, 0x63, 0x32, 0x30, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, - 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x3a, 0x27, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, - 0xb0, 0x2a, 0x1e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x6f, 0x75, 0x74, - 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x22, 0x6d, 0x0a, 0x0f, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6e, 0x67, - 0x50, 0x63, 0x54, 0x78, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, - 0x09, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x6c, 0x6f, 0x67, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x3a, 0x24, 0xe8, 0xa0, 0x1f, 0x01, - 0x8a, 0xe7, 0xb0, 0x2a, 0x1b, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x6f, - 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x63, 0x5f, 0x74, 0x78, - 0x22, 0xe2, 0x07, 0x0a, 0x0a, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x78, 0x12, - 0x2b, 0x0a, 0x11, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, - 0x68, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x73, 0x74, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x1c, 0x0a, 0x09, - 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, - 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x11, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x41, 0x64, - 0x64, 0x72, 0x12, 0x28, 0x0a, 0x10, 0x70, 0x72, 0x63, 0x32, 0x30, 0x5f, 0x61, 0x73, 0x73, 0x65, - 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x72, - 0x63, 0x32, 0x30, 0x41, 0x73, 0x73, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x16, 0x0a, 0x06, - 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, - 0x6e, 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x1b, - 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2d, 0x0a, 0x07, 0x74, - 0x78, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x75, - 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x78, 0x54, 0x79, - 0x70, 0x65, 0x52, 0x06, 0x74, 0x78, 0x54, 0x79, 0x70, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x70, 0x63, - 0x5f, 0x74, 0x78, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x75, 0x65, 0x78, 0x65, - 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, - 0x74, 0x69, 0x6e, 0x67, 0x50, 0x63, 0x54, 0x78, 0x52, 0x04, 0x70, 0x63, 0x54, 0x78, 0x12, 0x42, - 0x0a, 0x0b, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x74, 0x78, 0x18, 0x0b, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4f, 0x62, 0x73, 0x65, 0x72, - 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, - 0x54, 0x78, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x3d, 0x0a, 0x0f, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x75, 0x65, - 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x52, 0x0e, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x51, 0x0a, 0x13, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, - 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, - 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x76, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x52, 0x12, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x13, 0x70, 0x63, 0x5f, 0x72, 0x65, 0x76, 0x65, 0x72, - 0x74, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0f, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x12, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x50, 0x43, 0x54, 0x78, 0x52, 0x11, 0x70, 0x63, 0x52, 0x65, 0x76, 0x65, 0x72, 0x74, 0x45, - 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, - 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x67, 0x61, 0x73, - 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x61, 0x73, 0x5f, 0x66, 0x65, 0x65, - 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x61, 0x73, 0x46, 0x65, 0x65, 0x12, 0x42, - 0x0a, 0x13, 0x70, 0x63, 0x5f, 0x72, 0x65, 0x66, 0x75, 0x6e, 0x64, 0x5f, 0x65, 0x78, 0x65, 0x63, - 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x75, 0x65, - 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x43, 0x54, 0x78, 0x52, - 0x11, 0x70, 0x63, 0x52, 0x65, 0x66, 0x75, 0x6e, 0x64, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x66, 0x75, 0x6e, 0x64, 0x5f, 0x73, 0x77, 0x61, - 0x70, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x72, - 0x65, 0x66, 0x75, 0x6e, 0x64, 0x53, 0x77, 0x61, 0x70, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1b, - 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x67, 0x61, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x61, - 0x62, 0x6f, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x15, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0b, 0x61, 0x62, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x17, - 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x70, 0x63, 0x32, 0x30, 0x18, 0x16, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x06, 0x69, 0x73, 0x50, 0x63, 0x32, 0x30, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x63, 0x32, 0x30, 0x5f, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x18, 0x17, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x63, 0x32, 0x30, 0x43, 0x6f, 0x6e, 0x74, - 0x72, 0x61, 0x63, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x3a, 0x08, 0x98, 0xa0, 0x1f, - 0x00, 0xe8, 0xa0, 0x1f, 0x01, 0x22, 0xff, 0x01, 0x0a, 0x0b, 0x55, 0x6e, 0x69, 0x76, 0x65, 0x72, - 0x73, 0x61, 0x6c, 0x54, 0x78, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x34, 0x0a, 0x0a, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x5f, 0x74, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x75, 0x65, 0x78, 0x65, - 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x52, 0x09, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x78, 0x12, 0x27, 0x0a, 0x05, 0x70, - 0x63, 0x5f, 0x74, 0x78, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x75, 0x65, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x43, 0x54, 0x78, 0x52, 0x04, - 0x70, 0x63, 0x54, 0x78, 0x12, 0x39, 0x0a, 0x0b, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x5f, 0x74, 0x78, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x75, 0x65, 0x78, 0x65, - 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, - 0x64, 0x54, 0x78, 0x52, 0x0a, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x78, 0x12, - 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x76, 0x65, 0x72, 0x74, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x3a, 0x23, 0x98, 0xa0, 0x1f, 0x00, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, - 0x16, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x75, 0x6e, 0x69, 0x76, 0x65, - 0x72, 0x73, 0x61, 0x6c, 0x5f, 0x74, 0x78, 0x22, 0xab, 0x03, 0x0a, 0x0d, 0x49, 0x6e, 0x62, 0x6f, - 0x75, 0x6e, 0x64, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x17, 0x0a, 0x07, - 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, - 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x1c, 0x0a, - 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, - 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x64, 0x64, - 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x64, - 0x64, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x6f, 0x67, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, - 0x3a, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x21, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x78, 0x54, 0x79, 0x70, 0x65, 0x4c, 0x65, 0x67, - 0x61, 0x63, 0x79, 0x52, 0x06, 0x74, 0x78, 0x54, 0x79, 0x70, 0x65, 0x12, 0x4b, 0x0a, 0x11, 0x75, - 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x50, - 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x10, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, - 0x6c, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x76, 0x65, 0x72, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x10, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x44, 0x61, 0x74, 0x61, 0x3a, 0x1e, 0x98, 0xa0, 0x1f, 0x01, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, - 0xe7, 0xb0, 0x2a, 0x11, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x69, 0x6e, - 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0xd1, 0x01, 0x0a, 0x10, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, - 0x6e, 0x64, 0x54, 0x78, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x2b, 0x0a, 0x11, 0x64, 0x65, - 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, - 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, - 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x16, - 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, - 0x61, 0x64, 0x64, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x73, 0x73, 0x65, - 0x74, 0x41, 0x64, 0x64, 0x72, 0x3a, 0x22, 0x98, 0xa0, 0x1f, 0x01, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, - 0xe7, 0xb0, 0x2a, 0x15, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x6f, 0x75, - 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x74, 0x78, 0x22, 0xaa, 0x02, 0x0a, 0x11, 0x55, 0x6e, - 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x54, 0x78, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, - 0x3a, 0x0a, 0x0a, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x74, 0x78, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, - 0x52, 0x09, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x78, 0x12, 0x27, 0x0a, 0x05, 0x70, - 0x63, 0x5f, 0x74, 0x78, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x75, 0x65, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x43, 0x54, 0x78, 0x52, 0x04, - 0x70, 0x63, 0x54, 0x78, 0x12, 0x3f, 0x0a, 0x0b, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x5f, 0x74, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x75, 0x65, 0x78, 0x65, - 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, - 0x64, 0x54, 0x78, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x52, 0x0a, 0x6f, 0x75, 0x74, 0x62, 0x6f, - 0x75, 0x6e, 0x64, 0x54, 0x78, 0x12, 0x4a, 0x0a, 0x10, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, - 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x1f, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, - 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x54, 0x78, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x52, 0x0f, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x3a, 0x23, 0x98, 0xa0, 0x1f, 0x01, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x16, - 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, - 0x73, 0x61, 0x6c, 0x5f, 0x74, 0x78, 0x2a, 0x47, 0x0a, 0x10, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x73, 0x69, - 0x67, 0x6e, 0x65, 0x64, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x54, - 0x78, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0x01, 0x2a, - 0x83, 0x02, 0x0a, 0x11, 0x55, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x54, 0x78, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x23, 0x0a, 0x1f, 0x55, 0x4e, 0x49, 0x56, 0x45, 0x52, 0x53, - 0x41, 0x4c, 0x5f, 0x54, 0x58, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, - 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x49, 0x4e, - 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x01, 0x12, - 0x1d, 0x0a, 0x19, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x49, 0x4e, 0x42, 0x4f, 0x55, - 0x4e, 0x44, 0x5f, 0x45, 0x58, 0x45, 0x43, 0x55, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x17, - 0x0a, 0x13, 0x50, 0x43, 0x5f, 0x45, 0x58, 0x45, 0x43, 0x55, 0x54, 0x45, 0x44, 0x5f, 0x53, 0x55, - 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x43, 0x5f, 0x45, 0x58, - 0x45, 0x43, 0x55, 0x54, 0x45, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, - 0x15, 0x0a, 0x11, 0x50, 0x43, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x52, 0x45, - 0x56, 0x45, 0x52, 0x54, 0x10, 0x05, 0x12, 0x14, 0x0a, 0x10, 0x4f, 0x55, 0x54, 0x42, 0x4f, 0x55, - 0x4e, 0x44, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x06, 0x12, 0x14, 0x0a, 0x10, - 0x4f, 0x55, 0x54, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, - 0x10, 0x07, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x55, 0x54, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x46, - 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x08, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x41, 0x4e, 0x43, 0x45, - 0x4c, 0x45, 0x44, 0x10, 0x09, 0x2a, 0x4f, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0c, 0x0a, - 0x08, 0x4f, 0x42, 0x53, 0x45, 0x52, 0x56, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x52, - 0x45, 0x56, 0x45, 0x52, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x42, 0x4f, - 0x52, 0x54, 0x45, 0x44, 0x10, 0x04, 0x2a, 0x8f, 0x01, 0x0a, 0x06, 0x54, 0x78, 0x54, 0x79, 0x70, - 0x65, 0x12, 0x12, 0x0a, 0x0e, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, - 0x5f, 0x54, 0x58, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x47, 0x41, 0x53, 0x10, 0x01, 0x12, 0x13, - 0x0a, 0x0f, 0x47, 0x41, 0x53, 0x5f, 0x41, 0x4e, 0x44, 0x5f, 0x50, 0x41, 0x59, 0x4c, 0x4f, 0x41, - 0x44, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x55, 0x4e, 0x44, 0x53, 0x10, 0x03, 0x12, 0x15, - 0x0a, 0x11, 0x46, 0x55, 0x4e, 0x44, 0x53, 0x5f, 0x41, 0x4e, 0x44, 0x5f, 0x50, 0x41, 0x59, 0x4c, - 0x4f, 0x41, 0x44, 0x10, 0x04, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x41, 0x59, 0x4c, 0x4f, 0x41, 0x44, - 0x10, 0x05, 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x52, 0x45, - 0x56, 0x45, 0x52, 0x54, 0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, 0x53, 0x43, 0x55, 0x45, - 0x5f, 0x46, 0x55, 0x4e, 0x44, 0x53, 0x10, 0x07, 0x2a, 0xb4, 0x01, 0x0a, 0x13, 0x49, 0x6e, 0x62, - 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x78, 0x54, 0x79, 0x70, 0x65, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, - 0x12, 0x21, 0x0a, 0x1d, 0x49, 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x4c, 0x45, 0x47, 0x41, - 0x43, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, 0x54, - 0x58, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x4c, - 0x45, 0x47, 0x41, 0x43, 0x59, 0x5f, 0x47, 0x41, 0x53, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x49, - 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x5f, 0x46, 0x55, - 0x4e, 0x44, 0x53, 0x10, 0x02, 0x12, 0x24, 0x0a, 0x20, 0x49, 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, - 0x5f, 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x5f, 0x46, 0x55, 0x4e, 0x44, 0x53, 0x5f, 0x41, 0x4e, - 0x44, 0x5f, 0x50, 0x41, 0x59, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x03, 0x12, 0x22, 0x0a, 0x1e, 0x49, - 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x5f, 0x47, 0x41, - 0x53, 0x5f, 0x41, 0x4e, 0x44, 0x5f, 0x50, 0x41, 0x59, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x04, 0x42, - 0xb2, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, - 0x72, 0x2e, 0x76, 0x31, 0x42, 0x0a, 0x54, 0x79, 0x70, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, - 0x75, 0x73, 0x68, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2f, 0x70, 0x75, 0x73, 0x68, 0x2d, 0x63, 0x68, - 0x61, 0x69, 0x6e, 0x2d, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x65, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, - 0x74, 0x6f, 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x0c, 0x55, 0x65, - 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0c, 0x55, 0x65, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x18, 0x55, 0x65, 0x78, 0x65, - 0x63, 0x75, 0x74, 0x6f, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0d, 0x55, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, - 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1c, + 0x0a, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x72, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x64, + 0x64, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, + 0x64, 0x64, 0x72, 0x3a, 0x22, 0x98, 0xa0, 0x1f, 0x01, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, + 0x2a, 0x15, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x6f, 0x75, 0x74, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x74, 0x78, 0x22, 0xaa, 0x02, 0x0a, 0x11, 0x55, 0x6e, 0x69, 0x76, + 0x65, 0x72, 0x73, 0x61, 0x6c, 0x54, 0x78, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x3a, 0x0a, + 0x0a, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x52, 0x09, + 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x78, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x63, 0x5f, + 0x74, 0x78, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x43, 0x54, 0x78, 0x52, 0x04, 0x70, 0x63, + 0x54, 0x78, 0x12, 0x3f, 0x0a, 0x0b, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x74, + 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, + 0x78, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x52, 0x0a, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x54, 0x78, 0x12, 0x4a, 0x0a, 0x10, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, + 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, + 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x69, + 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x54, 0x78, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0f, + 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3a, + 0x23, 0x98, 0xa0, 0x1f, 0x01, 0xe8, 0xa0, 0x1f, 0x01, 0x8a, 0xe7, 0xb0, 0x2a, 0x16, 0x75, 0x65, + 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, + 0x6c, 0x5f, 0x74, 0x78, 0x2a, 0x47, 0x0a, 0x10, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x73, 0x69, 0x67, 0x6e, + 0x65, 0x64, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0x00, + 0x12, 0x1b, 0x0a, 0x17, 0x75, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x54, 0x78, 0x56, + 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0x01, 0x2a, 0x83, 0x02, + 0x0a, 0x11, 0x55, 0x6e, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x54, 0x78, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x23, 0x0a, 0x1f, 0x55, 0x4e, 0x49, 0x56, 0x45, 0x52, 0x53, 0x41, 0x4c, + 0x5f, 0x54, 0x58, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x49, 0x4e, 0x42, 0x4f, + 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x01, 0x12, 0x1d, 0x0a, + 0x19, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x49, 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, + 0x5f, 0x45, 0x58, 0x45, 0x43, 0x55, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x17, 0x0a, 0x13, + 0x50, 0x43, 0x5f, 0x45, 0x58, 0x45, 0x43, 0x55, 0x54, 0x45, 0x44, 0x5f, 0x53, 0x55, 0x43, 0x43, + 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x43, 0x5f, 0x45, 0x58, 0x45, 0x43, + 0x55, 0x54, 0x45, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, 0x15, 0x0a, + 0x11, 0x50, 0x43, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x52, 0x45, 0x56, 0x45, + 0x52, 0x54, 0x10, 0x05, 0x12, 0x14, 0x0a, 0x10, 0x4f, 0x55, 0x54, 0x42, 0x4f, 0x55, 0x4e, 0x44, + 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x06, 0x12, 0x14, 0x0a, 0x10, 0x4f, 0x55, + 0x54, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x07, + 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x55, 0x54, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x46, 0x41, 0x49, + 0x4c, 0x45, 0x44, 0x10, 0x08, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x45, + 0x44, 0x10, 0x09, 0x2a, 0x4f, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0f, 0x0a, + 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, + 0x0a, 0x07, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4f, + 0x42, 0x53, 0x45, 0x52, 0x56, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x56, + 0x45, 0x52, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x42, 0x4f, 0x52, 0x54, + 0x45, 0x44, 0x10, 0x04, 0x2a, 0x8f, 0x01, 0x0a, 0x06, 0x54, 0x78, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x12, 0x0a, 0x0e, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, 0x54, + 0x58, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x47, 0x41, 0x53, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, + 0x47, 0x41, 0x53, 0x5f, 0x41, 0x4e, 0x44, 0x5f, 0x50, 0x41, 0x59, 0x4c, 0x4f, 0x41, 0x44, 0x10, + 0x02, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x55, 0x4e, 0x44, 0x53, 0x10, 0x03, 0x12, 0x15, 0x0a, 0x11, + 0x46, 0x55, 0x4e, 0x44, 0x53, 0x5f, 0x41, 0x4e, 0x44, 0x5f, 0x50, 0x41, 0x59, 0x4c, 0x4f, 0x41, + 0x44, 0x10, 0x04, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x41, 0x59, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x05, + 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x52, 0x45, 0x56, 0x45, + 0x52, 0x54, 0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, 0x53, 0x43, 0x55, 0x45, 0x5f, 0x46, + 0x55, 0x4e, 0x44, 0x53, 0x10, 0x07, 0x2a, 0xb4, 0x01, 0x0a, 0x13, 0x49, 0x6e, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0x54, 0x78, 0x54, 0x79, 0x70, 0x65, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x21, + 0x0a, 0x1d, 0x49, 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, + 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, 0x54, 0x58, 0x10, + 0x00, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x4c, 0x45, 0x47, + 0x41, 0x43, 0x59, 0x5f, 0x47, 0x41, 0x53, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x49, 0x4e, 0x42, + 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x5f, 0x46, 0x55, 0x4e, 0x44, + 0x53, 0x10, 0x02, 0x12, 0x24, 0x0a, 0x20, 0x49, 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x4c, + 0x45, 0x47, 0x41, 0x43, 0x59, 0x5f, 0x46, 0x55, 0x4e, 0x44, 0x53, 0x5f, 0x41, 0x4e, 0x44, 0x5f, + 0x50, 0x41, 0x59, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x03, 0x12, 0x22, 0x0a, 0x1e, 0x49, 0x4e, 0x42, + 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x5f, 0x47, 0x41, 0x53, 0x5f, + 0x41, 0x4e, 0x44, 0x5f, 0x50, 0x41, 0x59, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x04, 0x42, 0xb2, 0x01, + 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, + 0x76, 0x31, 0x42, 0x0a, 0x54, 0x79, 0x70, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x75, 0x73, + 0x68, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2f, 0x70, 0x75, 0x73, 0x68, 0x2d, 0x63, 0x68, 0x61, 0x69, + 0x6e, 0x2d, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x65, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x6f, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, + 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x0c, 0x55, 0x65, 0x78, 0x65, + 0x63, 0x75, 0x74, 0x6f, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0c, 0x55, 0x65, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x6f, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x18, 0x55, 0x65, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x6f, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0xea, 0x02, 0x0d, 0x55, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x6f, 0x72, 0x3a, 0x3a, + 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -12859,7 +12314,7 @@ func file_uexecutor_v1_types_proto_rawDescGZIP() []byte { } var file_uexecutor_v1_types_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_uexecutor_v1_types_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_uexecutor_v1_types_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_uexecutor_v1_types_proto_goTypes = []interface{}{ (VerificationType)(0), // 0: uexecutor.v1.VerificationType (UniversalTxStatus)(0), // 1: uexecutor.v1.UniversalTxStatus @@ -12868,39 +12323,38 @@ var file_uexecutor_v1_types_proto_goTypes = []interface{}{ (InboundTxTypeLegacy)(0), // 4: uexecutor.v1.InboundTxTypeLegacy (*Params)(nil), // 5: uexecutor.v1.Params (*UniversalPayload)(nil), // 6: uexecutor.v1.UniversalPayload - (*MigrationPayload)(nil), // 7: uexecutor.v1.MigrationPayload - (*UniversalAccountId)(nil), // 8: uexecutor.v1.UniversalAccountId - (*RevertInstructions)(nil), // 9: uexecutor.v1.RevertInstructions - (*Inbound)(nil), // 10: uexecutor.v1.Inbound - (*PCTx)(nil), // 11: uexecutor.v1.PCTx - (*OutboundObservation)(nil), // 12: uexecutor.v1.OutboundObservation - (*OriginatingPcTx)(nil), // 13: uexecutor.v1.OriginatingPcTx - (*OutboundTx)(nil), // 14: uexecutor.v1.OutboundTx - (*UniversalTx)(nil), // 15: uexecutor.v1.UniversalTx - (*InboundLegacy)(nil), // 16: uexecutor.v1.InboundLegacy - (*OutboundTxLegacy)(nil), // 17: uexecutor.v1.OutboundTxLegacy - (*UniversalTxLegacy)(nil), // 18: uexecutor.v1.UniversalTxLegacy + (*UniversalAccountId)(nil), // 7: uexecutor.v1.UniversalAccountId + (*RevertInstructions)(nil), // 8: uexecutor.v1.RevertInstructions + (*Inbound)(nil), // 9: uexecutor.v1.Inbound + (*PCTx)(nil), // 10: uexecutor.v1.PCTx + (*OutboundObservation)(nil), // 11: uexecutor.v1.OutboundObservation + (*OriginatingPcTx)(nil), // 12: uexecutor.v1.OriginatingPcTx + (*OutboundTx)(nil), // 13: uexecutor.v1.OutboundTx + (*UniversalTx)(nil), // 14: uexecutor.v1.UniversalTx + (*InboundLegacy)(nil), // 15: uexecutor.v1.InboundLegacy + (*OutboundTxLegacy)(nil), // 16: uexecutor.v1.OutboundTxLegacy + (*UniversalTxLegacy)(nil), // 17: uexecutor.v1.UniversalTxLegacy } var file_uexecutor_v1_types_proto_depIdxs = []int32{ 0, // 0: uexecutor.v1.UniversalPayload.v_type:type_name -> uexecutor.v1.VerificationType 3, // 1: uexecutor.v1.Inbound.tx_type:type_name -> uexecutor.v1.TxType 6, // 2: uexecutor.v1.Inbound.universal_payload:type_name -> uexecutor.v1.UniversalPayload - 9, // 3: uexecutor.v1.Inbound.revert_instructions:type_name -> uexecutor.v1.RevertInstructions + 8, // 3: uexecutor.v1.Inbound.revert_instructions:type_name -> uexecutor.v1.RevertInstructions 3, // 4: uexecutor.v1.OutboundTx.tx_type:type_name -> uexecutor.v1.TxType - 13, // 5: uexecutor.v1.OutboundTx.pc_tx:type_name -> uexecutor.v1.OriginatingPcTx - 12, // 6: uexecutor.v1.OutboundTx.observed_tx:type_name -> uexecutor.v1.OutboundObservation + 12, // 5: uexecutor.v1.OutboundTx.pc_tx:type_name -> uexecutor.v1.OriginatingPcTx + 11, // 6: uexecutor.v1.OutboundTx.observed_tx:type_name -> uexecutor.v1.OutboundObservation 2, // 7: uexecutor.v1.OutboundTx.outbound_status:type_name -> uexecutor.v1.Status - 9, // 8: uexecutor.v1.OutboundTx.revert_instructions:type_name -> uexecutor.v1.RevertInstructions - 11, // 9: uexecutor.v1.OutboundTx.pc_revert_execution:type_name -> uexecutor.v1.PCTx - 11, // 10: uexecutor.v1.OutboundTx.pc_refund_execution:type_name -> uexecutor.v1.PCTx - 10, // 11: uexecutor.v1.UniversalTx.inbound_tx:type_name -> uexecutor.v1.Inbound - 11, // 12: uexecutor.v1.UniversalTx.pc_tx:type_name -> uexecutor.v1.PCTx - 14, // 13: uexecutor.v1.UniversalTx.outbound_tx:type_name -> uexecutor.v1.OutboundTx + 8, // 8: uexecutor.v1.OutboundTx.revert_instructions:type_name -> uexecutor.v1.RevertInstructions + 10, // 9: uexecutor.v1.OutboundTx.pc_revert_execution:type_name -> uexecutor.v1.PCTx + 10, // 10: uexecutor.v1.OutboundTx.pc_refund_execution:type_name -> uexecutor.v1.PCTx + 9, // 11: uexecutor.v1.UniversalTx.inbound_tx:type_name -> uexecutor.v1.Inbound + 10, // 12: uexecutor.v1.UniversalTx.pc_tx:type_name -> uexecutor.v1.PCTx + 13, // 13: uexecutor.v1.UniversalTx.outbound_tx:type_name -> uexecutor.v1.OutboundTx 4, // 14: uexecutor.v1.InboundLegacy.tx_type:type_name -> uexecutor.v1.InboundTxTypeLegacy 6, // 15: uexecutor.v1.InboundLegacy.universal_payload:type_name -> uexecutor.v1.UniversalPayload - 16, // 16: uexecutor.v1.UniversalTxLegacy.inbound_tx:type_name -> uexecutor.v1.InboundLegacy - 11, // 17: uexecutor.v1.UniversalTxLegacy.pc_tx:type_name -> uexecutor.v1.PCTx - 17, // 18: uexecutor.v1.UniversalTxLegacy.outbound_tx:type_name -> uexecutor.v1.OutboundTxLegacy + 15, // 16: uexecutor.v1.UniversalTxLegacy.inbound_tx:type_name -> uexecutor.v1.InboundLegacy + 10, // 17: uexecutor.v1.UniversalTxLegacy.pc_tx:type_name -> uexecutor.v1.PCTx + 16, // 18: uexecutor.v1.UniversalTxLegacy.outbound_tx:type_name -> uexecutor.v1.OutboundTxLegacy 1, // 19: uexecutor.v1.UniversalTxLegacy.universal_status:type_name -> uexecutor.v1.UniversalTxStatus 20, // [20:20] is the sub-list for method output_type 20, // [20:20] is the sub-list for method input_type @@ -12940,18 +12394,6 @@ func file_uexecutor_v1_types_proto_init() { } } file_uexecutor_v1_types_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MigrationPayload); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_uexecutor_v1_types_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UniversalAccountId); i { case 0: return &v.state @@ -12963,7 +12405,7 @@ func file_uexecutor_v1_types_proto_init() { return nil } } - file_uexecutor_v1_types_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_uexecutor_v1_types_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RevertInstructions); i { case 0: return &v.state @@ -12975,7 +12417,7 @@ func file_uexecutor_v1_types_proto_init() { return nil } } - file_uexecutor_v1_types_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_uexecutor_v1_types_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Inbound); i { case 0: return &v.state @@ -12987,7 +12429,7 @@ func file_uexecutor_v1_types_proto_init() { return nil } } - file_uexecutor_v1_types_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_uexecutor_v1_types_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PCTx); i { case 0: return &v.state @@ -12999,7 +12441,7 @@ func file_uexecutor_v1_types_proto_init() { return nil } } - file_uexecutor_v1_types_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_uexecutor_v1_types_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*OutboundObservation); i { case 0: return &v.state @@ -13011,7 +12453,7 @@ func file_uexecutor_v1_types_proto_init() { return nil } } - file_uexecutor_v1_types_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_uexecutor_v1_types_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*OriginatingPcTx); i { case 0: return &v.state @@ -13023,7 +12465,7 @@ func file_uexecutor_v1_types_proto_init() { return nil } } - file_uexecutor_v1_types_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_uexecutor_v1_types_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*OutboundTx); i { case 0: return &v.state @@ -13035,7 +12477,7 @@ func file_uexecutor_v1_types_proto_init() { return nil } } - file_uexecutor_v1_types_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_uexecutor_v1_types_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UniversalTx); i { case 0: return &v.state @@ -13047,7 +12489,7 @@ func file_uexecutor_v1_types_proto_init() { return nil } } - file_uexecutor_v1_types_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + file_uexecutor_v1_types_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*InboundLegacy); i { case 0: return &v.state @@ -13059,7 +12501,7 @@ func file_uexecutor_v1_types_proto_init() { return nil } } - file_uexecutor_v1_types_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + file_uexecutor_v1_types_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*OutboundTxLegacy); i { case 0: return &v.state @@ -13071,7 +12513,7 @@ func file_uexecutor_v1_types_proto_init() { return nil } } - file_uexecutor_v1_types_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + file_uexecutor_v1_types_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UniversalTxLegacy); i { case 0: return &v.state @@ -13090,7 +12532,7 @@ func file_uexecutor_v1_types_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_uexecutor_v1_types_proto_rawDesc, NumEnums: 5, - NumMessages: 14, + NumMessages: 13, NumExtensions: 0, NumServices: 0, }, diff --git a/api/utss/v1/tx.pulsar.go b/api/utss/v1/tx.pulsar.go index e063337e4..93adbc36f 100644 --- a/api/utss/v1/tx.pulsar.go +++ b/api/utss/v1/tx.pulsar.go @@ -2652,6 +2652,7 @@ var ( fd_MsgInitiateFundMigration_signer protoreflect.FieldDescriptor fd_MsgInitiateFundMigration_old_key_id protoreflect.FieldDescriptor fd_MsgInitiateFundMigration_chain protoreflect.FieldDescriptor + fd_MsgInitiateFundMigration_balance protoreflect.FieldDescriptor ) func init() { @@ -2660,6 +2661,7 @@ func init() { fd_MsgInitiateFundMigration_signer = md_MsgInitiateFundMigration.Fields().ByName("signer") fd_MsgInitiateFundMigration_old_key_id = md_MsgInitiateFundMigration.Fields().ByName("old_key_id") fd_MsgInitiateFundMigration_chain = md_MsgInitiateFundMigration.Fields().ByName("chain") + fd_MsgInitiateFundMigration_balance = md_MsgInitiateFundMigration.Fields().ByName("balance") } var _ protoreflect.Message = (*fastReflection_MsgInitiateFundMigration)(nil) @@ -2745,6 +2747,12 @@ func (x *fastReflection_MsgInitiateFundMigration) Range(f func(protoreflect.Fiel return } } + if x.Balance != "" { + value := protoreflect.ValueOfString(x.Balance) + if !f(fd_MsgInitiateFundMigration_balance, value) { + return + } + } } // Has reports whether a field is populated. @@ -2766,6 +2774,8 @@ func (x *fastReflection_MsgInitiateFundMigration) Has(fd protoreflect.FieldDescr return x.OldKeyId != "" case "utss.v1.MsgInitiateFundMigration.chain": return x.Chain != "" + case "utss.v1.MsgInitiateFundMigration.balance": + return x.Balance != "" default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: utss.v1.MsgInitiateFundMigration")) @@ -2788,6 +2798,8 @@ func (x *fastReflection_MsgInitiateFundMigration) Clear(fd protoreflect.FieldDes x.OldKeyId = "" case "utss.v1.MsgInitiateFundMigration.chain": x.Chain = "" + case "utss.v1.MsgInitiateFundMigration.balance": + x.Balance = "" default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: utss.v1.MsgInitiateFundMigration")) @@ -2813,6 +2825,9 @@ func (x *fastReflection_MsgInitiateFundMigration) Get(descriptor protoreflect.Fi case "utss.v1.MsgInitiateFundMigration.chain": value := x.Chain return protoreflect.ValueOfString(value) + case "utss.v1.MsgInitiateFundMigration.balance": + value := x.Balance + return protoreflect.ValueOfString(value) default: if descriptor.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: utss.v1.MsgInitiateFundMigration")) @@ -2839,6 +2854,8 @@ func (x *fastReflection_MsgInitiateFundMigration) Set(fd protoreflect.FieldDescr x.OldKeyId = value.Interface().(string) case "utss.v1.MsgInitiateFundMigration.chain": x.Chain = value.Interface().(string) + case "utss.v1.MsgInitiateFundMigration.balance": + x.Balance = value.Interface().(string) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: utss.v1.MsgInitiateFundMigration")) @@ -2865,6 +2882,8 @@ func (x *fastReflection_MsgInitiateFundMigration) Mutable(fd protoreflect.FieldD panic(fmt.Errorf("field old_key_id of message utss.v1.MsgInitiateFundMigration is not mutable")) case "utss.v1.MsgInitiateFundMigration.chain": panic(fmt.Errorf("field chain of message utss.v1.MsgInitiateFundMigration is not mutable")) + case "utss.v1.MsgInitiateFundMigration.balance": + panic(fmt.Errorf("field balance of message utss.v1.MsgInitiateFundMigration is not mutable")) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: utss.v1.MsgInitiateFundMigration")) @@ -2884,6 +2903,8 @@ func (x *fastReflection_MsgInitiateFundMigration) NewField(fd protoreflect.Field return protoreflect.ValueOfString("") case "utss.v1.MsgInitiateFundMigration.chain": return protoreflect.ValueOfString("") + case "utss.v1.MsgInitiateFundMigration.balance": + return protoreflect.ValueOfString("") default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: utss.v1.MsgInitiateFundMigration")) @@ -2965,6 +2986,10 @@ func (x *fastReflection_MsgInitiateFundMigration) ProtoMethods() *protoiface.Met if l > 0 { n += 1 + l + runtime.Sov(uint64(l)) } + l = len(x.Balance) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } if x.unknownFields != nil { n += len(x.unknownFields) } @@ -2994,6 +3019,13 @@ func (x *fastReflection_MsgInitiateFundMigration) ProtoMethods() *protoiface.Met i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } + if len(x.Balance) > 0 { + i -= len(x.Balance) + copy(dAtA[i:], x.Balance) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.Balance))) + i-- + dAtA[i] = 0x22 + } if len(x.Chain) > 0 { i -= len(x.Chain) copy(dAtA[i:], x.Chain) @@ -3160,6 +3192,38 @@ func (x *fastReflection_MsgInitiateFundMigration) ProtoMethods() *protoiface.Met } x.Chain = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 4: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Balance", wireType) + } + var stringLen uint64 + 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++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.Balance = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -4800,6 +4864,12 @@ type MsgInitiateFundMigration struct { Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"` OldKeyId string `protobuf:"bytes,2,opt,name=old_key_id,json=oldKeyId,proto3" json:"old_key_id,omitempty"` Chain string `protobuf:"bytes,3,opt,name=chain,proto3" json:"chain,omitempty"` // CAIP-2 chain identifier + // Native balance (wei, uint256 decimal) observed by the admin on the old TSS + // address. The chain derives transfer_amount = balance - gas - l1_gas_fee from + // it, using the same fee figures it pins into the migration record, so every + // universal validator signs one amount instead of re-deriving it from a live + // balance that a 1-wei inflow can shift (F-2026-18142). + Balance string `protobuf:"bytes,4,opt,name=balance,proto3" json:"balance,omitempty"` } func (x *MsgInitiateFundMigration) Reset() { @@ -4843,6 +4913,13 @@ func (x *MsgInitiateFundMigration) GetChain() string { return "" } +func (x *MsgInitiateFundMigration) GetBalance() string { + if x != nil { + return x.Balance + } + return "" +} + type MsgInitiateFundMigrationResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -5016,7 +5093,7 @@ var file_utss_v1_tx_proto_rawDesc = []byte{ 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x22, 0x1e, 0x0a, 0x1c, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xaf, 0x01, 0x0a, 0x18, 0x4d, 0x73, 0x67, 0x49, 0x6e, 0x69, 0x74, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xc9, 0x01, 0x0a, 0x18, 0x4d, 0x73, 0x67, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, @@ -5024,68 +5101,70 @@ var file_utss_v1_tx_proto_rawDesc = []byte{ 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x0a, 0x6f, 0x6c, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x6c, 0x64, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x3a, 0x2d, 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x69, - 0x67, 0x6e, 0x65, 0x72, 0x8a, 0xe7, 0xb0, 0x2a, 0x1d, 0x75, 0x74, 0x73, 0x73, 0x2f, 0x4d, 0x73, - 0x67, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x45, 0x0a, 0x20, 0x4d, 0x73, 0x67, 0x49, 0x6e, 0x69, - 0x74, 0x69, 0x61, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x69, - 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x0b, 0x6d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xc9, 0x01, - 0x0a, 0x14, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, - 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, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x69, 0x67, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, - 0x6d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x74, - 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, - 0x48, 0x61, 0x73, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x3a, 0x29, - 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x8a, 0xe7, 0xb0, 0x2a, 0x19, - 0x75, 0x74, 0x73, 0x73, 0x2f, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, - 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x1e, 0x0a, 0x1c, 0x4d, 0x73, 0x67, - 0x56, 0x6f, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xdc, 0x03, 0x0a, 0x03, 0x4d, 0x73, - 0x67, 0x12, 0x4a, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, - 0x73, 0x12, 0x18, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x20, 0x2e, 0x75, 0x74, + 0x52, 0x05, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, + 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, + 0x65, 0x3a, 0x2d, 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x8a, 0xe7, + 0xb0, 0x2a, 0x1d, 0x75, 0x74, 0x73, 0x73, 0x2f, 0x4d, 0x73, 0x67, 0x49, 0x6e, 0x69, 0x74, 0x69, + 0x61, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0x45, 0x0a, 0x20, 0x4d, 0x73, 0x67, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x46, + 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6d, 0x69, 0x67, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xc9, 0x01, 0x0a, 0x14, 0x4d, 0x73, 0x67, 0x56, + 0x6f, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x30, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 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, 0x06, 0x73, 0x69, 0x67, 0x6e, + 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6d, 0x69, 0x67, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x18, + 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x3a, 0x29, 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, + 0x69, 0x67, 0x6e, 0x65, 0x72, 0x8a, 0xe7, 0xb0, 0x2a, 0x19, 0x75, 0x74, 0x73, 0x73, 0x2f, 0x4d, + 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x22, 0x1e, 0x0a, 0x1c, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x46, 0x75, + 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x32, 0xdc, 0x03, 0x0a, 0x03, 0x4d, 0x73, 0x67, 0x12, 0x4a, 0x0a, 0x0c, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x18, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, - 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x65, 0x0a, - 0x15, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x50, - 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x12, 0x21, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x4d, 0x73, 0x67, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, - 0x65, 0x79, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x1a, 0x29, 0x2e, 0x75, 0x74, 0x73, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x54, - 0x73, 0x73, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x11, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, - 0x65, 0x79, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x12, 0x1d, 0x2e, 0x75, 0x74, 0x73, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, 0x65, - 0x79, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x1a, 0x25, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, 0x65, 0x79, - 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x65, 0x0a, 0x15, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, - 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x46, 0x75, - 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x29, 0x2e, 0x75, 0x74, - 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, - 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x11, 0x56, 0x6f, 0x74, 0x65, 0x46, 0x75, - 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x2e, 0x75, 0x74, - 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x46, 0x75, 0x6e, - 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x25, 0x2e, 0x75, 0x74, 0x73, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, - 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x1a, 0x05, 0x80, 0xe7, 0xb0, 0x2a, 0x01, 0x42, 0x8c, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, - 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x42, 0x07, 0x54, 0x78, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x50, 0x01, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x70, 0x75, 0x73, 0x68, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2f, 0x70, 0x75, 0x73, 0x68, 0x2d, 0x63, - 0x68, 0x61, 0x69, 0x6e, 0x2d, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x74, - 0x73, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x74, 0x73, 0x73, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x55, - 0x58, 0x58, 0xaa, 0x02, 0x07, 0x55, 0x74, 0x73, 0x73, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07, 0x55, - 0x74, 0x73, 0x73, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x55, 0x74, 0x73, 0x73, 0x5c, 0x56, 0x31, - 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x08, 0x55, - 0x74, 0x73, 0x73, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x20, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x4d, 0x73, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x65, 0x0a, 0x15, 0x49, 0x6e, 0x69, 0x74, 0x69, + 0x61, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, + 0x12, 0x21, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, 0x49, 0x6e, + 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x63, + 0x65, 0x73, 0x73, 0x1a, 0x29, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, + 0x67, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x50, + 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, + 0x0a, 0x11, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x63, + 0x65, 0x73, 0x73, 0x12, 0x1d, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, + 0x67, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x63, 0x65, + 0x73, 0x73, 0x1a, 0x25, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, + 0x56, 0x6f, 0x74, 0x65, 0x54, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x65, 0x0a, 0x15, 0x49, 0x6e, 0x69, + 0x74, 0x69, 0x61, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x21, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x73, 0x67, + 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x29, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x4d, 0x73, 0x67, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, + 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x59, 0x0a, 0x11, 0x56, 0x6f, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x4d, 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x25, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, + 0x73, 0x67, 0x56, 0x6f, 0x74, 0x65, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x05, 0x80, 0xe7, 0xb0, + 0x2a, 0x01, 0x42, 0x8c, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, + 0x76, 0x31, 0x42, 0x07, 0x54, 0x78, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x37, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x75, 0x73, 0x68, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x2f, 0x70, 0x75, 0x73, 0x68, 0x2d, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2d, 0x6e, + 0x6f, 0x64, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x74, 0x73, 0x73, 0x2f, 0x76, 0x31, 0x3b, + 0x75, 0x74, 0x73, 0x73, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x55, + 0x74, 0x73, 0x73, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07, 0x55, 0x74, 0x73, 0x73, 0x5c, 0x56, 0x31, + 0xe2, 0x02, 0x13, 0x55, 0x74, 0x73, 0x73, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x08, 0x55, 0x74, 0x73, 0x73, 0x3a, 0x3a, 0x56, + 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/api/utss/v1/types.pulsar.go b/api/utss/v1/types.pulsar.go index f9694e7c2..9d3acdaaa 100644 --- a/api/utss/v1/types.pulsar.go +++ b/api/utss/v1/types.pulsar.go @@ -2883,6 +2883,7 @@ var ( fd_FundMigration_gas_price protoreflect.FieldDescriptor fd_FundMigration_gas_limit protoreflect.FieldDescriptor fd_FundMigration_l1_gas_fee protoreflect.FieldDescriptor + fd_FundMigration_transfer_amount protoreflect.FieldDescriptor ) func init() { @@ -2901,6 +2902,7 @@ func init() { fd_FundMigration_gas_price = md_FundMigration.Fields().ByName("gas_price") fd_FundMigration_gas_limit = md_FundMigration.Fields().ByName("gas_limit") fd_FundMigration_l1_gas_fee = md_FundMigration.Fields().ByName("l1_gas_fee") + fd_FundMigration_transfer_amount = md_FundMigration.Fields().ByName("transfer_amount") } var _ protoreflect.Message = (*fastReflection_FundMigration)(nil) @@ -3046,6 +3048,12 @@ func (x *fastReflection_FundMigration) Range(f func(protoreflect.FieldDescriptor return } } + if x.TransferAmount != "" { + value := protoreflect.ValueOfString(x.TransferAmount) + if !f(fd_FundMigration_transfer_amount, value) { + return + } + } } // Has reports whether a field is populated. @@ -3087,6 +3095,8 @@ func (x *fastReflection_FundMigration) Has(fd protoreflect.FieldDescriptor) bool return x.GasLimit != uint64(0) case "utss.v1.FundMigration.l1_gas_fee": return x.L1GasFee != "" + case "utss.v1.FundMigration.transfer_amount": + return x.TransferAmount != "" default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: utss.v1.FundMigration")) @@ -3129,6 +3139,8 @@ func (x *fastReflection_FundMigration) Clear(fd protoreflect.FieldDescriptor) { x.GasLimit = uint64(0) case "utss.v1.FundMigration.l1_gas_fee": x.L1GasFee = "" + case "utss.v1.FundMigration.transfer_amount": + x.TransferAmount = "" default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: utss.v1.FundMigration")) @@ -3184,6 +3196,9 @@ func (x *fastReflection_FundMigration) Get(descriptor protoreflect.FieldDescript case "utss.v1.FundMigration.l1_gas_fee": value := x.L1GasFee return protoreflect.ValueOfString(value) + case "utss.v1.FundMigration.transfer_amount": + value := x.TransferAmount + return protoreflect.ValueOfString(value) default: if descriptor.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: utss.v1.FundMigration")) @@ -3230,6 +3245,8 @@ func (x *fastReflection_FundMigration) Set(fd protoreflect.FieldDescriptor, valu x.GasLimit = value.Uint() case "utss.v1.FundMigration.l1_gas_fee": x.L1GasFee = value.Interface().(string) + case "utss.v1.FundMigration.transfer_amount": + x.TransferAmount = value.Interface().(string) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: utss.v1.FundMigration")) @@ -3276,6 +3293,8 @@ func (x *fastReflection_FundMigration) Mutable(fd protoreflect.FieldDescriptor) panic(fmt.Errorf("field gas_limit of message utss.v1.FundMigration is not mutable")) case "utss.v1.FundMigration.l1_gas_fee": panic(fmt.Errorf("field l1_gas_fee of message utss.v1.FundMigration is not mutable")) + case "utss.v1.FundMigration.transfer_amount": + panic(fmt.Errorf("field transfer_amount of message utss.v1.FundMigration is not mutable")) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: utss.v1.FundMigration")) @@ -3315,6 +3334,8 @@ func (x *fastReflection_FundMigration) NewField(fd protoreflect.FieldDescriptor) return protoreflect.ValueOfUint64(uint64(0)) case "utss.v1.FundMigration.l1_gas_fee": return protoreflect.ValueOfString("") + case "utss.v1.FundMigration.transfer_amount": + return protoreflect.ValueOfString("") default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: utss.v1.FundMigration")) @@ -3431,6 +3452,10 @@ func (x *fastReflection_FundMigration) ProtoMethods() *protoiface.Methods { if l > 0 { n += 1 + l + runtime.Sov(uint64(l)) } + l = len(x.TransferAmount) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } if x.unknownFields != nil { n += len(x.unknownFields) } @@ -3460,6 +3485,13 @@ func (x *fastReflection_FundMigration) ProtoMethods() *protoiface.Methods { i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } + if len(x.TransferAmount) > 0 { + i -= len(x.TransferAmount) + copy(dAtA[i:], x.TransferAmount) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.TransferAmount))) + i-- + dAtA[i] = 0x72 + } if len(x.L1GasFee) > 0 { i -= len(x.L1GasFee) copy(dAtA[i:], x.L1GasFee) @@ -3941,6 +3973,38 @@ func (x *fastReflection_FundMigration) ProtoMethods() *protoiface.Methods { } x.L1GasFee = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 14: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field TransferAmount", wireType) + } + var stringLen uint64 + 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++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.TransferAmount = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -4545,9 +4609,10 @@ type FundMigration struct { InitiatedBlock int64 `protobuf:"varint,8,opt,name=initiated_block,json=initiatedBlock,proto3" json:"initiated_block,omitempty"` CompletedBlock int64 `protobuf:"varint,9,opt,name=completed_block,json=completedBlock,proto3" json:"completed_block,omitempty"` TxHash string `protobuf:"bytes,10,opt,name=tx_hash,json=txHash,proto3" json:"tx_hash,omitempty"` - GasPrice string `protobuf:"bytes,11,opt,name=gas_price,json=gasPrice,proto3" json:"gas_price,omitempty"` // gas price from oracle (wei) - GasLimit uint64 `protobuf:"varint,12,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty"` // gas limit sourced from UniversalCore per chain namespace - L1GasFee string `protobuf:"bytes,13,opt,name=l1_gas_fee,json=l1GasFee,proto3" json:"l1_gas_fee,omitempty"` // L1 data-availability fee (wei) from UniversalCore; 0 for non-L2 chains + GasPrice string `protobuf:"bytes,11,opt,name=gas_price,json=gasPrice,proto3" json:"gas_price,omitempty"` // gas price from oracle (wei) + GasLimit uint64 `protobuf:"varint,12,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty"` // gas limit sourced from UniversalCore per chain namespace + L1GasFee string `protobuf:"bytes,13,opt,name=l1_gas_fee,json=l1GasFee,proto3" json:"l1_gas_fee,omitempty"` // L1 data-availability fee (wei) from UniversalCore; 0 for non-L2 chains + TransferAmount string `protobuf:"bytes,14,opt,name=transfer_amount,json=transferAmount,proto3" json:"transfer_amount,omitempty"` // native amount (wei) to sweep, derived at initiate time as balance - (gas_price * gas_limit) - l1_gas_fee } func (x *FundMigration) Reset() { @@ -4661,6 +4726,13 @@ func (x *FundMigration) GetL1GasFee() string { return "" } +func (x *FundMigration) GetTransferAmount() string { + if x != nil { + return x.TransferAmount + } + return "" +} + var File_utss_v1_types_proto protoreflect.FileDescriptor var file_utss_v1_types_proto_rawDesc = []byte{ @@ -4732,7 +4804,7 @@ var file_utss_v1_types_proto_rawDesc = []byte{ 0x69, 0x67, 0x68, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x73, 0x73, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x74, 0x73, 0x73, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x22, 0xc6, 0x03, 0x0a, 0x0d, 0x46, + 0x09, 0x74, 0x73, 0x73, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x22, 0xef, 0x03, 0x0a, 0x0d, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x0a, 0x6f, 0x6c, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, @@ -4761,48 +4833,50 @@ var file_utss_v1_types_proto_rawDesc = []byte{ 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x1c, 0x0a, 0x0a, 0x6c, 0x31, 0x5f, 0x67, 0x61, 0x73, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x31, 0x47, 0x61, 0x73, - 0x46, 0x65, 0x65, 0x2a, 0x6b, 0x0a, 0x13, 0x54, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, - 0x63, 0x65, 0x73, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x17, 0x54, 0x53, - 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x45, - 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x54, 0x53, 0x53, 0x5f, 0x4b, - 0x45, 0x59, 0x5f, 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, - 0x53, 0x53, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x54, 0x53, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f, - 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, - 0x2a, 0x60, 0x0a, 0x0e, 0x54, 0x73, 0x73, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x54, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, - 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x47, 0x45, 0x4e, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x54, 0x53, - 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x52, 0x45, 0x46, 0x52, 0x45, 0x53, - 0x48, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x54, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x43, 0x45, - 0x53, 0x53, 0x5f, 0x51, 0x55, 0x4f, 0x52, 0x55, 0x4d, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, - 0x10, 0x02, 0x2a, 0x4c, 0x0a, 0x0c, 0x54, 0x73, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x54, 0x53, 0x53, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, - 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x45, - 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x54, 0x53, 0x53, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, - 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x46, 0x49, 0x4e, 0x41, 0x4c, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x01, - 0x2a, 0x56, 0x0a, 0x0e, 0x54, 0x73, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x53, 0x53, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, - 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x54, 0x53, 0x53, 0x5f, - 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, - 0x01, 0x12, 0x15, 0x0a, 0x11, 0x54, 0x53, 0x53, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x45, - 0x58, 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, 0x02, 0x2a, 0x7f, 0x0a, 0x13, 0x46, 0x75, 0x6e, 0x64, - 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x21, 0x0a, 0x1d, 0x46, 0x55, 0x4e, 0x44, 0x5f, 0x4d, 0x49, 0x47, 0x52, 0x41, 0x54, 0x49, 0x4f, - 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, - 0x10, 0x00, 0x12, 0x23, 0x0a, 0x1f, 0x46, 0x55, 0x4e, 0x44, 0x5f, 0x4d, 0x49, 0x47, 0x52, 0x41, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x4f, 0x4d, 0x50, - 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x20, 0x0a, 0x1c, 0x46, 0x55, 0x4e, 0x44, 0x5f, - 0x4d, 0x49, 0x47, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, - 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x42, 0x8f, 0x01, 0x0a, 0x0b, 0x63, 0x6f, - 0x6d, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, 0x76, 0x31, 0x42, 0x0a, 0x54, 0x79, 0x70, 0x65, 0x73, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x75, 0x73, 0x68, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2f, 0x70, 0x75, - 0x73, 0x68, 0x2d, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2d, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x75, 0x74, 0x73, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x74, 0x73, 0x73, 0x76, 0x31, - 0xa2, 0x02, 0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x55, 0x74, 0x73, 0x73, 0x2e, 0x56, 0x31, - 0xca, 0x02, 0x07, 0x55, 0x74, 0x73, 0x73, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x55, 0x74, 0x73, - 0x73, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0xea, 0x02, 0x08, 0x55, 0x74, 0x73, 0x73, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x46, 0x65, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x5f, + 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x2a, 0x6b, 0x0a, 0x13, + 0x54, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x17, 0x54, 0x53, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x50, + 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x00, + 0x12, 0x1b, 0x0a, 0x17, 0x54, 0x53, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x50, 0x52, 0x4f, 0x43, + 0x45, 0x53, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x01, 0x12, 0x1a, 0x0a, + 0x16, 0x54, 0x53, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, + 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x2a, 0x60, 0x0a, 0x0e, 0x54, 0x73, 0x73, + 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x54, + 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x47, 0x45, + 0x4e, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x54, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x43, 0x45, + 0x53, 0x53, 0x5f, 0x52, 0x45, 0x46, 0x52, 0x45, 0x53, 0x48, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, + 0x54, 0x53, 0x53, 0x5f, 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x51, 0x55, 0x4f, 0x52, + 0x55, 0x4d, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x10, 0x02, 0x2a, 0x4c, 0x0a, 0x0c, 0x54, + 0x73, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x54, + 0x53, 0x53, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, + 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, + 0x54, 0x53, 0x53, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x46, 0x49, + 0x4e, 0x41, 0x4c, 0x49, 0x5a, 0x45, 0x44, 0x10, 0x01, 0x2a, 0x56, 0x0a, 0x0e, 0x54, 0x73, 0x73, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x10, 0x54, + 0x53, 0x53, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, + 0x00, 0x12, 0x17, 0x0a, 0x13, 0x54, 0x53, 0x53, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x43, + 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x54, 0x53, + 0x53, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, + 0x02, 0x2a, 0x7f, 0x0a, 0x13, 0x46, 0x75, 0x6e, 0x64, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x1d, 0x46, 0x55, 0x4e, 0x44, + 0x5f, 0x4d, 0x49, 0x47, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x23, 0x0a, 0x1f, 0x46, + 0x55, 0x4e, 0x44, 0x5f, 0x4d, 0x49, 0x47, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x01, + 0x12, 0x20, 0x0a, 0x1c, 0x46, 0x55, 0x4e, 0x44, 0x5f, 0x4d, 0x49, 0x47, 0x52, 0x41, 0x54, 0x49, + 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, + 0x10, 0x02, 0x42, 0x8f, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x75, 0x74, 0x73, 0x73, 0x2e, + 0x76, 0x31, 0x42, 0x0a, 0x54, 0x79, 0x70, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x75, 0x73, + 0x68, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2f, 0x70, 0x75, 0x73, 0x68, 0x2d, 0x63, 0x68, 0x61, 0x69, + 0x6e, 0x2d, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x74, 0x73, 0x73, 0x2f, + 0x76, 0x31, 0x3b, 0x75, 0x74, 0x73, 0x73, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x55, 0x58, 0x58, 0xaa, + 0x02, 0x07, 0x55, 0x74, 0x73, 0x73, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07, 0x55, 0x74, 0x73, 0x73, + 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x55, 0x74, 0x73, 0x73, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, + 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x08, 0x55, 0x74, 0x73, 0x73, + 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/app/README.md b/app/README.md index 0b470db67..3ae9a4ab2 100644 --- a/app/README.md +++ b/app/README.md @@ -151,6 +151,9 @@ Push Chain ships exactly one custom precompile: | `0x00000000000000000000000000000000000000ca` | `usigverifier` (legacy) | Ed25519 signature verification (Solana signatures over `bytes32` digests) | | `0xEC00000000000000000000000000000000000001` | `usigverifier` (v2) | Same implementation, registered at the reserved Push range | +Gas cost: `4000` per `verifyEd25519` call (fixed 32-byte digest), and `4000` plus `12` per 32-byte +word of `message` for `verifyEd25519RawMessage`, whose message is hard-capped at 128 KiB. See +[`precompiles/usigverifier/README.md`](../precompiles/usigverifier/README.md). Both addresses are registered simultaneously for backward compatibility with deployed contracts that have the legacy address hardcoded. Gas cost: `4000` per `verifyEd25519` call. See [`precompiles/usigverifier/README.md`](../precompiles/usigverifier/README.md). The baseline EVM precompiles (`bech32`, `p256`, `staking`, `distribution`, `ics20`, `bank`, `gov`, `slashing`, `evidence`) are wired in via `app/precompiles.go:NewAvailableStaticPrecompiles`. diff --git a/app/ante/account_init_decorator.go b/app/ante/account_init_decorator.go index 29e6d4eaa..2c0b52815 100644 --- a/app/ante/account_init_decorator.go +++ b/app/ante/account_init_decorator.go @@ -1,6 +1,7 @@ package ante import ( + "bytes" "fmt" sdk "github.com/cosmos/cosmos-sdk/types" @@ -12,18 +13,58 @@ import ( codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/cosmos/cosmos-sdk/x/auth/ante" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" txpolicy "github.com/pushchain/push-chain-node/app/txpolicy" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" + utsstypes "github.com/pushchain/push-chain-node/x/utss/types" ) +// validatorOnlyGaslessMsgTypes is the subset of the gasless allowlist that only +// a bonded universal validator can ever execute successfully: every one of these +// msg servers gates on IsBondedUniversalValidator (VoteChainMeta gates on the +// strictly narrower eligible-voter set, of which bonded is a component). +// +// The remaining gasless type - MsgExecutePayload - is deliberately absent: it is +// permissionless by design and creating an account for a first-time universal +// user is the intended behaviour of this decorator. +var validatorOnlyGaslessMsgTypes = map[string]struct{}{ + sdk.MsgTypeURL(&uexecutortypes.MsgVoteInbound{}): {}, + sdk.MsgTypeURL(&uexecutortypes.MsgVoteOutbound{}): {}, + sdk.MsgTypeURL(&uexecutortypes.MsgVoteChainMeta{}): {}, + sdk.MsgTypeURL(&utsstypes.MsgVoteTssKeyProcess{}): {}, + sdk.MsgTypeURL(&utsstypes.MsgVoteFundMigration{}): {}, +} + +// isValidatorOnlyGaslessTx reports whether tx carries at least one message that +// only a bonded universal validator can execute. +// +// authz.MsgExec is deliberately NOT unwrapped. A universal validator submits its +// votes wrapped in authz.MsgExec (universalClient/pushsigner wrapWithAuthZ), and +// there the tx signer is the grantee hotkey while the vote's own signer - the one +// the msg server checks - is the granter. That hotkey is legitimately not a +// universal validator itself, so unwrapping here would reject the real voting +// path. Only a top-level vote message declares the universal validator as the tx +// signer, and that is exactly the case this gate covers. +func isValidatorOnlyGaslessTx(tx sdk.Tx) bool { + for _, msg := range tx.GetMsgs() { + if _, ok := validatorOnlyGaslessMsgTypes[sdk.MsgTypeURL(msg)]; ok { + return true + } + } + return false +} + type AccountInitDecorator struct { ak AccountKeeper + uvk UValidatorKeeper signModeHandler *txsigning.HandlerMap } -func NewAccountInitDecorator(ak AccountKeeper, signModeHandler *txsigning.HandlerMap) AccountInitDecorator { +func NewAccountInitDecorator(ak AccountKeeper, uvk UValidatorKeeper, signModeHandler *txsigning.HandlerMap) AccountInitDecorator { return AccountInitDecorator{ ak: ak, + uvk: uvk, signModeHandler: signModeHandler, } } @@ -55,7 +96,28 @@ func (aid AccountInitDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate "address", sdk.AccAddress(newAccAddr).String(), "simulate", simulate, ) - // if account does not exist on chain, bypass rest of ante chain (especially gas and signature verification) here. + // F-2026-18186: this decorator writes the account row and then returns + // without running the message, so the row survives even when the message + // later fails. For the validator-only vote messages that is a free, + // repeatable state-bloat primitive: a fresh key sends a gasless vote, the + // account is committed by the ante cache, and the msg server then rejects + // it for not being a bonded universal validator. + // + // Reject those before any account is created - and before the expensive + // signature verification below. A universal validator that can legitimately + // vote is bonded and therefore already has an account, so this path should + // never legitimately create one for a vote. + if isValidatorOnlyGaslessTx(tx) { + if err := aid.requireBondedUniversalValidator(ctx, newAccAddr); err != nil { + ctx.Logger().Debug("account init decorator: rejecting validator-only gasless tx from non-validator signer", + "address", sdk.AccAddress(newAccAddr).String(), + "error", err, + ) + return ctx, err + } + } + + // if account does not exist on chain, bypass rest of ante chain here. // Perform signature verification on account number e and sequence number e instead. if err := aid.verifySignatureForNewAccount(ctx, tx, simulate); err != nil { ctx.Logger().Debug("account init decorator: signature verification failed for new account", @@ -80,6 +142,33 @@ func (aid AccountInitDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate return next(ctx, tx, simulate) } +// requireBondedUniversalValidator returns nil only when signer is a bonded +// universal validator. +// +// IsBondedUniversalValidator takes the bech32 ACCOUNT address (it derives the +// operator address from those bytes itself), which is the same string the vote +// msg servers hand it as msg.Signer. It returns an error - not (false, nil) - +// when the signer is absent from the universal validator set, so both branches +// have to be treated as a rejection; failing closed is correct here because the +// only thing being denied is the creation of an account row for a message that +// cannot succeed. +func (aid AccountInitDecorator) requireBondedUniversalValidator(ctx sdk.Context, signer sdk.AccAddress) error { + if aid.uvk == nil { + return errorsmod.Wrap(sdkerrors.ErrLogic, "uvalidator keeper not configured on account init decorator") + } + + bonded, err := aid.uvk.IsBondedUniversalValidator(ctx, signer.String()) + if err != nil { + return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, + "signer %s may not create an account with a validator-only gasless message: %s", signer.String(), err.Error()) + } + if !bonded { + return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, + "signer %s may not create an account with a validator-only gasless message: not a bonded universal validator", signer.String()) + } + return nil +} + func (aid AccountInitDecorator) verifySignatureForNewAccount(ctx sdk.Context, tx sdk.Tx, simulate bool) error { sigTx, ok := tx.(authsigning.Tx) if !ok { @@ -103,13 +192,52 @@ func (aid AccountInitDecorator) verifySignatureForNewAccount(ctx sdk.Context, tx return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "invalid number of signer; expected: %d, got %d", len(signers), len(sigs)) } - newAccAddr := sdk.AccAddress(signers[0]) + params := aid.ak.GetParams(ctx) + + // Enforce the signature count limit before doing any verification work. + // This decorator short-circuits the ante chain for new accounts, so + // ante.ValidateSigCountDecorator never runs for them; without this hard cap + // a gasless tx could carry an arbitrarily large multisig key and force the + // node to verify every sub-signature. Gas is deliberately NOT consumed here: + // gasless txs skip fee deduction entirely, so charging gas would cost an + // attacker nothing - the count cap is what actually bounds the work. + sigCount := 0 for _, sig := range sigs { + if sig.PubKey == nil { + return errorsmod.Wrap(sdkerrors.ErrInvalidPubKey, "pubkey is not provided in signature") + } + sigCount += ante.CountSubKeys(sig.PubKey) + if uint64(sigCount) > params.TxSigLimit { + return errorsmod.Wrapf(sdkerrors.ErrTooManySignatures, + "signatures: %d, limit: %d", sigCount, params.TxSigLimit) + } + } + + newAccAddr := sdk.AccAddress(signers[0]) + for i, sig := range sigs { pubKey := sig.PubKey if pubKey == nil { return errorsmod.Wrap(sdkerrors.ErrInvalidPubKey, "pubkey is not provided in signature") } + // Bind the declared signer to the key that actually signed the tx. + // + // VerifySignature below only proves "this key signed this tx"; it says + // nothing about WHO the tx claims to be from. Because this decorator + // short-circuits the ante chain for new accounts, the SDK's + // SetPubKeyDecorator - which owns this check - never runs, so a tx could + // declare an arbitrary signer while being signed by an unrelated key. + // Bech32 account addresses may be up to 255 bytes, and downstream + // conversion to a 20-byte EVM address keeps only the rightmost bytes, so + // a crafted longer signer could alias a module address. + // + // Guards mirror x/auth/ante/sigverify.go exactly so simulation and gas + // estimation keep working. + if !simulate && ctx.IsSigverifyTx() && !bytes.Equal(pubKey.Address().Bytes(), signers[i]) { + return errorsmod.Wrapf(sdkerrors.ErrInvalidPubKey, + "pubKey does not match signer address %s with signer index: %d", sdk.AccAddress(signers[i]).String(), i) + } + // retrieve signer data chainID := ctx.ChainID() var accSequence uint64 = 0 diff --git a/app/ante/account_init_decorator_test.go b/app/ante/account_init_decorator_test.go index 8b128431c..1d6715f7b 100644 --- a/app/ante/account_init_decorator_test.go +++ b/app/ante/account_init_decorator_test.go @@ -18,7 +18,7 @@ import ( // gasless message type list). func TestAccountInitDecorator_NonGaslessTxPassesThrough(t *testing.T) { ak := newMockAccountKeeperAnte(sdk.AccAddress([]byte("feeCollector"))) - aid := ante.NewAccountInitDecorator(ak, nil /*signModeHandler not needed for non-gasless*/) + aid := ante.NewAccountInitDecorator(ak, newMockUValidatorKeeperAnte(), nil /*signModeHandler not needed for non-gasless*/) // banktypes.MsgSend is not gasless. tx := mockFeeTx{ @@ -45,7 +45,7 @@ func TestAccountInitDecorator_GaslessTxExistingAccountPassesThrough(t *testing.T // Pre-register the account. ak.SetAccount(context.Background(), authtypes.NewBaseAccountWithAddress(existingAddr)) - aid := ante.NewAccountInitDecorator(ak, nil) + aid := ante.NewAccountInitDecorator(ak, newMockUValidatorKeeperAnte(), nil) // Use a non-authsigning tx — the decorator skips signature verification // for existing accounts only when it can parse signers. Since mockFeeTx doesn't @@ -76,7 +76,7 @@ func TestAccountInitDecorator_GaslessTxExistingAccountPassesThrough(t *testing.T // tx that does not implement authsigning.Tx is rejected with ErrTxDecode. func TestAccountInitDecorator_NonAuthSigningTxReturnsError(t *testing.T) { ak := newMockAccountKeeperAnte(sdk.AccAddress([]byte("feeCollector"))) - aid := ante.NewAccountInitDecorator(ak, nil) + aid := ante.NewAccountInitDecorator(ak, newMockUValidatorKeeperAnte(), nil) // MsgVoteInbound is gasless. tx := mockFeeTx{ diff --git a/app/ante/account_init_signer_binding_test.go b/app/ante/account_init_signer_binding_test.go new file mode 100644 index 000000000..80ac6e6c8 --- /dev/null +++ b/app/ante/account_init_signer_binding_test.go @@ -0,0 +1,295 @@ +package ante_test + +import ( + "context" + "fmt" + "testing" + + kmultisig "github.com/cosmos/cosmos-sdk/crypto/keys/multisig" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + clienttx "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/std" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/authz" + + "github.com/pushchain/push-chain-node/app/ante" + appparams "github.com/pushchain/push-chain-node/app/params" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" + utsstypes "github.com/pushchain/push-chain-node/x/utss/types" +) + +// uexecutorModuleEVMAddr is the EVM address of the uexecutor module account - +// sha256("uexecutor")[:20]. The UEA contract trusts calls coming from it +// unconditionally, which is what makes aliasing onto it so damaging. +const uexecutorModuleEVMAddr = "0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7" + +const anteTestChainID = "push_9000-1" + +// newSignerBindingEncodingConfig returns an encoding config able to build and +// sign real uexecutor transactions. +func newSignerBindingEncodingConfig(t *testing.T) appparams.EncodingConfig { + t.Helper() + encCfg := appparams.MakeEncodingConfig() + std.RegisterInterfaces(encCfg.InterfaceRegistry) + authtypes.RegisterInterfaces(encCfg.InterfaceRegistry) + uexecutortypes.RegisterInterfaces(encCfg.InterfaceRegistry) + utsstypes.RegisterInterfaces(encCfg.InterfaceRegistry) + authz.RegisterInterfaces(encCfg.InterfaceRegistry) + return encCfg +} + +// aliasedSigner returns a `length`-byte address whose RIGHTMOST 20 bytes are the +// uexecutor module account. common.BytesToAddress keeps exactly those bytes, so +// every such address collapses onto the module's EVM address. +func aliasedSigner(t *testing.T, length int) sdk.AccAddress { + t.Helper() + require.Greater(t, length, common.AddressLength) + + moduleAddr := authtypes.NewModuleAddress(uexecutortypes.ModuleName) + require.Len(t, moduleAddr, common.AddressLength) + require.Equal(t, uexecutorModuleEVMAddr, common.BytesToAddress(moduleAddr).Hex()) + + prefix := make([]byte, length-common.AddressLength) + prefix[0] = 0x01 + addr := sdk.AccAddress(append(prefix, moduleAddr...)) + require.Len(t, addr, length) + + // The whole point of the finding: this longer address truncates onto the + // module's EVM address downstream. + require.Equal(t, uexecutorModuleEVMAddr, common.BytesToAddress(addr).Hex()) + return addr +} + +// gaslessMsgFor builds a user-facing gasless message with the given declared +// signer. +func gaslessMsgFor(t *testing.T, msgType string, signer sdk.AccAddress) sdk.Msg { + t.Helper() + ua := &uexecutortypes.UniversalAccountId{ + ChainNamespace: "eip155", + ChainId: "11155111", + Owner: "0x000000000000000000000000000000000000dead", + } + + switch msgType { + case "MsgExecutePayload": + return &uexecutortypes.MsgExecutePayload{ + Signer: signer.String(), + UniversalAccountId: ua, + UniversalPayload: &uexecutortypes.UniversalPayload{ + To: "0x000000000000000000000000000000000000dead", + Data: "0xabcdef", + }, + VerificationData: "0xabcdef", + } + default: + t.Fatalf("unknown msg type %q", msgType) + return nil + } +} + +// buildSignedTx returns a tx carrying msg whose declared signer is +// `declaredSigner` but which is signed by `priv` - the two need not be related, +// which is exactly the confusion the fix has to reject. +func buildSignedTx(t *testing.T, encCfg appparams.EncodingConfig, msg sdk.Msg, declaredSigner sdk.AccAddress, priv cryptotypes.PrivKey) sdk.Tx { + t.Helper() + + txb := encCfg.TxConfig.NewTxBuilder() + require.NoError(t, txb.SetMsgs(msg)) + txb.SetGasLimit(300_000) + + require.NoError(t, txb.SetSignatures(signing.SignatureV2{ + PubKey: priv.PubKey(), + Data: &signing.SingleSignatureData{SignMode: signing.SignMode_SIGN_MODE_DIRECT}, + Sequence: 0, + })) + + // The gasless new-account path signs over account number 0 / sequence 0, + // since the account does not exist on chain yet. + signerData := authsigning.SignerData{ + Address: declaredSigner.String(), + ChainID: anteTestChainID, + AccountNumber: 0, + Sequence: 0, + PubKey: priv.PubKey(), + } + + sigV2, err := clienttx.SignWithPrivKey( + context.Background(), signing.SignMode_SIGN_MODE_DIRECT, signerData, + txb, priv, encCfg.TxConfig, 0, + ) + require.NoError(t, err) + require.NoError(t, txb.SetSignatures(sigV2)) + + return txb.GetTx() +} + +func newSignerBindingDecorator(t *testing.T, encCfg appparams.EncodingConfig) (ante.AccountInitDecorator, *mockAccountKeeperAnte) { + t.Helper() + ak := newMockAccountKeeperAnte(sdk.AccAddress([]byte("feeCollector"))) + // The uvalidator mock knows about nobody, so it rejects every address it is + // asked about. Every test in this file uses MsgExecutePayload, which is + // deliberately NOT gated on validator status (F-2026-18186), so it must keep + // working against it. + return ante.NewAccountInitDecorator(ak, newMockUValidatorKeeperAnte(), encCfg.TxConfig.SignModeHandler()), ak +} + +// TestAccountInitDecorator_RejectsAliasedModuleSigner is the regression test for +// F-2026-18200: a gasless tx may not declare an over-long signer that truncates +// onto the uexecutor module address while being signed by an unrelated key. +// +// Hacken's PoC only used the 21-byte case; truncation works for ANY length > 20, +// so 21, 22 and 32 bytes are all covered. +func TestAccountInitDecorator_RejectsAliasedModuleSigner(t *testing.T) { + encCfg := newSignerBindingEncodingConfig(t) + + for _, msgType := range []string{"MsgExecutePayload"} { + for _, length := range []int{21, 22, 32} { + t.Run(fmt.Sprintf("%s/%dbytes", msgType, length), func(t *testing.T) { + attackerKey := secp256k1.GenPrivKey() + declaredSigner := aliasedSigner(t, length) + msg := gaslessMsgFor(t, msgType, declaredSigner) + tx := buildSignedTx(t, encCfg, msg, declaredSigner, attackerKey) + + aid, ak := newSignerBindingDecorator(t, encCfg) + ctx := newAnteTestCtx(t, false).WithChainID(anteTestChainID) + + nextCalled := false + _, err := aid.AnteHandle(ctx, tx, false, func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + nextCalled = true + return ctx, nil + }) + + require.Error(t, err, "aliased signer must not pass the ante chain") + require.True(t, sdkerrors.ErrInvalidPubKey.Is(err), "expected ErrInvalidPubKey, got: %v", err) + require.False(t, nextCalled, "the message must never reach execution") + require.False(t, ak.HasAccount(context.Background(), declaredSigner), + "no account may be persisted for a rejected signer") + }) + } + } +} + +// TestAccountInitDecorator_RejectsMismatchedSigner covers the general case: a +// well-formed 20-byte signer that is simply not the address of the signing key. +func TestAccountInitDecorator_RejectsMismatchedSigner(t *testing.T) { + encCfg := newSignerBindingEncodingConfig(t) + + attackerKey := secp256k1.GenPrivKey() + victimKey := secp256k1.GenPrivKey() + declaredSigner := sdk.AccAddress(victimKey.PubKey().Address()) + + msg := gaslessMsgFor(t, "MsgExecutePayload", declaredSigner) + tx := buildSignedTx(t, encCfg, msg, declaredSigner, attackerKey) + + aid, ak := newSignerBindingDecorator(t, encCfg) + ctx := newAnteTestCtx(t, false).WithChainID(anteTestChainID) + + _, err := aid.AnteHandle(ctx, tx, false, emptyNext) + require.Error(t, err) + require.True(t, sdkerrors.ErrInvalidPubKey.Is(err), "expected ErrInvalidPubKey, got: %v", err) + require.False(t, ak.HasAccount(context.Background(), declaredSigner)) +} + +// TestAccountInitDecorator_AcceptsMatchingSigner is the positive control: a +// normal 20-byte signer whose key matches still creates the account and passes. +func TestAccountInitDecorator_AcceptsMatchingSigner(t *testing.T) { + encCfg := newSignerBindingEncodingConfig(t) + + for _, msgType := range []string{"MsgExecutePayload"} { + t.Run(msgType, func(t *testing.T) { + key := secp256k1.GenPrivKey() + signer := sdk.AccAddress(key.PubKey().Address()) + require.Len(t, signer, common.AddressLength) + + msg := gaslessMsgFor(t, msgType, signer) + tx := buildSignedTx(t, encCfg, msg, signer, key) + + aid, ak := newSignerBindingDecorator(t, encCfg) + ctx := newAnteTestCtx(t, false).WithChainID(anteTestChainID) + + _, err := aid.AnteHandle(ctx, tx, false, emptyNext) + require.NoError(t, err) + + acc := ak.GetAccount(context.Background(), signer) + require.NotNil(t, acc, "the account must be created for a legitimate gasless tx") + require.Equal(t, uint64(1), acc.GetSequence()) + }) + } +} + +// TestAccountInitDecorator_SimulationUnaffected checks that the new binding +// check keeps the SDK's `!simulate` guard, so simulation and gas estimation - +// which carry no usable signature - keep working. +func TestAccountInitDecorator_SimulationUnaffected(t *testing.T) { + encCfg := newSignerBindingEncodingConfig(t) + + attackerKey := secp256k1.GenPrivKey() + victimKey := secp256k1.GenPrivKey() + + for name, declaredSigner := range map[string]sdk.AccAddress{ + "matching_signer": sdk.AccAddress(attackerKey.PubKey().Address()), + "mismatched_signer": sdk.AccAddress(victimKey.PubKey().Address()), + } { + t.Run(name, func(t *testing.T) { + msg := gaslessMsgFor(t, "MsgExecutePayload", declaredSigner) + tx := buildSignedTx(t, encCfg, msg, declaredSigner, attackerKey) + + aid, _ := newSignerBindingDecorator(t, encCfg) + ctx := newAnteTestCtx(t, false).WithChainID(anteTestChainID) + + _, err := aid.AnteHandle(ctx, tx, true /* simulate */, emptyNext) + require.NoError(t, err, "simulation must not be affected by the binding check") + }) + } +} + +// TestAccountInitDecorator_EnforcesSignatureLimit covers F-2026-18186: the +// new-account path short-circuits the ante chain, so it has to enforce the +// signature count limit itself instead of verifying an unbounded multisig for +// free. +func TestAccountInitDecorator_EnforcesSignatureLimit(t *testing.T) { + encCfg := newSignerBindingEncodingConfig(t) + + params := authtypes.DefaultParams() + numKeys := int(params.TxSigLimit) + 1 + + pubKeys := make([]cryptotypes.PubKey, numKeys) + sigs := make([]signing.SignatureData, numKeys) + bitArray := cryptotypes.NewCompactBitArray(numKeys) + for i := 0; i < numKeys; i++ { + pubKeys[i] = secp256k1.GenPrivKey().PubKey() + sigs[i] = &signing.SingleSignatureData{ + SignMode: signing.SignMode_SIGN_MODE_DIRECT, + Signature: []byte("not-checked-the-limit-trips-first"), + } + bitArray.SetIndex(i, true) + } + + multisigPk := kmultisig.NewLegacyAminoPubKey(numKeys, pubKeys) + signer := sdk.AccAddress(multisigPk.Address()) + + txb := encCfg.TxConfig.NewTxBuilder() + require.NoError(t, txb.SetMsgs(gaslessMsgFor(t, "MsgExecutePayload", signer))) + txb.SetGasLimit(300_000) + require.NoError(t, txb.SetSignatures(signing.SignatureV2{ + PubKey: multisigPk, + Data: &signing.MultiSignatureData{BitArray: bitArray, Signatures: sigs}, + Sequence: 0, + })) + + aid, ak := newSignerBindingDecorator(t, encCfg) + ctx := newAnteTestCtx(t, false).WithChainID(anteTestChainID) + + _, err := aid.AnteHandle(ctx, txb.GetTx(), false, emptyNext) + require.Error(t, err) + require.True(t, sdkerrors.ErrTooManySignatures.Is(err), "expected ErrTooManySignatures, got: %v", err) + require.False(t, ak.HasAccount(context.Background(), signer)) +} diff --git a/app/ante/account_init_validator_gate_test.go b/app/ante/account_init_validator_gate_test.go new file mode 100644 index 000000000..953d78715 --- /dev/null +++ b/app/ante/account_init_validator_gate_test.go @@ -0,0 +1,318 @@ +package ante_test + +import ( + "context" + "fmt" + "testing" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/authz" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app/ante" + appparams "github.com/pushchain/push-chain-node/app/params" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" + utsstypes "github.com/pushchain/push-chain-node/x/utss/types" +) + +// --------------------------------------------------------------------------- +// mock uvalidator keeper +// --------------------------------------------------------------------------- + +// mockUValidatorKeeperAnte satisfies ante.UValidatorKeeper and mirrors the real +// keeper's return shape, which matters: x/uvalidator's +// IsBondedUniversalValidator returns an ERROR (not (false, nil)) for an address +// that is absent from the universal validator set, and (false, nil) only for a +// registered-but-unbonded one. Both have to be treated as a rejection. +type mockUValidatorKeeperAnte struct { + // registered maps bech32 account address -> bonded. + registered map[string]bool +} + +func newMockUValidatorKeeperAnte(bonded ...sdk.AccAddress) *mockUValidatorKeeperAnte { + m := &mockUValidatorKeeperAnte{registered: map[string]bool{}} + for _, addr := range bonded { + m.registered[addr.String()] = true + } + return m +} + +// withUnbonded registers an address that is in the universal validator set but +// whose stake is not bonded - the (false, nil) branch of the real keeper. +func (m *mockUValidatorKeeperAnte) withUnbonded(addr sdk.AccAddress) *mockUValidatorKeeperAnte { + m.registered[addr.String()] = false + return m +} + +func (m *mockUValidatorKeeperAnte) IsBondedUniversalValidator(_ context.Context, universalValidator string) (bool, error) { + bonded, ok := m.registered[universalValidator] + if !ok { + return false, fmt.Errorf("validator %s not present in the registered universal validators set", universalValidator) + } + return bonded, nil +} + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +// validatorOnlyMsgTypes are the five gasless message types that only a bonded +// universal validator can ever execute successfully. +var validatorOnlyMsgTypes = []string{ + "MsgVoteInbound", + "MsgVoteOutbound", + "MsgVoteChainMeta", + "MsgVoteTssKeyProcess", + "MsgVoteFundMigration", +} + +// voteMsgFor builds one of the five validator-only gasless messages with the +// given declared signer. +func voteMsgFor(t *testing.T, msgType string, signer sdk.AccAddress) sdk.Msg { + t.Helper() + switch msgType { + case "MsgVoteInbound": + return &uexecutortypes.MsgVoteInbound{Signer: signer.String()} + case "MsgVoteOutbound": + return &uexecutortypes.MsgVoteOutbound{Signer: signer.String(), TxId: "0xdead", UtxId: "0xbeef"} + case "MsgVoteChainMeta": + return &uexecutortypes.MsgVoteChainMeta{ + Signer: signer.String(), + ObservedChainId: "eip155:11155111", + Price: 1, + ChainHeight: 2, + } + case "MsgVoteTssKeyProcess": + return &utsstypes.MsgVoteTssKeyProcess{Signer: signer.String(), TssPubkey: "0xpub", KeyId: "key-1", ProcessId: 1} + case "MsgVoteFundMigration": + return &utsstypes.MsgVoteFundMigration{Signer: signer.String(), MigrationId: 1, TxHash: "0xdead", Success: true} + default: + t.Fatalf("unknown vote msg type %q", msgType) + return nil + } +} + +// newGateDecorator builds the decorator under test with a uvalidator mock that +// knows only about `bondedUVs`. +func newGateDecorator(t *testing.T, encCfg appparams.EncodingConfig, uvk *mockUValidatorKeeperAnte) (ante.AccountInitDecorator, *mockAccountKeeperAnte) { + t.Helper() + ak := newMockAccountKeeperAnte(sdk.AccAddress([]byte("feeCollector"))) + return ante.NewAccountInitDecorator(ak, uvk, encCfg.TxConfig.SignModeHandler()), ak +} + +// --------------------------------------------------------------------------- +// F-2026-18186 - the finding itself +// --------------------------------------------------------------------------- + +// TestAccountInitDecorator_VoteFromFreshSignerCreatesNoAccount is the regression +// test for F-2026-18186 (remediation 3). +// +// AccountInitDecorator writes the account row and then returns WITHOUT running +// the message, so the row survives even though the message subsequently fails. +// For the five validator-only vote messages that is a free, repeatable +// state-bloat primitive: a fresh key sends a gasless vote, the ante cache +// commits the account, and the msg server then rejects the vote because the +// signer is not a bonded universal validator. +// +// The load-bearing assertion is HasAccount == false; it is asserted BEFORE the +// error assertion on purpose, because require.Error aborts the subtest and would +// otherwise mask a vacuous pass. +func TestAccountInitDecorator_VoteFromFreshSignerCreatesNoAccount(t *testing.T) { + encCfg := newSignerBindingEncodingConfig(t) + + for _, msgType := range validatorOnlyMsgTypes { + t.Run(msgType, func(t *testing.T) { + key := secp256k1.GenPrivKey() + signer := sdk.AccAddress(key.PubKey().Address()) + + // Correctly signed by its own key: the tx is valid in every respect + // except that the signer is not a universal validator. + tx := buildSignedTx(t, encCfg, voteMsgFor(t, msgType, signer), signer, key) + + // The uvalidator mock knows about nobody: this signer is a fresh key. + aid, ak := newGateDecorator(t, encCfg, newMockUValidatorKeeperAnte()) + ctx := newAnteTestCtx(t, false).WithChainID(anteTestChainID) + + nextCalled := false + _, err := aid.AnteHandle(ctx, tx, false, func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + nextCalled = true + return ctx, nil + }) + + // THE finding: no account row may be written for a message that + // cannot succeed. Asserted first so a vacuous test cannot hide. + require.False(t, ak.HasAccount(context.Background(), signer), + "F-2026-18186: no account row may be persisted for a gasless vote from a non-validator signer") + + require.Error(t, err, "a gasless vote from a non-validator signer must be rejected") + require.True(t, sdkerrors.ErrUnauthorized.Is(err), "expected ErrUnauthorized, got: %v", err) + require.False(t, nextCalled, "the message must never reach execution") + }) + } +} + +// TestAccountInitDecorator_VoteFromRegisteredButUnbondedSigner covers the other +// rejection branch of the real keeper: an address that IS in the universal +// validator set but whose stake is not bonded returns (false, nil) rather than +// an error, and must be rejected just the same. +func TestAccountInitDecorator_VoteFromRegisteredButUnbondedSigner(t *testing.T) { + encCfg := newSignerBindingEncodingConfig(t) + + key := secp256k1.GenPrivKey() + signer := sdk.AccAddress(key.PubKey().Address()) + tx := buildSignedTx(t, encCfg, voteMsgFor(t, "MsgVoteInbound", signer), signer, key) + + aid, ak := newGateDecorator(t, encCfg, newMockUValidatorKeeperAnte().withUnbonded(signer)) + ctx := newAnteTestCtx(t, false).WithChainID(anteTestChainID) + + _, err := aid.AnteHandle(ctx, tx, false, emptyNext) + + require.False(t, ak.HasAccount(context.Background(), signer), + "a registered-but-unbonded signer must not get an account row either") + require.Error(t, err) + require.True(t, sdkerrors.ErrUnauthorized.Is(err), "expected ErrUnauthorized, got: %v", err) + require.Contains(t, err.Error(), "not a bonded universal validator") +} + +// TestAccountInitDecorator_GateRunsBeforeSignatureVerification pins the ordering. +// The gate is meant to reject before the expensive signature verification, so a +// vote tx that is BOTH signed by an unrelated key AND sent from a non-validator +// signer must come back with the validator rejection, not ErrInvalidPubKey. +func TestAccountInitDecorator_GateRunsBeforeSignatureVerification(t *testing.T) { + encCfg := newSignerBindingEncodingConfig(t) + + attackerKey := secp256k1.GenPrivKey() + victimKey := secp256k1.GenPrivKey() + declaredSigner := sdk.AccAddress(victimKey.PubKey().Address()) + + tx := buildSignedTx(t, encCfg, voteMsgFor(t, "MsgVoteInbound", declaredSigner), declaredSigner, attackerKey) + + aid, ak := newGateDecorator(t, encCfg, newMockUValidatorKeeperAnte()) + ctx := newAnteTestCtx(t, false).WithChainID(anteTestChainID) + + _, err := aid.AnteHandle(ctx, tx, false, emptyNext) + + require.False(t, ak.HasAccount(context.Background(), declaredSigner)) + require.Error(t, err) + require.True(t, sdkerrors.ErrUnauthorized.Is(err), + "the validator gate must fire before signature verification, got: %v", err) + require.False(t, sdkerrors.ErrInvalidPubKey.Is(err)) +} + +// --------------------------------------------------------------------------- +// no regression: the legitimate paths +// --------------------------------------------------------------------------- + +// TestAccountInitDecorator_BondedValidatorVoteStillWorks is the positive control +// for the gate: a bonded universal validator's vote passes it, for all five +// message types. +// +// Both sub-cases matter. In practice a bonded universal validator already has an +// account, so it takes the "existing account" branch and reaches next(); the +// no-account variant proves the gate itself is not what would reject it if it +// somehow did not. +func TestAccountInitDecorator_BondedValidatorVoteStillWorks(t *testing.T) { + encCfg := newSignerBindingEncodingConfig(t) + + for _, msgType := range validatorOnlyMsgTypes { + t.Run(msgType+"/no_account_yet", func(t *testing.T) { + key := secp256k1.GenPrivKey() + signer := sdk.AccAddress(key.PubKey().Address()) + tx := buildSignedTx(t, encCfg, voteMsgFor(t, msgType, signer), signer, key) + + aid, ak := newGateDecorator(t, encCfg, newMockUValidatorKeeperAnte(signer)) + ctx := newAnteTestCtx(t, false).WithChainID(anteTestChainID) + + _, err := aid.AnteHandle(ctx, tx, false, emptyNext) + require.NoError(t, err, "a bonded universal validator must not be rejected by the gate") + + acc := ak.GetAccount(context.Background(), signer) + require.NotNil(t, acc, "the bonded validator's account is still created") + require.Equal(t, uint64(1), acc.GetSequence()) + }) + + t.Run(msgType+"/existing_account", func(t *testing.T) { + key := secp256k1.GenPrivKey() + signer := sdk.AccAddress(key.PubKey().Address()) + tx := buildSignedTx(t, encCfg, voteMsgFor(t, msgType, signer), signer, key) + + aid, ak := newGateDecorator(t, encCfg, newMockUValidatorKeeperAnte(signer)) + ak.SetAccount(context.Background(), authtypes.NewBaseAccountWithAddress(signer)) + ctx := newAnteTestCtx(t, false).WithChainID(anteTestChainID) + + nextCalled := false + _, err := aid.AnteHandle(ctx, tx, false, func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + nextCalled = true + return ctx, nil + }) + require.NoError(t, err) + require.True(t, nextCalled, "an existing account must still fall through to the rest of the ante chain") + }) + } +} + +// TestAccountInitDecorator_PermissionlessGaslessMsgsUngated proves the scoping. +// MsgExecutePayload is permissionless by design: a first-time universal user has +// no account and no validator status, and creating the account for them is the +// intended behaviour of this decorator. Gating it would break real users, so it +// must still work against a uvalidator keeper that rejects every address. +func TestAccountInitDecorator_PermissionlessGaslessMsgsUngated(t *testing.T) { + encCfg := newSignerBindingEncodingConfig(t) + + for _, msgType := range []string{"MsgExecutePayload"} { + t.Run(msgType, func(t *testing.T) { + key := secp256k1.GenPrivKey() + signer := sdk.AccAddress(key.PubKey().Address()) + tx := buildSignedTx(t, encCfg, gaslessMsgFor(t, msgType, signer), signer, key) + + // Knows about nobody: it would reject the signer if it were consulted. + aid, ak := newGateDecorator(t, encCfg, newMockUValidatorKeeperAnte()) + ctx := newAnteTestCtx(t, false).WithChainID(anteTestChainID) + + _, err := aid.AnteHandle(ctx, tx, false, emptyNext) + require.NoError(t, err, "%s is permissionless and must not be gated on validator status", msgType) + + acc := ak.GetAccount(context.Background(), signer) + require.NotNil(t, acc, "a first-time universal user must still get an account") + require.Equal(t, uint64(1), acc.GetSequence()) + }) + } +} + +// TestAccountInitDecorator_AuthzWrappedVoteUngated pins the deliberate decision +// not to unwrap authz.MsgExec. +// +// A universal validator submits its votes wrapped in authz.MsgExec +// (universalClient/pushsigner wrapWithAuthZ). There the TX signer is the grantee +// hotkey while the vote's own signer - the address the msg server checks - is the +// granter. The hotkey is legitimately not a universal validator, so unwrapping +// here would reject the real voting path. +func TestAccountInitDecorator_AuthzWrappedVoteUngated(t *testing.T) { + encCfg := newSignerBindingEncodingConfig(t) + + hotKey := secp256k1.GenPrivKey() + grantee := sdk.AccAddress(hotKey.PubKey().Address()) + granter := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + inner, err := codectypes.NewAnyWithValue(voteMsgFor(t, "MsgVoteInbound", granter)) + require.NoError(t, err) + execMsg := &authz.MsgExec{Grantee: grantee.String(), Msgs: []*codectypes.Any{inner}} + + tx := buildSignedTx(t, encCfg, execMsg, grantee, hotKey) + + // Neither the hotkey nor the granter is known to the mock; only the absence of + // the gate can let this through. + aid, ak := newGateDecorator(t, encCfg, newMockUValidatorKeeperAnte()) + ctx := newAnteTestCtx(t, false).WithChainID(anteTestChainID) + + _, err = aid.AnteHandle(ctx, tx, false, emptyNext) + require.NoError(t, err, "the authz-wrapped voting path must keep working for a fresh grantee hotkey") + + acc := ak.GetAccount(context.Background(), grantee) + require.NotNil(t, acc, "the grantee hotkey must still get its account created") + require.Equal(t, uint64(1), acc.GetSequence()) +} diff --git a/app/ante/ante_cosmos.go b/app/ante/ante_cosmos.go index 08be3f011..47b6acfda 100755 --- a/app/ante/ante_cosmos.go +++ b/app/ante/ante_cosmos.go @@ -25,8 +25,22 @@ func NewCosmosAnteHandler(ctx sdk.Context, options HandlerOptions) sdk.AnteHandl sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}), sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}), ), + // Vesting accounts can delegate locked coins, but the EVM state view only + // tracks spendable balance. Delegating more than the spendable balance makes + // the StateDB subtract more than it holds, which reconciles back to bank as a + // mint (or a burn for the victim). Block vesting-account creation outright so + // the precondition cannot be created permissionlessly. + NewBlockedMsgsDecorator( + sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}), + sdk.MsgTypeURL(&sdkvesting.MsgCreatePermanentLockedAccount{}), + sdk.MsgTypeURL(&sdkvesting.MsgCreatePeriodicVestingAccount{}), + ), ante.NewSetUpContextDecorator(), + // Gasless txs pay no fee, so the fee is not a bound on the gas they + // declare. Cap it explicitly, before NewGasWantedDecorator adds the + // declared gas to the block's cumulative gas wanted. + NewGaslessGasLimitDecorator(options.UexecutorKeeper), wasmkeeper.NewLimitSimulationGasDecorator(options.WasmConfig.SimulationGasLimit), // after setup context to enforce limits early wasmkeeper.NewCountTXDecorator(options.TXCounterStoreService), wasmkeeper.NewGasRegisterDecorator(options.WasmKeeper.GetGasRegister()), @@ -43,9 +57,12 @@ func NewCosmosAnteHandler(ctx sdk.Context, options HandlerOptions) sdk.AnteHandl // NewAccountInitDecorator must be called before all signature verification decorators and SetPubKeyDecorator // - this // 1. generates the account for the new accounts only for gasless transactions, - // 2. verifies the sig, and + // refusing to do so for the validator-only vote messages, whose signer + // must already be a bonded universal validator (F-2026-18186), + // 2. binds the declared signer to the signing key, enforces the signature + // count limit and verifies the sig, and // 3. bypasses the rest of the ante chain - NewAccountInitDecorator(options.AccountKeeper, options.SignModeHandler), + NewAccountInitDecorator(options.AccountKeeper, options.UValidatorKeeper, options.SignModeHandler), // SetPubKeyDecorator must be called before all signature verification decorators ante.NewSetPubKeyDecorator(options.AccountKeeper), ante.NewValidateSigCountDecorator(options.AccountKeeper), diff --git a/app/ante/blocked_msgs.go b/app/ante/blocked_msgs.go new file mode 100644 index 000000000..a1cae66b3 --- /dev/null +++ b/app/ante/blocked_msgs.go @@ -0,0 +1,91 @@ +package ante + +import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/authz" +) + +// maxNestedBlockedMsgs caps how deep the decorator recurses into nested +// authz.MsgExec messages while looking for blocked msg types. +const maxNestedBlockedMsgs = 7 + +// BlockedMsgsDecorator rejects a fixed set of msg type URLs anywhere in a tx: +// at the top level, and nested inside authz.MsgExec (arbitrarily deep, up to +// maxNestedBlockedMsgs). +// +// It complements cosmosante.NewAuthzLimiterDecorator, which only blocks msgs +// carried *inside* an authz message and lets the same msg through when it is +// submitted directly. +type BlockedMsgsDecorator struct { + // blockedMsgTypes is the set of msg type URLs to reject. + blockedMsgTypes map[string]struct{} +} + +// NewBlockedMsgsDecorator creates a decorator that rejects the given msg type +// URLs regardless of where they appear in the tx. +func NewBlockedMsgsDecorator(blockedMsgTypes ...string) BlockedMsgsDecorator { + blocked := make(map[string]struct{}, len(blockedMsgTypes)) + for _, msgType := range blockedMsgTypes { + blocked[msgType] = struct{}{} + } + + return BlockedMsgsDecorator{blockedMsgTypes: blocked} +} + +func (bmd BlockedMsgsDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + if err := bmd.checkBlockedMsgs(tx.GetMsgs(), 1); err != nil { + return ctx, errorsmod.Wrapf(errortypes.ErrUnauthorized, "%s", err.Error()) + } + + return next(ctx, tx, simulate) +} + +// checkBlockedMsgs walks the msgs and returns an error on the first blocked msg +// type it finds. authz.MsgExec is unwrapped so a blocked msg cannot be smuggled +// through the authz module; authz.MsgGrant is checked so a grant for a blocked +// msg type cannot be created either. +func (bmd BlockedMsgsDecorator) checkBlockedMsgs(msgs []sdk.Msg, nestedLvl int) error { + if nestedLvl >= maxNestedBlockedMsgs { + return fmt.Errorf("found more nested msgs than permitted; got: %d, expected: <%d", nestedLvl, maxNestedBlockedMsgs) + } + + for _, msg := range msgs { + switch msg := msg.(type) { + case *authz.MsgExec: + innerMsgs, err := msg.GetMessages() + if err != nil { + return err + } + if err := bmd.checkBlockedMsgs(innerMsgs, nestedLvl+1); err != nil { + return err + } + case *authz.MsgGrant: + authorization, err := msg.GetAuthorization() + if err != nil { + return err + } + if err := bmd.rejectIfBlocked(authorization.MsgTypeURL()); err != nil { + return err + } + default: + if err := bmd.rejectIfBlocked(sdk.MsgTypeURL(msg)); err != nil { + return err + } + } + } + + return nil +} + +func (bmd BlockedMsgsDecorator) rejectIfBlocked(msgTypeURL string) error { + if _, blocked := bmd.blockedMsgTypes[msgTypeURL]; blocked { + return fmt.Errorf("found blocked msg type: %s", msgTypeURL) + } + + return nil +} diff --git a/app/ante/blocked_msgs_test.go b/app/ante/blocked_msgs_test.go new file mode 100644 index 000000000..b0f3af2b3 --- /dev/null +++ b/app/ante/blocked_msgs_test.go @@ -0,0 +1,147 @@ +package ante_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app/ante" +) + +// blockedVestingMsgURLs mirrors the list wired into NewCosmosAnteHandler. +var blockedVestingMsgURLs = []string{ + sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}), + sdk.MsgTypeURL(&sdkvesting.MsgCreatePermanentLockedAccount{}), + sdk.MsgTypeURL(&sdkvesting.MsgCreatePeriodicVestingAccount{}), +} + +func vestingTestAddrs() (from, to sdk.AccAddress) { + return sdk.AccAddress([]byte("from________________")), sdk.AccAddress([]byte("to__________________")) +} + +// nestMsgExec wraps msgs in `depth` levels of authz.MsgExec. +func nestMsgExec(grantee sdk.AccAddress, depth int, msgs []sdk.Msg) sdk.Msg { + inner := msgs + var out sdk.Msg + for i := 0; i < depth; i++ { + exec := authz.NewMsgExec(grantee, inner) + out = &exec + inner = []sdk.Msg{out} + } + return out +} + +// TestBlockedMsgsDecorator_VestingMsgs asserts that all three vesting-account +// creation msgs are rejected at the TOP LEVEL of a tx (F-2026-18201). Before +// this decorator only MsgCreateVestingAccount was blocked, and only when nested +// inside an authz.MsgExec, so a plain top-level tx created the vesting account +// that the staking-precompile underflow attack needs. +func TestBlockedMsgsDecorator_VestingMsgs(t *testing.T) { + from, to := vestingTestAddrs() + amount := sdk.NewCoins(sdk.NewInt64Coin("upc", 1_000_000)) + future := time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC) + + createVesting := sdkvesting.NewMsgCreateVestingAccount(from, to, amount, future.Unix(), false) + createPermanentLocked := sdkvesting.NewMsgCreatePermanentLockedAccount(from, to, amount) + createPeriodicVesting := sdkvesting.NewMsgCreatePeriodicVestingAccount(from, to, 0, []sdkvesting.Period{ + {Length: 3600, Amount: amount}, + }) + send := banktypes.NewMsgSend(from, to, amount) + + decorator := ante.NewBlockedMsgsDecorator(blockedVestingMsgURLs...) + + testCases := []struct { + name string + msgs []sdk.Msg + expFail bool + }{ + {"allowed msg passes", []sdk.Msg{send}, false}, + {"top-level MsgCreateVestingAccount", []sdk.Msg{createVesting}, true}, + {"top-level MsgCreatePermanentLockedAccount", []sdk.Msg{createPermanentLocked}, true}, + {"top-level MsgCreatePeriodicVestingAccount", []sdk.Msg{createPeriodicVesting}, true}, + {"blocked msg alongside allowed msgs", []sdk.Msg{send, createPermanentLocked, send}, true}, + { + "blocked msg inside authz.MsgExec", + []sdk.Msg{nestMsgExec(from, 1, []sdk.Msg{createPermanentLocked})}, + true, + }, + { + "blocked msg inside deeply nested authz.MsgExec", + []sdk.Msg{nestMsgExec(from, 4, []sdk.Msg{createPeriodicVesting})}, + true, + }, + { + "allowed msg inside authz.MsgExec passes", + []sdk.Msg{nestMsgExec(from, 2, []sdk.Msg{send})}, + false, + }, + { + "nesting deeper than the cap is rejected", + []sdk.Msg{nestMsgExec(from, 8, []sdk.Msg{send})}, + true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tx := mockFeeTx{msgs: tc.msgs} + + called := false + next := func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { + called = true + return ctx, nil + } + + _, err := decorator.AnteHandle(sdk.Context{}, tx, false, next) + if tc.expFail { + require.Error(t, err) + require.ErrorIs(t, err, sdkerrors.ErrUnauthorized) + require.False(t, called, "blocked tx must not reach the next decorator") + return + } + + require.NoError(t, err) + require.True(t, called, "allowed tx must reach the next decorator") + }) + } +} + +// TestBlockedMsgsDecorator_AuthzGrant asserts that an authz grant for a blocked +// vesting msg type cannot be created either, so the block cannot be sidestepped +// by pre-authorizing a grantee. +func TestBlockedMsgsDecorator_AuthzGrant(t *testing.T) { + from, to := vestingTestAddrs() + future := time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC) + + decorator := ante.NewBlockedMsgsDecorator(blockedVestingMsgURLs...) + + for _, msgURL := range blockedVestingMsgURLs { + t.Run(msgURL, func(t *testing.T) { + grant, err := authz.NewMsgGrant(from, to, authz.NewGenericAuthorization(msgURL), &future) + require.NoError(t, err) + + _, err = decorator.AnteHandle(sdk.Context{}, mockFeeTx{msgs: []sdk.Msg{grant}}, false, noopAnteNext) + require.Error(t, err) + require.ErrorIs(t, err, sdkerrors.ErrUnauthorized) + }) + } + + t.Run("grant for an allowed msg type passes", func(t *testing.T) { + grant, err := authz.NewMsgGrant(from, to, + authz.NewGenericAuthorization(sdk.MsgTypeURL(&banktypes.MsgSend{})), &future) + require.NoError(t, err) + + _, err = decorator.AnteHandle(sdk.Context{}, mockFeeTx{msgs: []sdk.Msg{grant}}, false, noopAnteNext) + require.NoError(t, err) + }) +} + +func noopAnteNext(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { + return ctx, nil +} diff --git a/app/ante/gasless_gas_limit.go b/app/ante/gasless_gas_limit.go new file mode 100644 index 000000000..a750b4b47 --- /dev/null +++ b/app/ante/gasless_gas_limit.go @@ -0,0 +1,79 @@ +package ante + +import ( + "context" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + txpolicy "github.com/pushchain/push-chain-node/app/txpolicy" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// GaslessParamsKeeper reads the module parameters that bound fee-exempt txs. +type GaslessParamsKeeper interface { + GetParams(ctx context.Context) (uexecutortypes.Params, error) +} + +// GaslessGasLimitDecorator caps the gas limit a fee-exempt (gasless) tx may +// declare. +// +// A fee-paying tx is bounded by its own fee: the ante handler requires +// ceil(minGasPrice * gasLimit), so an absurd gas limit costs an absurd amount +// of tokens. A gasless tx pays nothing, so nothing bounds the gas it declares +// while that declared gas is still added to the block's cumulative gas wanted. +// Enough of them, or few enough with a large enough declaration, push the +// cumulative total past what the fee market can represent. +// +// CONTRACT: must run before the EVM GasWantedDecorator, which is what +// accumulates the declared gas into the fee market transient store. +type GaslessGasLimitDecorator struct { + paramsKeeper GaslessParamsKeeper +} + +func NewGaslessGasLimitDecorator(pk GaslessParamsKeeper) GaslessGasLimitDecorator { + return GaslessGasLimitDecorator{paramsKeeper: pk} +} + +func (ggd GaslessGasLimitDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + if !txpolicy.IsGaslessTx(tx) { + return next(ctx, tx, simulate) + } + + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return ctx, errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") + } + + maxGas := ggd.maxGaslessTxGas(ctx) + if gas := feeTx.GetGas(); gas > maxGas { + ctx.Logger().Debug("gasless gas limit decorator: declared gas over cap", + "gas", gas, + "max_gas", maxGas, + ) + return ctx, errorsmod.Wrapf(sdkerrors.ErrInvalidGasLimit, + "gasless tx gas limit %d exceeds the maximum allowed %d", gas, maxGas) + } + + return next(ctx, tx, simulate) +} + +// maxGaslessTxGas resolves the governance-controlled cap, falling back to the +// default when it cannot be read or was never set. The fallback is deliberate: +// a missing parameter must not mean "no cap". +func (ggd GaslessGasLimitDecorator) maxGaslessTxGas(ctx sdk.Context) uint64 { + params, err := ggd.paramsKeeper.GetParams(ctx) + if err != nil { + ctx.Logger().Error("gasless gas limit decorator: failed to read uexecutor params, using default cap", + "error", err, + ) + return uexecutortypes.DefaultMaxGaslessTxGas + } + + if params.MaxGaslessTxGas == 0 { + return uexecutortypes.DefaultMaxGaslessTxGas + } + + return params.MaxGaslessTxGas +} diff --git a/app/ante/gasless_gas_limit_test.go b/app/ante/gasless_gas_limit_test.go new file mode 100644 index 000000000..a2f917a84 --- /dev/null +++ b/app/ante/gasless_gas_limit_test.go @@ -0,0 +1,235 @@ +package ante_test + +import ( + "context" + "errors" + "math" + "testing" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app/ante" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// --------------------------------------------------------------------------- +// GaslessGasLimitDecorator — F-2026-18144 +// +// Gasless txs pay no fee, so the fee is not a bound on the gas they declare, +// and the declared gas is what accumulates into the block's cumulative gas +// wanted. These tests pin the cap that replaces the missing economic bound. +// --------------------------------------------------------------------------- + +// mockGaslessParamsKeeper satisfies ante.GaslessParamsKeeper. +type mockGaslessParamsKeeper struct { + params uexecutortypes.Params + err error + calls int +} + +func (m *mockGaslessParamsKeeper) GetParams(_ context.Context) (uexecutortypes.Params, error) { + m.calls++ + if m.err != nil { + return uexecutortypes.Params{}, m.err + } + return m.params, nil +} + +func paramsWithCap(cap uint64) *mockGaslessParamsKeeper { + return &mockGaslessParamsKeeper{params: uexecutortypes.Params{SomeValue: true, MaxGaslessTxGas: cap}} +} + +// gaslessTx returns a tx whose only msg is on the IsGaslessTx allowlist. +func gaslessTx(gas uint64) mockFeeTx { + return mockFeeTx{ + msgs: []sdk.Msg{&uexecutortypes.MsgVoteInbound{}}, + gas: gas, + fee: sdk.NewCoins(), + feePayer: sdk.AccAddress([]byte("payer")), + } +} + +// runDecorator returns (nextCalled, err). +func runDecorator(t *testing.T, pk ante.GaslessParamsKeeper, tx sdk.Tx, simulate bool) (bool, error) { + t.Helper() + ggd := ante.NewGaslessGasLimitDecorator(pk) + ctx := newAnteTestCtx(t, false) + nextCalled := false + _, err := ggd.AnteHandle(ctx, tx, simulate, func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + nextCalled = true + return ctx, nil + }) + return nextCalled, err +} + +// TestGaslessGasLimit_AboveDefaultCapRejected is the core regression: a gasless +// tx declaring more than the cap must not reach the rest of the ante chain, so +// its declared gas is never added to the block's cumulative gas wanted. +func TestGaslessGasLimit_AboveDefaultCapRejected(t *testing.T) { + pk := paramsWithCap(uexecutortypes.DefaultMaxGaslessTxGas) + + nextCalled, err := runDecorator(t, pk, gaslessTx(uexecutortypes.DefaultMaxGaslessTxGas+1), false) + + require.False(t, nextCalled, "over-cap gasless tx must not reach the next decorator") + require.Error(t, err) + require.True(t, sdkerrors.ErrInvalidGasLimit.Is(err), "expected ErrInvalidGasLimit, got: %v", err) + require.Contains(t, err.Error(), "100000001") + require.Contains(t, err.Error(), "100000000") +} + +// TestGaslessGasLimit_AtCapAccepted pins the boundary: exactly the cap passes. +func TestGaslessGasLimit_AtCapAccepted(t *testing.T) { + pk := paramsWithCap(uexecutortypes.DefaultMaxGaslessTxGas) + + nextCalled, err := runDecorator(t, pk, gaslessTx(uexecutortypes.DefaultMaxGaslessTxGas), false) + + require.True(t, nextCalled, "gasless tx at exactly the cap must be accepted") + require.NoError(t, err) +} + +// TestGaslessGasLimit_BelowCapAccepted covers the ordinary case. +func TestGaslessGasLimit_BelowCapAccepted(t *testing.T) { + pk := paramsWithCap(uexecutortypes.DefaultMaxGaslessTxGas) + + nextCalled, err := runDecorator(t, pk, gaslessTx(200_000), false) + + require.True(t, nextCalled) + require.NoError(t, err) +} + +// TestGaslessGasLimit_MaxInt64Rejected is the shape from the finding: two txs +// each declaring MaxInt64 sum past what the fee market EndBlock can convert. +func TestGaslessGasLimit_MaxInt64Rejected(t *testing.T) { + pk := paramsWithCap(uexecutortypes.DefaultMaxGaslessTxGas) + + nextCalled, err := runDecorator(t, pk, gaslessTx(math.MaxInt64), false) + + require.False(t, nextCalled, "MaxInt64 gasless tx must not reach the next decorator") + require.Error(t, err) + require.True(t, sdkerrors.ErrInvalidGasLimit.Is(err), "expected ErrInvalidGasLimit, got: %v", err) +} + +// TestGaslessGasLimit_NonGaslessTxNotCapped proves the cap is scoped to +// fee-exempt txs: a fee-paying tx is bounded by its own fee, not by this cap, +// and the params are not even read for it. +func TestGaslessGasLimit_NonGaslessTxNotCapped(t *testing.T) { + pk := paramsWithCap(uexecutortypes.DefaultMaxGaslessTxGas) + + tx := mockFeeTx{ + msgs: []sdk.Msg{&banktypes.MsgSend{}}, + gas: math.MaxInt64, + fee: sdk.NewCoins(sdk.NewInt64Coin("upc", 1)), + feePayer: sdk.AccAddress([]byte("payer")), + } + + nextCalled, err := runDecorator(t, pk, tx, false) + + require.True(t, nextCalled, "fee-paying tx must not be capped here") + require.Equal(t, 0, pk.calls, "params must not be read for a non-gasless tx") + require.NoError(t, err) +} + +// TestGaslessGasLimit_AuthzExecVoteShape uses the exact wire shape the universal +// validators send: an authz.MsgExec wrapping a vote. This is what declared the +// hardcoded 500,000,000 on donut. +func TestGaslessGasLimit_AuthzExecVoteShape(t *testing.T) { + inner, err := codectypes.NewAnyWithValue(&uexecutortypes.MsgVoteInbound{}) + require.NoError(t, err) + + execTx := func(gas uint64) mockFeeTx { + return mockFeeTx{ + msgs: []sdk.Msg{&authz.MsgExec{Grantee: "push1grantee", Msgs: []*codectypes.Any{inner}}}, + gas: gas, + fee: sdk.NewCoins(), + feePayer: sdk.AccAddress([]byte("payer")), + } + } + + t.Run("500M vote is rejected", func(t *testing.T) { + pk := paramsWithCap(uexecutortypes.DefaultMaxGaslessTxGas) + + nextCalled, err := runDecorator(t, pk, execTx(500_000_000), false) + + require.False(t, nextCalled, "500M MsgExec vote must not reach the next decorator") + require.Error(t, err) + require.True(t, sdkerrors.ErrInvalidGasLimit.Is(err), "expected ErrInvalidGasLimit, got: %v", err) + }) + + t.Run("100M vote is accepted", func(t *testing.T) { + pk := paramsWithCap(uexecutortypes.DefaultMaxGaslessTxGas) + + nextCalled, err := runDecorator(t, pk, execTx(100_000_000), false) + + require.True(t, nextCalled, "100M MsgExec vote must be accepted") + require.NoError(t, err) + }) +} + +// TestGaslessGasLimit_GovernanceParamTakesEffect proves the cap is the +// governance parameter and not a compiled-in constant: it must bind both +// tighter and looser than the default. +func TestGaslessGasLimit_GovernanceParamTakesEffect(t *testing.T) { + t.Run("lowered cap binds below the default", func(t *testing.T) { + pk := paramsWithCap(30_000_000) + + acceptedNext, acceptedErr := runDecorator(t, pk, gaslessTx(30_000_000), false) + require.True(t, acceptedNext, "tx at the lowered cap must be accepted") + require.NoError(t, acceptedErr) + + rejectedNext, rejectedErr := runDecorator(t, pk, gaslessTx(30_000_001), false) + require.False(t, rejectedNext, "tx above the lowered cap must be rejected") + require.Error(t, rejectedErr) + require.Contains(t, rejectedErr.Error(), "30000000") + }) + + t.Run("raised cap admits gas the default would reject", func(t *testing.T) { + pk := paramsWithCap(500_000_000) + + nextCalled, err := runDecorator(t, pk, gaslessTx(400_000_000), false) + + require.True(t, nextCalled, "raised cap must admit 400M") + require.NoError(t, err) + }) +} + +// TestGaslessGasLimit_UnsetParamFallsBackToDefault: a missing parameter must +// never mean "no cap". +func TestGaslessGasLimit_UnsetParamFallsBackToDefault(t *testing.T) { + t.Run("zero param", func(t *testing.T) { + pk := paramsWithCap(0) + + acceptedNext, acceptedErr := runDecorator(t, pk, gaslessTx(uexecutortypes.DefaultMaxGaslessTxGas), false) + require.True(t, acceptedNext) + require.NoError(t, acceptedErr) + + rejectedNext, rejectedErr := runDecorator(t, pk, gaslessTx(uexecutortypes.DefaultMaxGaslessTxGas+1), false) + require.False(t, rejectedNext, "zero param must fall back to the default cap, not disable it") + require.Error(t, rejectedErr) + }) + + t.Run("params read failure", func(t *testing.T) { + pk := &mockGaslessParamsKeeper{err: errors.New("collections: not found")} + + nextCalled, err := runDecorator(t, pk, gaslessTx(uexecutortypes.DefaultMaxGaslessTxGas+1), false) + + require.False(t, nextCalled, "unreadable params must fall back to the default cap, not disable it") + require.Error(t, err) + require.True(t, sdkerrors.ErrInvalidGasLimit.Is(err), "expected ErrInvalidGasLimit, got: %v", err) + }) +} + +// TestGaslessGasLimit_SimulationIsAlsoCapped keeps simulation honest: a gas +// estimate that would be rejected on delivery must not come back clean. +func TestGaslessGasLimit_SimulationIsAlsoCapped(t *testing.T) { + pk := paramsWithCap(uexecutortypes.DefaultMaxGaslessTxGas) + + nextCalled, err := runDecorator(t, pk, gaslessTx(uexecutortypes.DefaultMaxGaslessTxGas+1), true) + + require.False(t, nextCalled, "simulation must apply the same cap") + require.Error(t, err) +} diff --git a/app/ante/handler_options.go b/app/ante/handler_options.go index 1c820e0a6..58859eb14 100755 --- a/app/ante/handler_options.go +++ b/app/ante/handler_options.go @@ -45,12 +45,20 @@ type AccountKeeper interface { TryAddUnorderedNonce(ctx sdk.Context, sender []byte, timestamp time.Time) error } +// UValidatorKeeper is the minimal slice of the uvalidator keeper the ante chain +// needs. Declared locally, like AccountKeeper/BankKeeper above, so the ante +// package does not depend on a concrete keeper. +type UValidatorKeeper interface { + IsBondedUniversalValidator(ctx context.Context, universalValidator string) (bool, error) +} + // HandlerOptions defines the list of module keepers required to run the EVM // AnteHandler decorators. type HandlerOptions struct { Cdc codec.BinaryCodec AccountKeeper AccountKeeper BankKeeper BankKeeper + UValidatorKeeper UValidatorKeeper FeegrantKeeper ante.FeegrantKeeper ExtensionOptionChecker ante.ExtensionOptionChecker SignModeHandler *txsigning.HandlerMap @@ -63,6 +71,10 @@ type HandlerOptions struct { FeeMarketKeeper anteinterfaces.FeeMarketKeeper EvmKeeper anteinterfaces.EVMKeeper + // UexecutorKeeper supplies the governance-controlled cap on the gas a + // fee-exempt (gasless) tx may declare. + UexecutorKeeper GaslessParamsKeeper + IBCKeeper *ibckeeper.Keeper CircuitKeeper *circuitkeeper.Keeper @@ -79,6 +91,9 @@ func (options HandlerOptions) Validate() error { if options.BankKeeper == nil { return errorsmod.Wrap(errortypes.ErrLogic, "bank keeper is required for AnteHandler") } + if options.UValidatorKeeper == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "uvalidator keeper is required for AnteHandler") + } if options.SigGasConsumer == nil { return errorsmod.Wrap(errortypes.ErrLogic, "signature gas consumer is required for AnteHandler") } @@ -105,6 +120,9 @@ func (options HandlerOptions) Validate() error { if options.EvmKeeper == nil { return errorsmod.Wrap(errortypes.ErrLogic, "evm keeper is required for AnteHandler") } + if options.UexecutorKeeper == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "uexecutor keeper is required for AnteHandler") + } return nil } diff --git a/app/app.go b/app/app.go index 838a8838a..c70f7a614 100755 --- a/app/app.go +++ b/app/app.go @@ -91,9 +91,6 @@ import ( govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" - "github.com/cosmos/cosmos-sdk/x/group" - groupkeeper "github.com/cosmos/cosmos-sdk/x/group/keeper" - groupmodule "github.com/cosmos/cosmos-sdk/x/group/module" "github.com/cosmos/cosmos-sdk/x/mint" mintkeeper "github.com/cosmos/cosmos-sdk/x/mint/keeper" minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" @@ -194,7 +191,6 @@ var ( capabilities = []string{ "iterator", "staking", - "stargate", "cosmwasm_1_1", "cosmwasm_1_2", "cosmwasm_1_3", "cosmwasm_1_4", "token_factory", } @@ -312,7 +308,6 @@ type ChainApp struct { AuthzKeeper authzkeeper.Keeper EvidenceKeeper evidencekeeper.Keeper FeeGrantKeeper feegrantkeeper.Keeper - GroupKeeper groupkeeper.Keeper NFTKeeper nftkeeper.Keeper ConsensusParamsKeeper consensusparamkeeper.Keeper CircuitKeeper circuitkeeper.Keeper @@ -434,7 +429,6 @@ func NewChainApp( circuittypes.StoreKey, authzkeeper.StoreKey, nftkeeper.StoreKey, - group.StoreKey, // non sdk store keys ibcexported.StoreKey, ibctransfertypes.StoreKey, @@ -512,8 +506,31 @@ func NewChainApp( logger, ) - // enable sign mode textual by overwriting the default tx config (after setting the bank keeper) - enabledSignModes := append(tx.DefaultSignModes, signingtype.SignMode_SIGN_MODE_TEXTUAL) + // Enabled sign modes, listed explicitly rather than appending to + // tx.DefaultSignModes so that what the chain accepts is stated here rather + // than inherited. + // + // SIGN_MODE_DIRECT_AUX is deliberately excluded (F-2026-18784). The handler + // in cosmossdk.io/x/tx rejects a fee payer who also signs with DIRECT_AUX + // using a raw string compare: + // + // if feePayer == signerData.Address { ... unauthorized ... } + // + // BIP-173 permits an all-uppercase bech32 encoding of the same account, so + // an uppercase Fee.Payer aliasing the victim's lowercase signer address + // fails that check open, while everything downstream decodes both to the + // same AccAddress and deduplicates signers. A sponsor holding a victim's + // DIRECT_AUX signature over a fixed TxBody could then rewrite AuthInfo to + // charge the victim. Still present in our pinned x/tx v0.14.0. + // + // Nothing on Push signs with DIRECT_AUX — the universal client pins + // SIGN_MODE_DIRECT — so enabling it only exposes surface. Restore it once + // x/tx compares decoded bytes (or folds case), not before. + enabledSignModes := []signingtype.SignMode{ + signingtype.SignMode_SIGN_MODE_DIRECT, + signingtype.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, + signingtype.SignMode_SIGN_MODE_TEXTUAL, + } txConfigOpts := tx.ConfigOptions{ EnabledSignModes: enabledSignModes, TextualCoinMetadataQueryFn: txmodule.NewBankKeeperCoinMetadataQueryFn(app.BankKeeper), @@ -591,17 +608,6 @@ func NewChainApp( app.AccountKeeper, ) - groupConfig := group.DefaultConfig() - groupConfig.MaxMetadataLen = 10000 - app.GroupKeeper = groupkeeper.NewKeeper( - keys[group.StoreKey], - // runtime.NewKVStoreService(keys[group.StoreKey]), - appCodec, - app.MsgServiceRouter(), - app.AccountKeeper, - groupConfig, - ) - // get skipUpgradeHeights from the app options skipUpgradeHeights := map[int64]bool{} for _, h := range cast.ToIntSlice(appOpts.Get(server.FlagUnsafeSkipUpgrades)) { @@ -1076,7 +1082,6 @@ func NewChainApp( evidence.NewAppModule(app.EvidenceKeeper), params.NewAppModule(app.ParamsKeeper), authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), - groupmodule.NewAppModule(appCodec, app.GroupKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), nftmodule.NewAppModule(appCodec, app.NFTKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), consensus.NewAppModule(appCodec, app.ConsensusParamsKeeper), circuit.NewAppModule(appCodec, app.CircuitKeeper), @@ -1163,7 +1168,6 @@ func NewChainApp( stakingtypes.ModuleName, genutiltypes.ModuleName, feegrant.ModuleName, - group.ModuleName, // additional non simd modules evmtypes.ModuleName, erc20types.ModuleName, feemarkettypes.ModuleName, ibctransfertypes.ModuleName, @@ -1210,7 +1214,6 @@ func NewChainApp( authz.ModuleName, feegrant.ModuleName, nft.ModuleName, - group.ModuleName, paramstypes.ModuleName, upgradetypes.ModuleName, vestingtypes.ModuleName, @@ -1284,6 +1287,7 @@ func NewChainApp( Cdc: app.appCodec, AccountKeeper: app.AccountKeeper, BankKeeper: app.BankKeeper, + UValidatorKeeper: app.UvalidatorKeeper, FeegrantKeeper: app.FeeGrantKeeper, FeeMarketKeeper: app.FeeMarketKeeper, SignModeHandler: txConfig.SignModeHandler(), @@ -1294,6 +1298,7 @@ func NewChainApp( CircuitKeeper: &app.CircuitKeeper, EvmKeeper: app.EVMKeeper, + UexecutorKeeper: app.UexecutorKeeper, ExtensionOptionChecker: antetypes.HasDynamicFeeExtensionOption, SigGasConsumer: cosmosevmante.SigVerificationGasConsumer, MaxTxGasWanted: cast.ToUint64(appOpts.Get(srvflags.EVMMaxTxGasWanted)), diff --git a/app/nested_dispatch_test.go b/app/nested_dispatch_test.go new file mode 100644 index 000000000..0ac59d9a1 --- /dev/null +++ b/app/nested_dispatch_test.go @@ -0,0 +1,75 @@ +package app + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// Regression tests for F-2026-18197 (nested message dispatch bypasses the EVM ante). +// +// Ethereum signature, nonce and gas checks live only in the EVM ante handler; +// x/vm's Keeper.EthereumTx assumes the ante already ran. Any module that unpacks +// and re-dispatches an embedded sdk.Msg therefore reaches the EVM executor with +// none of those checks applied. Push had two such dispatchers wired: x/group +// (MsgSubmitProposal/MsgExec) and the CosmWasm "stargate" capability +// (CosmosMsg::Any). Both are removed; these tests keep them removed. + +// groupMsgTypeURLs are the x/group entry points that unpack and dispatch a +// nested sdk.Msg. They must not resolve or route. +var groupMsgTypeURLs = []string{ + "/cosmos.group.v1.MsgSubmitProposal", + "/cosmos.group.v1.MsgExec", + "/cosmos.group.v1.MsgCreateGroup", + "/cosmos.group.v1.MsgCreateGroupWithPolicy", + "/cosmos.group.v1.MsgCreateGroupPolicy", +} + +// TestGroupModuleNotWired asserts x/group is gone from every wiring point: the +// module manager, the store keys, the message router and the interface registry. +func TestGroupModuleNotWired(t *testing.T) { + // setup() constructs the app without InitChain, which is all these + // assertions need: the module manager, store keys, message routes and + // interface registry are populated by then. Setup() is avoided on purpose - + // it passes the "testing" chain ID and only works once another test has + // already initialised the global EVM configurator. + gapp, _ := setup(t, ChainID, false, 0) + + t.Run("not in module manager", func(t *testing.T) { + _, ok := gapp.ModuleManager.Modules["group"] + require.False(t, ok, "x/group must not be registered in the module manager") + }) + + t.Run("no store key", func(t *testing.T) { + require.Nil(t, gapp.GetKey("group"), "x/group must not have a KV store key") + }) + + t.Run("msgs unroutable", func(t *testing.T) { + for _, typeURL := range groupMsgTypeURLs { + require.Nil(t, gapp.MsgServiceRouter().HandlerByTypeURL(typeURL), + "%s must have no handler on the msg service router", typeURL) + } + }) + + t.Run("msgs unresolvable", func(t *testing.T) { + for _, typeURL := range groupMsgTypeURLs { + _, err := gapp.InterfaceRegistry().Resolve(typeURL) + require.Error(t, err, + "%s must not resolve in the interface registry (tx decoding must fail)", typeURL) + } + }) +} + +// TestWasmStargateCapabilityDisabled asserts the "stargate" wasmvm capability is +// off for both wasm VMs. With it enabled, an uploaded contract may emit an +// arbitrary encoded sdk.Msg (CosmosMsg::Any / Stargate) that the wasm message +// handler forwards straight to the message router, after ante has already run. +func TestWasmStargateCapabilityDisabled(t *testing.T) { + t.Run("x/wasm", func(t *testing.T) { + require.NotContains(t, AllCapabilities(), "stargate") + }) + + t.Run("08-wasm light client", func(t *testing.T) { + require.NotContains(t, capabilities, "stargate") + }) +} diff --git a/app/sign_modes_test.go b/app/sign_modes_test.go new file mode 100644 index 000000000..573dca469 --- /dev/null +++ b/app/sign_modes_test.go @@ -0,0 +1,75 @@ +package app + +import ( + "testing" + + "github.com/stretchr/testify/require" + + signingtype "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + "github.com/cosmos/cosmos-sdk/x/auth/tx" +) + +// Regression test for F-2026-18784 (uppercase bech32 fee payer bypasses the +// DIRECT_AUX fee-payer guard). +// +// The handler in cosmossdk.io/x/tx compares the fee payer against the signer +// with a raw string compare: +// +// if feePayer == signerData.Address { ... unauthorized ... } +// +// BIP-173 allows an all-uppercase bech32 encoding of the same account, so an +// uppercase Fee.Payer aliasing the victim's lowercase signer address slips past +// that check while everything downstream decodes both to the same AccAddress. +// Our pinned x/tx v0.14.0 still has the raw compare, so the mode stays off. +// +// This test exists so that a future refactor cannot quietly re-enable DIRECT_AUX +// by going back to appending to tx.DefaultSignModes, which contains it. +func TestEnabledSignModes_ExcludesDirectAux(t *testing.T) { + // setup() constructs the app without InitChain, which is all this needs. + // Setup() is avoided on purpose: it passes the "testing" chain ID and panics + // in the EVM configurator unless another test has already initialised it. + gapp, _ := setup(t, ChainID, false, 0) + modes := gapp.TxConfig().SignModeHandler().SupportedModes() + + for _, m := range modes { + require.NotEqual(t, signingtype.SignMode_SIGN_MODE_DIRECT_AUX, m, + "SIGN_MODE_DIRECT_AUX must stay disabled until x/tx compares decoded "+ + "bytes rather than raw strings (F-2026-18784)") + } +} + +// TestEnabledSignModes_KeepsTheModesWeActuallyUse guards the other direction: +// dropping DIRECT_AUX must not take anything else with it. The universal client +// signs with SIGN_MODE_DIRECT, and TEXTUAL is enabled deliberately (it is not in +// tx.DefaultSignModes and needs the bank keeper). +func TestEnabledSignModes_KeepsTheModesWeActuallyUse(t *testing.T) { + gapp, _ := setup(t, ChainID, false, 0) + modes := gapp.TxConfig().SignModeHandler().SupportedModes() + + has := func(want signingtype.SignMode) bool { + for _, m := range modes { + if m == want { + return true + } + } + return false + } + + require.True(t, has(signingtype.SignMode_SIGN_MODE_DIRECT), "DIRECT is what the universal client signs with") + require.True(t, has(signingtype.SignMode_SIGN_MODE_LEGACY_AMINO_JSON), "AMINO_JSON is needed for ledger/legacy clients") + require.True(t, has(signingtype.SignMode_SIGN_MODE_TEXTUAL), "TEXTUAL is enabled deliberately") +} + +// TestDefaultSignModesStillContainsDirectAux documents why the explicit list +// exists. If upstream ever drops DIRECT_AUX from DefaultSignModes this test +// fails, and the explicit enumeration can be reconsidered. +func TestDefaultSignModesStillContainsDirectAux(t *testing.T) { + found := false + for _, m := range tx.DefaultSignModes { + if m.String() == "SIGN_MODE_DIRECT_AUX" { + found = true + } + } + require.True(t, found, + "tx.DefaultSignModes no longer contains DIRECT_AUX; the explicit list in app.go may no longer be needed") +} diff --git a/app/txpolicy/gasless.go b/app/txpolicy/gasless.go index bc3d7e7fc..c39c0eb36 100644 --- a/app/txpolicy/gasless.go +++ b/app/txpolicy/gasless.go @@ -16,7 +16,6 @@ func IsGaslessTx(tx sdk.Tx) bool { var ( // GaslessMsgTypes defines the message types that are allowed in gasless transactions GaslessMsgTypes = []string{ - sdk.MsgTypeURL(&uexecutortypes.MsgMigrateUEA{}), sdk.MsgTypeURL(&uexecutortypes.MsgExecutePayload{}), sdk.MsgTypeURL(&uexecutortypes.MsgVoteInbound{}), sdk.MsgTypeURL(&uexecutortypes.MsgVoteOutbound{}), @@ -35,6 +34,11 @@ func IsGaslessTx(tx sdk.Tx) bool { for _, msg := range msgs { switch m := msg.(type) { case *authz.MsgExec: + // An empty nest would pass the loop below vacuously and make the whole + // tx gasless, bypassing the fee and min-gas-price decorators (F-2026-18816). + if len(m.Msgs) == 0 { + return false + } // Only gasless if ALL inner messages are allowed for _, innerMsg := range m.Msgs { if !slices.Contains(GaslessMsgTypes, innerMsg.TypeUrl) { diff --git a/app/txpolicy/gasless_test.go b/app/txpolicy/gasless_test.go new file mode 100644 index 000000000..9531ce0a5 --- /dev/null +++ b/app/txpolicy/gasless_test.go @@ -0,0 +1,138 @@ +package txpolicy_test + +import ( + "testing" + + protov2 "google.golang.org/protobuf/proto" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" + evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app/txpolicy" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// msgsOnlyTx is the minimal sdk.Tx IsGaslessTx needs. +type msgsOnlyTx struct{ msgs []sdk.Msg } + +func (t msgsOnlyTx) GetMsgs() []sdk.Msg { return t.msgs } +func (t msgsOnlyTx) GetMsgsV2() ([]protov2.Message, error) { return nil, nil } + +// TestGaslessMsgTypesExcludeEthereumTx is one half of the F-2026-18197 invariant +// guard (see test/integration/uexecutor/gasless_module_sender_test.go for the +// other half). +// +// x/vm's Keeper.EthereumTx now rejects any MsgEthereumTx whose From is not the +// ECDSA signer of the raw transaction. A module account is derived from a name +// and has no key pair, so a module-signed MsgEthereumTx could never pass that +// check. Push's gasless flows are safe precisely because none of them is a +// MsgEthereumTx - they reach the EVM through CallEVM / DerivedEVMCall, which +// call ApplyMessageWithConfig directly. If a MsgEthereumTx were ever added to +// the gasless set, that flow would break 100% of the time; this test fails first. +func TestGaslessMsgTypesExcludeEthereumTx(t *testing.T) { + t.Run("MsgEthereumTx is not gasless", func(t *testing.T) { + tx := msgsOnlyTx{msgs: []sdk.Msg{&evmtypes.MsgEthereumTx{}}} + require.False(t, txpolicy.IsGaslessTx(tx), + "MsgEthereumTx must never be a gasless message type") + }) + + t.Run("MsgEthereumTx nested in authz is not gasless", func(t *testing.T) { + inner, err := codectypes.NewAnyWithValue(&evmtypes.MsgEthereumTx{}) + require.NoError(t, err) + tx := msgsOnlyTx{msgs: []sdk.Msg{&authz.MsgExec{Msgs: []*codectypes.Any{inner}}}} + require.False(t, txpolicy.IsGaslessTx(tx), + "MsgEthereumTx nested in authz.MsgExec must never be a gasless message type") + }) + + t.Run("MsgExecutePayload stays gasless", func(t *testing.T) { + tx := msgsOnlyTx{msgs: []sdk.Msg{&uexecutortypes.MsgExecutePayload{}}} + require.True(t, txpolicy.IsGaslessTx(tx)) + }) +} + +// TestIsGaslessTxAuthzExecNesting guards the authz.MsgExec branch of IsGaslessTx. +// +// The inner-message loop is an "all must be allowlisted" check, so an empty nest +// satisfies it vacuously and would make the whole tx gasless - skipping +// DeductFeeDecorator and MinGasPriceDecorator for a zero-fee tx, and handing the +// signer a free on-chain account via AccountInitDecorator, which gates on this +// same predicate. Nothing upstream catches it: authz.MsgExec has no ValidateBasic +// in SDK v0.53.7, and the empty check lives only in the msg server, which runs +// after the fee decorators (F-2026-18816). +func TestIsGaslessTxAuthzExecNesting(t *testing.T) { + anyOf := func(t *testing.T, msg sdk.Msg) *codectypes.Any { + t.Helper() + a, err := codectypes.NewAnyWithValue(msg) + require.NoError(t, err) + return a + } + + tests := []struct { + name string + inner []sdk.Msg + gasless bool + reason string + }{ + { + name: "empty nest is not gasless", + inner: nil, + gasless: false, + reason: "an empty authz.MsgExec must not pass the inner allowlist loop vacuously", + }, + { + name: "empty non-nil nest is not gasless", + inner: []sdk.Msg{}, + gasless: false, + reason: "a zero-length (but non-nil) inner message list must be rejected too", + }, + { + name: "all-allowlisted nest stays gasless", + inner: []sdk.Msg{&uexecutortypes.MsgVoteInbound{}, &uexecutortypes.MsgVoteOutbound{}}, + gasless: true, + reason: "a nest of only allowlisted messages must remain gasless", + }, + { + name: "mixed nest is not gasless", + inner: []sdk.Msg{&uexecutortypes.MsgVoteInbound{}, &evmtypes.MsgEthereumTx{}}, + gasless: false, + reason: "one non-allowlisted inner message must disqualify the whole tx", + }, + { + name: "nested MsgExec is not gasless", + inner: []sdk.Msg{&authz.MsgExec{Msgs: []*codectypes.Any{}}}, + gasless: false, + reason: "authz.MsgExec is not itself an allowlisted type, so nesting one must not recurse into a vacuous pass", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Preserve the nil vs. zero-length distinction: len() treats them the + // same, but constructing both proves the guard does not depend on it. + var inner []*codectypes.Any + if tc.inner != nil { + inner = make([]*codectypes.Any, 0, len(tc.inner)) + for _, m := range tc.inner { + inner = append(inner, anyOf(t, m)) + } + } + + tx := msgsOnlyTx{msgs: []sdk.Msg{&authz.MsgExec{Msgs: inner}}} + require.Equal(t, tc.gasless, txpolicy.IsGaslessTx(tx), tc.reason) + }) + } +} + +// TestIsGaslessTxEmptyExecAlongsideAllowedMsg pins the multi-message case: the +// outer loop must not let an allowlisted sibling carry an empty nest through. +func TestIsGaslessTxEmptyExecAlongsideAllowedMsg(t *testing.T) { + tx := msgsOnlyTx{msgs: []sdk.Msg{ + &uexecutortypes.MsgVoteInbound{}, + &authz.MsgExec{}, + }} + require.False(t, txpolicy.IsGaslessTx(tx), + "an empty authz.MsgExec must disqualify the tx even next to an allowlisted message") +} diff --git a/app/upgrades.go b/app/upgrades.go index 3c489f141..4bc60636f 100755 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -8,6 +8,7 @@ import ( "github.com/pushchain/push-chain-node/app/upgrades" aiauditfixes "github.com/pushchain/push-chain-node/app/upgrades/ai-audit-fixes" aiauditfixes2 "github.com/pushchain/push-chain-node/app/upgrades/ai-audit-fixes-2" + auditfixes "github.com/pushchain/push-chain-node/app/upgrades/audit-fixes" ceagasandpayload "github.com/pushchain/push-chain-node/app/upgrades/cea-gas-and-payload" ceapayloadverificationfix "github.com/pushchain/push-chain-node/app/upgrades/cea-payload-verification-fix" chainmeta "github.com/pushchain/push-chain-node/app/upgrades/chain-meta" @@ -102,6 +103,7 @@ var Upgrades = []upgrades.Upgrade{ // address still unclaimed in the A/B/C ranges (41 of 47 on donut, incl. 0xC2) readstate.NewUpgrade(), evmv063.NewUpgrade(), + auditfixes.NewUpgrade(), } // RegisterUpgradeHandlers registers the chain upgrade handlers diff --git a/app/upgrades/audit-fixes/upgrade.go b/app/upgrades/audit-fixes/upgrade.go new file mode 100644 index 000000000..9238909ef --- /dev/null +++ b/app/upgrades/audit-fixes/upgrade.go @@ -0,0 +1,83 @@ +package auditfixes + +import ( + "context" + "fmt" + + storetypes "cosmossdk.io/store/types" + upgradetypes "cosmossdk.io/x/upgrade/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/pushchain/push-chain-node/app/upgrades" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +const UpgradeName = "audit-fixes" + +// Hacken audit remediation plus the evm-side fixes. No store or module version +// changes; the handler seeds the one new param so the ante's zero-fallback is +// never the operative value. +func NewUpgrade() upgrades.Upgrade { + return upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateUpgradeHandler, + StoreUpgrades: storetypes.StoreUpgrades{ + Added: []string{}, + Deleted: []string{}, + }, + } +} + +func CreateUpgradeHandler( + mm upgrades.ModuleManager, + configurator module.Configurator, + ak *upgrades.AppKeepers, +) upgradetypes.UpgradeHandler { + return func(ctx context.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + logger := sdkCtx.Logger().With("upgrade", UpgradeName) + logger.Info("starting audit-fixes upgrade") + + if err := setMaxGaslessTxGas(ctx, ak, logger); err != nil { + return nil, err + } + + versionMap, err := mm.RunMigrations(ctx, configurator, fromVM) + if err != nil { + return nil, fmt.Errorf("run migrations: %w", err) + } + + logger.Info("audit-fixes upgrade complete") + return versionMap, nil + } +} + +// setMaxGaslessTxGas writes the cap a gasless tx may declare. Params stored before +// this release decode the new field as 0, which the ante reads as "unset" and +// substitutes the default for; setting it here makes the live value explicit and +// governance-adjustable instead. +func setMaxGaslessTxGas(ctx context.Context, ak *upgrades.AppKeepers, logger interface{ Info(string, ...any) }) error { + if ak == nil || ak.UExecutorKeeper == nil { + return fmt.Errorf("uexecutor keeper unavailable") + } + + params, err := ak.UExecutorKeeper.GetParams(ctx) + if err != nil { + return fmt.Errorf("read uexecutor params: %w", err) + } + + if params.MaxGaslessTxGas != 0 { + logger.Info("max gasless tx gas already set, leaving it alone", + "value", params.MaxGaslessTxGas) + return nil + } + + params.MaxGaslessTxGas = uexecutortypes.DefaultMaxGaslessTxGas + if err := ak.UExecutorKeeper.UpdateParams(ctx, params); err != nil { + return fmt.Errorf("set max gasless tx gas: %w", err) + } + + logger.Info("max gasless tx gas set", "value", uexecutortypes.DefaultMaxGaslessTxGas) + return nil +} diff --git a/app/wasm.go b/app/wasm.go index 70f811bc1..1facc0d0b 100755 --- a/app/wasm.go +++ b/app/wasm.go @@ -1,13 +1,18 @@ package app -// AllCapabilities returns all capabilities available with the current wasmvm +// AllCapabilities returns the wasmvm capabilities enabled on this chain. // See https://github.com/CosmWasm/cosmwasm/blob/main/docs/CAPABILITIES-BUILT-IN.md -// This functionality is going to be moved upstream: https://github.com/CosmWasm/wasmvm/issues/425 +// +// NOTE: "stargate" is deliberately NOT enabled. It lets a contract emit an +// arbitrary encoded sdk.Msg (CosmosMsg::Any / Stargate), which reaches the +// message router without the tx ever passing through the ante handler. That is +// the nested-dispatch vector reported as F-2026-18197: an MsgEthereumTx routed +// that way skips the EVM ante entirely (signature, nonce and gas checks). No +// contract deployed on Push requires it. func AllCapabilities() []string { return []string{ "iterator", "staking", - "stargate", "cosmwasm_1_1", "cosmwasm_1_2", "cosmwasm_1_3", diff --git a/go.mod b/go.mod index 99a0fb2da..0494903e7 100755 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ replace ( cosmossdk.io/x/upgrade => cosmossdk.io/x/upgrade v0.2.0 github.com/CosmWasm/wasmd => github.com/CosmWasm/wasmd v0.55.0 // Keep v0.55.0 github.com/cosmos/cosmos-sdk => github.com/cosmos/cosmos-sdk v0.53.7 // Use stable v0.53.7 - github.com/cosmos/evm => github.com/pushchain/evm v1.0.0-rc2.0.20260907111253-e9816bddce44 + github.com/cosmos/evm => github.com/pushchain/evm v1.0.0-rc2.0.20260911052808-93debf9a8fe6 github.com/ethereum/go-ethereum => github.com/cosmos/go-ethereum v0.0.0-20250806193535-2fc7571efa91 github.com/spf13/viper => github.com/spf13/viper v1.17.0 github.com/strangelove-ventures/tokenfactory => github.com/strangelove-ventures/tokenfactory v0.50.7-wasmvm2 diff --git a/go.sum b/go.sum index cd390b942..d3ce5b1b8 100755 --- a/go.sum +++ b/go.sum @@ -1769,8 +1769,8 @@ github.com/prysmaticlabs/gohashtree v0.0.4-beta.0.20240624100937-73632381301b h1 github.com/prysmaticlabs/gohashtree v0.0.4-beta.0.20240624100937-73632381301b/go.mod h1:HRuvtXLZ4WkaB1MItToVH2e8ZwKwZPY5/Rcby+CvvLY= github.com/prysmaticlabs/prysm/v5 v5.3.0 h1:7Lr8ndapBTZg00YE+MgujN6+yvJR6Bdfn28ZDSJ00II= github.com/prysmaticlabs/prysm/v5 v5.3.0/go.mod h1:r1KhlduqDMIGZ1GhR5pjZ2Ko8Q89noTDYTRoPKwf1+c= -github.com/pushchain/evm v1.0.0-rc2.0.20260907111253-e9816bddce44 h1:AwjH9/uMSblFHrclRp7zctH4+0VdZqChMoeXh3v6jHk= -github.com/pushchain/evm v1.0.0-rc2.0.20260907111253-e9816bddce44/go.mod h1:QuenX5DgRhWeYdIg0J/p65cyS/ntpgnzpZOIajZ/SHk= +github.com/pushchain/evm v1.0.0-rc2.0.20260911052808-93debf9a8fe6 h1:nsKZBgR7H5stpZmeex23MlsQrtPDrlN/CzEgeCcJZOs= +github.com/pushchain/evm v1.0.0-rc2.0.20260911052808-93debf9a8fe6/go.mod h1:QuenX5DgRhWeYdIg0J/p65cyS/ntpgnzpZOIajZ/SHk= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= diff --git a/local-multi-validator/scripts/setup-genesis-auto.sh b/local-multi-validator/scripts/setup-genesis-auto.sh index a24f4d9d5..20eebc97e 100755 --- a/local-multi-validator/scripts/setup-genesis-auto.sh +++ b/local-multi-validator/scripts/setup-genesis-auto.sh @@ -231,7 +231,7 @@ update_genesis '.app_state["gov"]["params"]["expedited_voting_period"]="150s"' # EVM update_genesis `printf '.app_state["evm"]["params"]["evm_denom"]="%s"' $DENOM` -update_genesis '.app_state["evm"]["params"]["active_static_precompiles"]=["0x00000000000000000000000000000000000000CB","0x00000000000000000000000000000000000000ca","0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805"]' +update_genesis '.app_state["evm"]["params"]["active_static_precompiles"]=["0x00000000000000000000000000000000000000ca","0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805","0xEC00000000000000000000000000000000000001"]' # EVM Chain config update_genesis `printf '.app_state["evm"]["params"]["chain_config"]["chain_id"]=%s' $EVM_CHAIN_ID` diff --git a/local-native/scripts/setup-genesis-auto.sh b/local-native/scripts/setup-genesis-auto.sh index 6fb6eb72e..ff12f9c52 100755 --- a/local-native/scripts/setup-genesis-auto.sh +++ b/local-native/scripts/setup-genesis-auto.sh @@ -113,7 +113,7 @@ update_genesis '.app_state["gov"]["params"]["max_deposit_period"]="300s"' update_genesis '.app_state["gov"]["params"]["voting_period"]="300s"' update_genesis '.app_state["gov"]["params"]["expedited_voting_period"]="60s"' update_genesis ".app_state[\"evm\"][\"params\"][\"evm_denom\"]=\"$DENOM\"" -update_genesis '.app_state["evm"]["params"]["active_static_precompiles"]=["0x00000000000000000000000000000000000000CB","0x00000000000000000000000000000000000000ca","0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805"]' +update_genesis '.app_state["evm"]["params"]["active_static_precompiles"]=["0x00000000000000000000000000000000000000ca","0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805","0xEC00000000000000000000000000000000000001"]' update_genesis ".app_state[\"staking\"][\"params\"][\"bond_denom\"]=\"$DENOM\"" update_genesis ".app_state[\"mint\"][\"params\"][\"mint_denom\"]=\"$DENOM\"" update_genesis '.consensus["params"]["abci"]["vote_extensions_enable_height"]="2"' diff --git a/precompiles/usigverifier/README.md b/precompiles/usigverifier/README.md index dd061ea25..72e95d33a 100644 --- a/precompiles/usigverifier/README.md +++ b/precompiles/usigverifier/README.md @@ -44,11 +44,36 @@ interface IUSigVerifier { | Method | Signed bytes | Gas | Use when | |---|---|---|---| -| `verifyEd25519(bytes,bytes32,bytes)` | `"0x" + hex(msgDigest)` (66 ASCII bytes) | 4000 | UEA_SVM / Solana-wallet flows where the user signs a hex string in Phantom/Solflare | -| `verifyEd25519RawMessage(bytes,bytes,bytes)` | Raw `message` bytes | 4000 | New integrations / relayers using standard `ed25519.Sign(privKey, rawBytes)` | +| `verifyEd25519(bytes,bytes32,bytes)` | `"0x" + hex(msgDigest)` (66 ASCII bytes) | 4000 (flat) | UEA_SVM / Solana-wallet flows where the user signs a hex string in Phantom/Solflare | +| `verifyEd25519RawMessage(bytes,bytes,bytes)` | Raw `message` bytes | `4000 + 12` per 32-byte word of `message` | New integrations / relayers using standard `ed25519.Sign(privKey, rawBytes)` | Both methods are `view` and touch no chain state. +### Why only the raw method scales with size + +`ed25519.Verify` hashes the whole message, so its CPU cost grows with the message +(~58 µs at 32 B, ~146 µs at 128 KiB, ~922 µs at 1 MB). `verifyEd25519` always verifies +the same 66-byte ASCII string no matter what the caller sends, so its cost is constant +and its price stays flat. `verifyEd25519RawMessage` verifies caller-supplied bytes, so +it is priced per 32-byte word — the same per-word rate the EVM `SHA-256` precompile +charges for comparable hashing work. + +Because both methods are `view`, a contract can park one large message in memory and +loop `STATICCALL`s over it, paying the calldata only once. Pricing alone is therefore +not the whole defence: `message` is also **hard-capped at 128 KiB** +(`MaxEd25519MessageBytes`), and anything larger reverts with `message too large` +instead of being verified. + +| `len(message)` | Gas | +|---|---| +| 0 | 4,000 | +| 32 B | 4,012 | +| 1 KiB | 4,384 | +| 8 KiB | 7,072 | +| 64 KiB | 28,576 | +| 128 KiB (cap) | 53,152 | +| > 128 KiB | reverts | + ## Verification Semantics Two methods, two distinct signing conventions. **A signature produced for one method will not verify under the other** — the test vectors in `query_test.go` lock this in. @@ -73,13 +98,14 @@ Standard Ed25519 verification — signature is checked against the raw `message` ok = ed25519.Verify(pubKeyBytes, message, signature) ``` -Use this when your signer uses `ed25519.Sign(privKey, rawBytes)` (default in every Solana SDK / nacl library). `message` may be any length, not just 32 bytes. +Use this when your signer uses `ed25519.Sign(privKey, rawBytes)` (default in every Solana SDK / nacl library). `message` may be any length up to `MaxEd25519MessageBytes` (128 KiB), not just 32 bytes. ### Common rules - `pubKey` must be exactly 32 bytes; `signature` must be exactly 64 bytes — otherwise the precompile reverts with `invalid params`. +- `verifyEd25519RawMessage` reverts with `message too large` past `MaxEd25519MessageBytes` (128 KiB). - Unknown method IDs revert with the standard `unknown method` error. -- Both methods cost `4000` gas. +- `verifyEd25519` costs a flat `4000` gas; `verifyEd25519RawMessage` costs `4000` plus `12` per 32-byte word of `message`. ## Generating the ABI @@ -125,7 +151,8 @@ If the call returns `0x` (empty), the precompile is not in `active_static_precom precompiles/usigverifier/ |-- USigVerifier.sol Solidity interface (the source of truth for the ABI) |-- abi.json Embedded into the binary via go:embed -|-- usigverifier.go Precompile struct, NewPrecompile / NewPrecompileV2, RequiredGas, Run -|-- query.go VerifyEd25519 method handler +|-- usigverifier.go Precompile struct, NewPrecompile / NewPrecompileV2, RequiredGas (gas schedule), Run +|-- query.go VerifyEd25519 / VerifyEd25519RawMessage method handlers +|-- gas_test.go Gas-schedule + size-cap regression tests and benchmarks +-- README.md (this file) ``` diff --git a/precompiles/usigverifier/gas_test.go b/precompiles/usigverifier/gas_test.go new file mode 100644 index 000000000..2b188144c --- /dev/null +++ b/precompiles/usigverifier/gas_test.go @@ -0,0 +1,370 @@ +package usigverifier + +import ( + "crypto/ed25519" + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/core/vm" + "github.com/stretchr/testify/require" +) + +// Gas pricing for verifyEd25519RawMessage (F-2026-18140 remediation). +// +// ed25519.Verify runs a SHA-512 pass over the whole message, so its CPU cost +// grows with the message while the old price was a flat 4000 regardless of +// length (~58 µs at 32 B vs ~922 µs at 1 MB on a live node — 16x the work for +// the same fee). The remediation is twofold: price the message per 32-byte word, +// and refuse messages past MaxEd25519MessageBytes outright, because this is a +// view method a contract can loop from memory without re-paying the calldata. + +// capGas is what a message of exactly MaxEd25519MessageBytes costs, and the most +// any verifyEd25519RawMessage call can ever be charged. +const capGas = VerifyEd25519RawMessageBaseGas + + (MaxEd25519MessageBytes/32)*VerifyEd25519RawMessagePerWordGas + +// rawMessageCalldata ABI-encodes a verifyEd25519RawMessage call with a message +// of msgLen bytes, exactly as the EVM would hand it to the precompile. +func rawMessageCalldata(tb testing.TB, msgLen int) []byte { + tb.Helper() + + cd, err := ABI.Pack( + VerifyEd25519RawMessageMethod, + make([]byte, ed25519.PublicKeySize), + make([]byte, msgLen), + make([]byte, ed25519.SignatureSize), + ) + require.NoError(tb, err) + + return cd +} + +// TestRequiredGas_RawMessageScalesWithMessageLength locks in the price curve: +// a flat base plus VerifyEd25519RawMessagePerWordGas for every 32-byte word of +// the message. A flat price fails every row past the first. +func TestRequiredGas_RawMessageScalesWithMessageLength(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + for _, tc := range []struct { + name string + msgLen int + want uint64 + }{ + {"empty", 0, 4000}, + {"1 byte rounds up to a word", 1, 4012}, + {"32 bytes / 1 word", 32, 4012}, + {"33 bytes / 2 words", 33, 4024}, + {"1 KiB", 1024, 4000 + 32*12}, + {"8 KiB", 8 * 1024, 4000 + 256*12}, + {"64 KiB", 64 * 1024, 4000 + 2048*12}, + {"128 KiB (cap)", MaxEd25519MessageBytes, 4000 + 4096*12}, + } { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.want, p.RequiredGas(rawMessageCalldata(t, tc.msgLen)), + "gas must be %d base + %d per 32-byte word", + VerifyEd25519RawMessageBaseGas, VerifyEd25519RawMessagePerWordGas) + }) + } +} + +// TestRequiredGas_RawMessageIsStrictlyIncreasing is the property behind the +// table: every extra word of message must cost more than the word before it. +func TestRequiredGas_RawMessageIsStrictlyIncreasing(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + prev := uint64(0) + for _, msgLen := range []int{0, 32, 64, 1024, 8 * 1024, 64 * 1024, MaxEd25519MessageBytes} { + gas := p.RequiredGas(rawMessageCalldata(t, msgLen)) + require.Greater(t, gas, prev, + "a %d-byte message must cost more than the smaller one before it", msgLen) + prev = gas + } + + // And the growth has to be material, not a rounding error: the largest + // accepted message costs an order of magnitude more than the base. + require.Greater(t, prev, 10*VerifyEd25519RawMessageBaseGas, + "a cap-sized message must cost far more than the flat base price") +} + +// TestRequiredGas_SmallMessagesKeepASaneCost guards the other direction: the +// per-word term must not make ordinary calls (a digest, a short payload) +// noticeably more expensive than they used to be. +func TestRequiredGas_SmallMessagesKeepASaneCost(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + require.Equal(t, VerifyEd25519RawMessageBaseGas, p.RequiredGas(rawMessageCalldata(t, 0)), + "an empty message costs exactly the base") + + for _, msgLen := range []int{1, 32, 64, 128} { + gas := p.RequiredGas(rawMessageCalldata(t, msgLen)) + require.Greater(t, gas, VerifyEd25519RawMessageBaseGas, + "a %d-byte message must cost more than an empty one", msgLen) + require.LessOrEqual(t, gas, VerifyEd25519RawMessageBaseGas+100, + "a %d-byte message must stay within a rounding error of the old flat price", msgLen) + } +} + +// TestRequiredGas_AboveCapIsClampedNotUnbounded: a message past the cap is +// rejected by Run, but it must still be priced — at the cap's price, never more, +// so the charge cannot be inflated (or overflowed) by a declared length. +func TestRequiredGas_AboveCapIsClampedNotUnbounded(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + for _, tc := range []struct { + name string + msgLen int + }{ + {"one byte over the cap", MaxEd25519MessageBytes + 1}, + {"twice the cap", 2 * MaxEd25519MessageBytes}, + {"1 MiB", 1024 * 1024}, + } { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, capGas, p.RequiredGas(rawMessageCalldata(t, tc.msgLen)), + "oversized messages must be priced at the cap, not above it") + }) + } +} + +// TestRequiredGas_LegacyMethodStaysFlat documents the deliberate divergence +// between the two constants. verifyEd25519 takes a bytes32 digest and always +// verifies the same 66-byte ASCII hex string, so its cost does not depend on the +// calldata — even an oversized pubKey argument cannot change the work done. +func TestRequiredGas_LegacyMethodStaysFlat(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + var digest [32]byte + + for _, pubKeyLen := range []int{32, 1024, 64 * 1024} { + cd, err := ABI.Pack( + VerifyEd25519Method, + make([]byte, pubKeyLen), + digest, + make([]byte, ed25519.SignatureSize), + ) + require.NoError(t, err) + + require.Equal(t, VerifyEd25519Gas, p.RequiredGas(cd), + "verifyEd25519 verifies a fixed 66-byte message, so it stays flat (pubKey len=%d)", pubKeyLen) + } +} + +// TestRequiredGas_MalformedCalldataIsPanicFreeAndBounded: RequiredGas runs +// before any validation, on whatever bytes the caller supplied, so it must never +// panic and must never charge more than a cap-sized message. +func TestRequiredGas_MalformedCalldataIsPanicFreeAndBounded(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + valid := rawMessageCalldata(t, 64) + + // mutate overwrites the 32-byte word at args[wordIdx] of a valid calldata. + mutate := func(wordIdx int, word []byte) []byte { + out := make([]byte, len(valid)) + copy(out, valid) + copy(out[4+wordIdx*32:4+(wordIdx+1)*32], word) + return out + } + + allOnes := make([]byte, 32) + for i := range allOnes { + allOnes[i] = 0xff + } + + // The `message` tail sits at args[160:] for a 32-byte pubKey: 3 head words + // plus the pubKey tail (length word + one data word). + const messageLenWordIdx = 5 + + for _, tc := range []struct { + name string + input []byte + }{ + {"nil", nil}, + {"one byte", []byte{0x01}}, + {"selector only", valid[:4]}, + {"selector plus half a head word", valid[:4+16]}, + {"head truncated before the message offset", valid[:4+32]}, + {"tail truncated at the message length word", valid[:4+160]}, + {"message offset larger than a uint64", mutate(1, allOnes)}, + {"message offset points past the calldata", mutate(1, append(make([]byte, 31), 0xff))}, + {"message length larger than a uint64", mutate(messageLenWordIdx, allOnes)}, + } { + t.Run(tc.name, func(t *testing.T) { + var gas uint64 + require.NotPanics(t, func() { gas = p.RequiredGas(tc.input) }, + "RequiredGas must never panic on malformed calldata") + require.LessOrEqual(t, gas, capGas, + "malformed calldata must never be charged more than a cap-sized message") + }) + } + + // An absurd declared length is priced at the cap rather than at the base, so + // lying about the size is not the cheap path. + require.Equal(t, capGas, p.RequiredGas(mutate(messageLenWordIdx, allOnes))) +} + +// TestVerifyEd25519RawMessage_RejectsOversizedMessage is the hard cap: past +// MaxEd25519MessageBytes the call reverts instead of verifying. +func TestVerifyEd25519RawMessage_RejectsOversizedMessage(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + priv := ed25519.NewKeyFromSeed(testSeed) + pub := priv.Public().(ed25519.PublicKey) + + method := ABI.Methods[VerifyEd25519RawMessageMethod] + msg := make([]byte, MaxEd25519MessageBytes+1) + sig := ed25519.Sign(priv, msg) + + bz, err := p.VerifyEd25519RawMessage(&method, []interface{}{[]byte(pub), msg, sig}) + + // Value assertion first: an error assertion aborts the test, and a nil + // result is the thing that proves no verification happened. + require.Nil(t, bz, "an oversized message must not produce a verification result") + require.Error(t, err, "a message past the cap must be rejected, not verified") + require.Contains(t, err.Error(), "message too large") +} + +// TestVerifyEd25519RawMessage_AcceptsMessageAtCap: the cap is inclusive — a +// message of exactly MaxEd25519MessageBytes still verifies. +func TestVerifyEd25519RawMessage_AcceptsMessageAtCap(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + priv := ed25519.NewKeyFromSeed(testSeed) + pub := priv.Public().(ed25519.PublicKey) + + method := ABI.Methods[VerifyEd25519RawMessageMethod] + msg := make([]byte, MaxEd25519MessageBytes) + sig := ed25519.Sign(priv, msg) + + bz, err := p.VerifyEd25519RawMessage(&method, []interface{}{[]byte(pub), msg, sig}) + require.NoError(t, err) + + out, err := method.Outputs.Unpack(bz) + require.NoError(t, err) + require.Equal(t, []interface{}{true}, out, "a message of exactly the cap must still verify") +} + +// TestRun_OversizedMessageReverts drives the same cap through the entry point +// the EVM actually calls, on real ABI-encoded calldata. +func TestRun_OversizedMessageReverts(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + priv := ed25519.NewKeyFromSeed(testSeed) + pub := priv.Public().(ed25519.PublicKey) + + t.Run("at the cap it verifies", func(t *testing.T) { + msg := make([]byte, MaxEd25519MessageBytes) + cd, err := ABI.Pack(VerifyEd25519RawMessageMethod, []byte(pub), msg, ed25519.Sign(priv, msg)) + require.NoError(t, err) + + bz, err := p.Run(nil, &vm.Contract{Input: cd}, true) + require.NoError(t, err) + + method := ABI.Methods[VerifyEd25519RawMessageMethod] + out, err := method.Outputs.Unpack(bz) + require.NoError(t, err) + require.Equal(t, []interface{}{true}, out) + }) + + t.Run("past the cap it reverts", func(t *testing.T) { + msg := make([]byte, MaxEd25519MessageBytes+1) + cd, err := ABI.Pack(VerifyEd25519RawMessageMethod, []byte(pub), msg, ed25519.Sign(priv, msg)) + require.NoError(t, err) + + bz, err := p.Run(nil, &vm.Contract{Input: cd}, true) + + require.Nil(t, bz, "an oversized message must not produce a verification result") + require.Error(t, err) + require.Contains(t, err.Error(), "message too large") + }) +} + +// TestLargeMessageLoopIsGasProhibitive is the abuse shape the finding describes: +// a contract parks one large message in memory (paying its calldata once) and +// loops STATICCALLs over it. What bounds that loop is the per-call gas, so the +// number of verifications a single block can be made to run must drop sharply +// against the old flat price. +func TestLargeMessageLoopIsGasProhibitive(t *testing.T) { + p, err := NewPrecompile() + require.NoError(t, err) + + const ( + blockGasLimit = uint64(100_000_000) + oldFlatGas = uint64(4000) // the pre-fix price, at any message length + ) + + gas := p.RequiredGas(rawMessageCalldata(t, MaxEd25519MessageBytes)) + + iterationsNow := blockGasLimit / gas + iterationsBefore := blockGasLimit / oldFlatGas + + require.Less(t, iterationsNow, iterationsBefore/10, + "a cap-sized message must buy at least 10x fewer verifications per block "+ + "than the old flat price did (now %d, before %d)", iterationsNow, iterationsBefore) +} + +// BenchmarkVerifyEd25519RawMessage shows the cost curve the pricing has to +// track. The gas/us column is the one to read: under the old flat price it fell +// away as the message grew (the same 4000 gas bought steadily more CPU), which +// is the finding. With the per-word term it holds up instead. +// +// go test ./precompiles/usigverifier/ -run '^$' -bench VerifyEd25519RawMessage -benchmem +func BenchmarkVerifyEd25519RawMessage(b *testing.B) { + p, err := NewPrecompile() + require.NoError(b, err) + + priv := ed25519.NewKeyFromSeed(testSeed) + pub := priv.Public().(ed25519.PublicKey) + method := ABI.Methods[VerifyEd25519RawMessageMethod] + + for _, msgLen := range []int{32, 1024, 8 * 1024, 64 * 1024, MaxEd25519MessageBytes} { + msg := make([]byte, msgLen) + sig := ed25519.Sign(priv, msg) + args := []interface{}{[]byte(pub), msg, sig} + gas := p.RequiredGas(rawMessageCalldata(b, msgLen)) + + b.Run(fmt.Sprintf("msg=%dB", msgLen), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := p.VerifyEd25519RawMessage(&method, args); err != nil { + b.Fatal(err) + } + } + usPerOp := float64(b.Elapsed().Nanoseconds()) / float64(b.N) / 1000 + b.ReportMetric(float64(gas), "gas/op") + b.ReportMetric(float64(gas)/usPerOp, "gas/us") + }) + } +} + +// BenchmarkRequiredGas checks the pricing itself stays cheap — it runs on every +// call, before any validation, so it must not become the expensive part. +// +// go test ./precompiles/usigverifier/ -run '^$' -bench RequiredGas -benchmem +func BenchmarkRequiredGas(b *testing.B) { + p, err := NewPrecompile() + require.NoError(b, err) + + cd, err := ABI.Pack( + VerifyEd25519RawMessageMethod, + make([]byte, ed25519.PublicKeySize), + make([]byte, MaxEd25519MessageBytes), + make([]byte, ed25519.SignatureSize), + ) + require.NoError(b, err) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if p.RequiredGas(cd) == 0 { + b.Fatal("unexpected zero gas") + } + } +} diff --git a/precompiles/usigverifier/genesis_scripts_test.go b/precompiles/usigverifier/genesis_scripts_test.go new file mode 100644 index 000000000..00f73b88e --- /dev/null +++ b/precompiles/usigverifier/genesis_scripts_test.go @@ -0,0 +1,60 @@ +package usigverifier_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + usigverifierprecompile "github.com/pushchain/push-chain-node/precompiles/usigverifier" +) + +// legacyUtxHashVerifierAddress is the stale entry: the utxhashverifier +// precompile has no implementation anywhere in the tree, and the +// remove-utxverifier upgrade strips it from live chains. Leaving it in genesis +// would re-introduce on every fresh chain exactly the address that upgrade +// exists to remove. +const legacyUtxHashVerifierAddress = "0x00000000000000000000000000000000000000cb" + +// TestGenesisScriptsActivateCurrentVerifier guards the genesis half of +// F-2026-18829: a fresh chain must activate the address the verifier is actually +// registered at, and must not declare the legacy one. +func TestGenesisScriptsActivateCurrentVerifier(t *testing.T) { + repoRoot := filepath.Join("..", "..") + + scripts := []string{ + "scripts/test_node.sh", + "local-native/scripts/setup-genesis-auto.sh", + "local-multi-validator/scripts/setup-genesis-auto.sh", + "testnet/core/setup/setup_genesis_validator.sh", + } + + for _, script := range scripts { + t.Run(script, func(t *testing.T) { + raw, err := os.ReadFile(filepath.Join(repoRoot, script)) + require.NoError(t, err) + + var line string + for _, l := range strings.Split(string(raw), "\n") { + if strings.Contains(l, "active_static_precompiles") { + line = l + break + } + } + require.NotEmpty(t, line, "no active_static_precompiles assignment found") + + require.NotContains(t, strings.ToLower(line), strings.ToLower(legacyUtxHashVerifierAddress), + "genesis must not declare the utxhashverifier address, nothing is registered at it") + // The node registers the verifier at both addresses (app.go), so genesis + // must activate both: 0x…ca stays live for contracts that hardcoded it. + require.Contains(t, strings.ToLower(line), + strings.ToLower(usigverifierprecompile.USigVerifierPrecompileAddress), + "genesis must activate the verifier address the node registers") + require.Contains(t, strings.ToLower(line), + strings.ToLower(usigverifierprecompile.USigVerifierPrecompileAddressV2), + "genesis must activate the v2 verifier address the node registers") + }) + } +} diff --git a/precompiles/usigverifier/query.go b/precompiles/usigverifier/query.go index ce94da087..a01c9f278 100644 --- a/precompiles/usigverifier/query.go +++ b/precompiles/usigverifier/query.go @@ -57,7 +57,8 @@ func (p Precompile) VerifyEd25519( // VerifyEd25519RawMessage verifies a signature over raw message bytes — // standard Ed25519 semantics. Use this when the signer used the conventional -// ed25519.Sign(privKey, rawBytes) API. +// ed25519.Sign(privKey, rawBytes) API. Messages larger than +// MaxEd25519MessageBytes are rejected. func (p Precompile) VerifyEd25519RawMessage( method *abi.Method, args []interface{}, @@ -73,6 +74,13 @@ func (p Precompile) VerifyEd25519RawMessage( return nil, fmt.Errorf("invalid message type") } + // Hard size limit. RequiredGas already prices the message per 32-byte word, + // but this is a view method a contract can loop cheaply from memory, so the + // length is bounded outright rather than only priced. + if len(message) > MaxEd25519MessageBytes { + return nil, fmt.Errorf("message too large: %d bytes, max %d", len(message), MaxEd25519MessageBytes) + } + signature, ok := args[2].([]byte) if !ok { return nil, fmt.Errorf("invalid signature type") diff --git a/precompiles/usigverifier/usigverifier.go b/precompiles/usigverifier/usigverifier.go index 7e6f4d12c..9e70ef107 100644 --- a/precompiles/usigverifier/usigverifier.go +++ b/precompiles/usigverifier/usigverifier.go @@ -2,7 +2,9 @@ package usigverifier import ( "embed" + "encoding/binary" "fmt" + "math" storetypes "cosmossdk.io/store/types" "github.com/ethereum/go-ethereum/accounts/abi" @@ -13,13 +15,29 @@ import ( ) const ( + // Both addresses stay registered: 0x…CA is live on donut and dropping it + // would unregister the precompile for contracts already calling it. USigVerifierPrecompileAddress = "0x00000000000000000000000000000000000000ca" USigVerifierPrecompileAddressV2 = "0xEC00000000000000000000000000000000000001" - // VerifyEd25519Gas is the gas cost for verifying an Ed25519 signature. + // VerifyEd25519Gas is the gas cost for verifying an Ed25519 signature over a + // bytes32 digest. The verified message is always the 66-byte ASCII hex form + // of that digest, so the work does not vary with the calldata — flat is the + // honest price here. VerifyEd25519Gas uint64 = 4000 - // VerifyEd25519RawMessageGas matches VerifyEd25519Gas — same Ed25519 - // verification cost, only the message-prep step differs (no hex encoding). - VerifyEd25519RawMessageGas uint64 = 4000 + // VerifyEd25519RawMessageBaseGas is the fixed part of a raw-message + // verification: the Ed25519 curve arithmetic, which dominates below ~8 KiB. + VerifyEd25519RawMessageBaseGas uint64 = 4000 + // VerifyEd25519RawMessagePerWordGas is charged for every 32-byte word of the + // message on top of the base, so that the SHA-512 pass Ed25519 makes over the + // whole message is paid for. Priced off the EVM SHA-256 precompile, which + // charges 12 gas per 32-byte word for comparable hashing work. + VerifyEd25519RawMessagePerWordGas uint64 = 12 + // MaxEd25519MessageBytes hard-caps the message a raw-message verification + // accepts (128 KiB, the same limit used for gateway payloads). This is a view + // method, so a contract can hold one large message in memory and loop + // STATICCALLs over it, paying the calldata only once; on that path a price + // curve alone is not a defence, the size has to be bounded outright. + MaxEd25519MessageBytes = 128 * 1024 ) var _ vm.PrecompiledContract = &Precompile{} @@ -85,6 +103,8 @@ func NewPrecompileV2() (*Precompile, error) { return p, nil } +// RequiredGas is charged before Run executes, so it runs on unvalidated, +// attacker-controlled calldata and must never panic. func (p Precompile) RequiredGas(input []byte) uint64 { // NOTE: This check avoid panicking when trying to decode the method ID if len(input) < 4 { @@ -101,12 +121,77 @@ func (p Precompile) RequiredGas(input []byte) uint64 { case VerifyEd25519Method: return VerifyEd25519Gas case VerifyEd25519RawMessageMethod: - return VerifyEd25519RawMessageGas + return verifyEd25519RawMessageGas(rawMessageLen(input)) default: return p.Precompile.RequiredGas(input, p.IsTransaction(method)) } } +// verifyEd25519RawMessageGas prices a raw-message verification: a flat base for +// the curve arithmetic plus a per-word term for the hash pass over the message. +// A msgLen past MaxEd25519MessageBytes is clamped — Run rejects those calls, and +// clamping keeps the charge bounded (and overflow-free) for a calldata that +// declares an absurd length. +func verifyEd25519RawMessageGas(msgLen uint64) uint64 { + if msgLen > MaxEd25519MessageBytes { + msgLen = MaxEd25519MessageBytes + } + + words := (msgLen + 31) / 32 + + return VerifyEd25519RawMessageBaseGas + words*VerifyEd25519RawMessagePerWordGas +} + +// rawMessageLen recovers the declared length of the `message` argument of +// verifyEd25519RawMessage(bytes,bytes,bytes) straight out of the ABI-encoded +// calldata, without decoding the payload. `input` includes the 4-byte method ID. +// +// Layout: one 32-byte head slot per argument holding the offset of its tail, +// then each dynamic tail starting with a 32-byte length. `message` is argument +// index 1. Calldata that does not parse is priced at 0 extra — Run reverts on it +// before any verification happens — while a length too large for a uint64 is +// reported as the maximum so it prices at the cap instead of the base. +func rawMessageLen(input []byte) uint64 { + const ( + wordSize = 32 + messageArgIdx = 1 + ) + + if len(input) < 4 { + return 0 + } + args := input[4:] + + head := messageArgIdx * wordSize + if len(args) < head+wordSize { + return 0 + } + + offset, ok := abiWordToUint64(args[head : head+wordSize]) + if !ok || offset > uint64(len(args)) || uint64(len(args))-offset < wordSize { + return 0 + } + + length, ok := abiWordToUint64(args[offset : offset+wordSize]) + if !ok { + return math.MaxUint64 + } + + return length +} + +// abiWordToUint64 reads a big-endian 32-byte ABI word as a uint64. ok is false +// when the word does not fit one, i.e. its top 24 bytes are not all zero. +func abiWordToUint64(word []byte) (value uint64, ok bool) { + for _, b := range word[:len(word)-8] { + if b != 0 { + return 0, false + } + } + + return binary.BigEndian.Uint64(word[len(word)-8:]), true +} + func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz []byte, err error) { if len(contract.Input) < 4 { return nil, vm.ErrExecutionReverted diff --git a/proto/uexecutor/v1/tx.proto b/proto/uexecutor/v1/tx.proto index 48ea21115..0e4248a5e 100755 --- a/proto/uexecutor/v1/tx.proto +++ b/proto/uexecutor/v1/tx.proto @@ -22,9 +22,6 @@ service Msg { // ExecutePayload defines a message for executing a universal payload rpc ExecutePayload(MsgExecutePayload) returns (MsgExecutePayloadResponse); - // MigrateUEA defines a message for migrating UEA - rpc MigrateUEA(MsgMigrateUEA) returns (MsgMigrateUEAResponse); - // VoteInbound defines a message for voting on synthetic assets bridging from external chain to PC rpc VoteInbound(MsgVoteInbound) returns (MsgVoteInboundResponse); @@ -38,6 +35,17 @@ service Msg { // ballot has expired without finalizing, refunding the user on the source // chain via the normal revert/outbound flow. Admin-only escape hatch. rpc RevertStuckInbound(MsgRevertStuckInbound) returns (MsgRevertStuckInboundResponse); + + // ExecuteStuckInbound finalizes an inbound ballot that is provably unable to + // finalize on its own yet already carries enough YES votes, then runs the + // normal post-finalization pipeline so the user receives funds on Push. + // Admin-only escape hatch, sibling of RevertStuckInbound. + rpc ExecuteStuckInbound(MsgExecuteStuckInbound) returns (MsgExecuteStuckInboundResponse); + + // ExecuteStuckOutbound settles an outbound whose ballot can no longer reach a + // terminal-and-settled state, running the same post-finalization pipeline a + // finalizing vote would have run. Admin-only escape hatch. + rpc ExecuteStuckOutbound(MsgExecuteStuckOutbound) returns (MsgExecuteStuckOutboundResponse); } // MsgUpdateParams is the Msg/UpdateParams request type. @@ -83,27 +91,6 @@ message MsgExecutePayload { // MsgExecutePayloadResponse defines the response for MsgExecutePayload. message MsgExecutePayloadResponse {} -// MsgMigrateUEA defines a message for migarting Universal Executor Account (UEA) -message MsgMigrateUEA { - option (amino.name) = "uexecutor/MsgMigrateUEA"; - option (cosmos.msg.v1.signer) = "signer"; - - // signer is the Cosmos address initiating the tx (used for tx signing) - string signer = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; - - // universal_account_id is the identifier of the owner account - UniversalAccountId universal_account_id = 2; - - // payload is the migration payload to be executed - MigrationPayload migration_payload = 3; - - // signature is the bytes passed as verifier data for the given payload. - string signature = 4; -} - -// MsgMigrateUEAResponse defines the response for MsgMigrateUEA. -message MsgMigrateUEAResponse {} - // MsgVoteInbound allows a universal validator to vote on an inbound transfer. message MsgVoteInbound { option (amino.name) = "ue/MsgVoteInbound"; @@ -166,3 +153,51 @@ message MsgRevertStuckInboundResponse { string utx_id = 1; // ID of the UTX created to hold the revert string outbound_id = 2; // ID of the INBOUND_REVERT outbound created } + +// MsgExecuteStuckInbound is an admin escape hatch and the sibling of +// MsgRevertStuckInbound. For an inbound whose ballot is stored PENDING but can +// never finalize on its own, and whose YES votes already meet the recomputed +// threshold, this marks the ballot PASSED and runs the same post-finalization +// pipeline a finalizing vote would have run - so the user receives the bridged +// funds on Push instead of a source-chain refund. +message MsgExecuteStuckInbound { + option (amino.name) = "uexecutor/MsgExecuteStuckInbound"; + option (cosmos.msg.v1.signer) = "signer"; + + // signer must equal uvalidator Params.Admin + string signer = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // inbound is the original payload the stuck ballot was voting on. Admin + // supplies this from off-chain UV observation logs since the chain does not + // persist ballot payloads. + Inbound inbound = 2; +} + +message MsgExecuteStuckInboundResponse { + string utx_id = 1; // ID of the UTX created for the executed inbound +} + +// MsgExecuteStuckOutbound is an admin escape hatch for an outbound whose ballot +// can no longer reach a terminal-and-settled state — EXPIRED, or PENDING with +// every eligible voter already voted and the YES votes at the stored threshold. +// It runs the same settlement pipeline a finalizing vote would have run, so the +// outcome follows observed_tx.success: a success settles, a failure mints the +// bridged tokens back to the revert recipient and refunds the excess gas. +message MsgExecuteStuckOutbound { + option (amino.name) = "uexecutor/MsgExecuteStuckOutbound"; + option (cosmos.msg.v1.signer) = "signer"; + + // signer must equal uvalidator Params.Admin + string signer = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string tx_id = 2; // txId of outbound tx + string utx_id = 3; // UniversalTx Id + // observed_tx is the destination-chain observation the stuck ballot was voting + // on. Admin supplies this from off-chain UV observation logs since the chain + // does not persist ballot payloads; it must match field-for-field or the + // derived ballot key finds no ballot. + OutboundObservation observed_tx = 4; +} + +message MsgExecuteStuckOutboundResponse { + string outbound_id = 1; // ID of the outbound that was settled +} diff --git a/proto/uexecutor/v1/types.proto b/proto/uexecutor/v1/types.proto index d5c5a591e..ff166ab56 100644 --- a/proto/uexecutor/v1/types.proto +++ b/proto/uexecutor/v1/types.proto @@ -13,6 +13,13 @@ message Params { option (gogoproto.goproto_stringer) = false; bool some_value = 2; + + // max_gasless_tx_gas is the maximum gas limit a fee-exempt (gasless) + // transaction is allowed to declare. Gasless transactions pay no fee, so + // their declared gas is not bounded by anything the sender has to spend; + // this cap is the only bound on how much a single gasless transaction can + // contribute to the block's cumulative gas wanted. + uint64 max_gasless_tx_gas = 3; } // Signature verification types @@ -38,17 +45,6 @@ message UniversalPayload { VerificationType v_type = 9; // Type of verification to use before execution } -// MigrationPayload mirrors the Solidity struct -message MigrationPayload { - option (amino.name) = "uexecutor/migration_payload"; - option (gogoproto.equal) = true; - option (gogoproto.goproto_stringer) = false; - - string migration = 1; // Migration Address - string nonce = 2; // unit256 as string - string deadline = 3; // unit256 as string -} - // UniversalAccountId is the identifier of a owner account message UniversalAccountId { option (amino.name) = "uexecutor/universal_account"; diff --git a/proto/utss/v1/tx.proto b/proto/utss/v1/tx.proto index 735c019da..e490097bc 100755 --- a/proto/utss/v1/tx.proto +++ b/proto/utss/v1/tx.proto @@ -87,6 +87,12 @@ message MsgInitiateFundMigration { string signer = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; string old_key_id = 2; string chain = 3; // CAIP-2 chain identifier + // Native balance (wei, uint256 decimal) observed by the admin on the old TSS + // address. The chain derives transfer_amount = balance - gas - l1_gas_fee from + // it, using the same fee figures it pins into the migration record, so every + // universal validator signs one amount instead of re-deriving it from a live + // balance that a 1-wei inflow can shift (F-2026-18142). + string balance = 4; } message MsgInitiateFundMigrationResponse { diff --git a/proto/utss/v1/types.proto b/proto/utss/v1/types.proto index 7084065da..9ca513147 100644 --- a/proto/utss/v1/types.proto +++ b/proto/utss/v1/types.proto @@ -105,4 +105,5 @@ message FundMigration { string gas_price = 11; // gas price from oracle (wei) uint64 gas_limit = 12; // gas limit sourced from UniversalCore per chain namespace string l1_gas_fee = 13; // L1 data-availability fee (wei) from UniversalCore; 0 for non-L2 chains + string transfer_amount = 14; // native amount (wei) to sweep, derived at initiate time as balance - (gas_price * gas_limit) - l1_gas_fee } diff --git a/scripts/test_node.sh b/scripts/test_node.sh index 86d1feb8a..e073e98df 100755 --- a/scripts/test_node.sh +++ b/scripts/test_node.sh @@ -116,7 +116,7 @@ from_scratch () { # (LoadEvmCoinInfo); without metadata for the base denom the node panics on start # with "denom metadata could not be found". update_test_genesis '.app_state["bank"]["denom_metadata"]=[{"description":"Native token of Push Chain","denom_units":[{"denom":"upc","exponent":0,"aliases":[]},{"denom":"pushchain","exponent":18,"aliases":[]}],"base":"upc","display":"pushchain","name":"Push Chain","symbol":"PC"}]' - update_test_genesis '.app_state["evm"]["params"]["active_static_precompiles"]=["0x00000000000000000000000000000000000000CB","0x00000000000000000000000000000000000000ca","0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805"]' + update_test_genesis '.app_state["evm"]["params"]["active_static_precompiles"]=["0x00000000000000000000000000000000000000ca","0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805","0xEC00000000000000000000000000000000000001"]' update_test_genesis '.app_state["erc20"]["native_precompiles"]=["0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"]' # https://eips.ethereum.org/EIPS/eip-7528 update_test_genesis `printf '.app_state["erc20"]["token_pairs"]=[{contract_owner:1,erc20_address:"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",denom:"%s",enabled:true}]' $DENOM` update_test_genesis '.app_state["feemarket"]["params"]["no_base_fee"]=false' diff --git a/test/integration/ante/gasless_gas_limit_test.go b/test/integration/ante/gasless_gas_limit_test.go new file mode 100644 index 000000000..1ad5fe3c9 --- /dev/null +++ b/test/integration/ante/gasless_gas_limit_test.go @@ -0,0 +1,332 @@ +package ante_test + +import ( + "fmt" + "math" + "math/rand" + "testing" + "time" + + abci "github.com/cometbft/cometbft/abci/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/stretchr/testify/require" + + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/testutil/mock" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/pushchain/push-chain-node/app" + uexecutorkeeper "github.com/pushchain/push-chain-node/x/uexecutor/keeper" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// --------------------------------------------------------------------------- +// F-2026-18144 — Unchecked Cumulative GasWanted Can Fail FinalizeBlock Under +// Unbounded Block Gas. +// +// Fee-paying txs are self-limiting: the ante handler requires +// ceil(minGasPrice * gasLimit), so a huge declared gas costs huge money. +// Gasless txs pay nothing, so the declared gas is free — and it is still added +// to the fee market's cumulative gas wanted for the block. Two gasless txs each +// declaring MaxInt64 sum to a value that x/feemarket EndBlock cannot convert +// back to int64; it returns an error, and that error comes out of +// FinalizeBlock, after the block has already been decided. +// +// These tests run real signed txs through the whole baseapp -> ante -> +// EndBlock pipeline with the block gas limit set to the unbounded value donut +// runs (max_gas: -1), which is what makes the per-tx block-limit check in the +// EVM ante inert. +// --------------------------------------------------------------------------- + +// setupGaslessAnteApp boots a chain app with a single validator and one funded +// genesis account whose private key we keep, so we can sign real gasless txs. +func setupGaslessAnteApp(t *testing.T) (*app.ChainApp, cryptotypes.PrivKey, sdk.AccAddress) { + t.Helper() + + privVal := mock.NewPV() + valPubKey, err := privVal.GetPubKey() + require.NoError(t, err) + + valSet := cmttypes.NewValidatorSet([]*cmttypes.Validator{cmttypes.NewValidator(valPubKey, 1)}) + + senderPrivKey := secp256k1.GenPrivKey() + senderAcc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), 0, 0) + senderAddr := senderAcc.GetAddress() + + balance := banktypes.Balance{ + Address: senderAddr.String(), + Coins: sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100_000_000_000_000)), + sdk.NewCoin(app.BaseDenom, sdkmath.NewInt(1).MulRaw(1e18).MulRaw(100)), + ), + } + + chainApp := app.SetupWithGenesisValSet( + t, valSet, []authtypes.GenesisAccount{senderAcc}, testChainID, nil, balance, + ) + + setUnboundedBlockGas(t, chainApp) + + return chainApp, senderPrivKey, senderAddr +} + +// setUnboundedBlockGas reproduces donut's consensus configuration +// (update_max_block_gas.json sets max_gas to -1), under which +// ante/types.BlockGasLimit returns math.MaxUint64 and the per-tx block gas +// check can never fire. Written through an uncached context so it survives +// into FinalizeBlock. +func setUnboundedBlockGas(t *testing.T, chainApp *app.ChainApp) { + t.Helper() + + ctx := chainApp.BaseApp.NewUncachedContext(false, cmtproto.Header{}) + cp, err := chainApp.ConsensusParamsKeeper.ParamsStore.Get(ctx) + require.NoError(t, err) + cp.Block.MaxGas = -1 + require.NoError(t, chainApp.ConsensusParamsKeeper.ParamsStore.Set(ctx, cp)) +} + +// gaslessVoteMsg builds a MsgVoteInbound — one of the fee-exempt message types +// in app/txpolicy/gasless.go — that passes ValidateBasic, so the tx is only +// ever stopped by a gas decision and not by message validation. +func gaslessVoteMsg(signer sdk.AccAddress, nonce int) sdk.Msg { + return &uexecutortypes.MsgVoteInbound{ + Signer: signer.String(), + Inbound: &uexecutortypes.Inbound{ + SourceChain: "eip155:11155111", + TxHash: fmt.Sprintf("0xf18144000000000000000000000000000000000000000000000000000000%04d", nonce), + Sender: "0x1111111111111111111111111111111111111111", + Recipient: "0x2222222222222222222222222222222222222222", + Amount: "1", + AssetAddr: "0x3333333333333333333333333333333333333333", + LogIndex: "0", + TxType: uexecutortypes.TxType_FUNDS, + }, + } +} + +// deliverGaslessBlock signs one gasless tx per entry in gasLimits and delivers +// them as a single block. +func deliverGaslessBlock( + t *testing.T, + chainApp *app.ChainApp, + priv cryptotypes.PrivKey, + addr sdk.AccAddress, + gasLimits []uint64, +) (*abci.ResponseFinalizeBlock, error) { + t.Helper() + + ctx := chainApp.BaseApp.NewContext(true) + acc := chainApp.AccountKeeper.GetAccount(ctx, addr) + require.NotNil(t, acc) + + txBytes := make([][]byte, 0, len(gasLimits)) + for i, gas := range gasLimits { + tx, err := simtestutil.GenSignedMockTx( + rand.New(rand.NewSource(int64(i)+1)), + chainApp.TxConfig(), + []sdk.Msg{gaslessVoteMsg(addr, i)}, + sdk.NewCoins(), // gasless: no fee is offered at all + gas, + testChainID, + []uint64{acc.GetAccountNumber()}, + []uint64{acc.GetSequence() + uint64(i)}, + priv, + ) + require.NoError(t, err) + + bz, err := chainApp.TxConfig().TxEncoder()(tx) + require.NoError(t, err) + txBytes = append(txBytes, bz) + } + + return chainApp.FinalizeBlock(&abci.RequestFinalizeBlock{ + Height: chainApp.LastBlockHeight() + 1, + Time: time.Now(), + Txs: txBytes, + }) +} + +func blockGasWanted(t *testing.T, chainApp *app.ChainApp) uint64 { + t.Helper() + return chainApp.FeeMarketKeeper.GetBlockGasWanted(chainApp.BaseApp.NewContext(true)) +} + +// TestGaslessCumulativeGasWantedCannotFailFinalizeBlock is the chain-level +// regression for the finding itself. Two gasless txs each declaring MaxInt64 +// sum to 2^64-2 — a valid uint64 that x/feemarket EndBlock cannot convert to +// int64. Neither tx is individually rejectable without the cap. +func TestGaslessCumulativeGasWantedCannotFailFinalizeBlock(t *testing.T) { + chainApp, priv, addr := setupGaslessAnteApp(t) + + res, err := deliverGaslessBlock(t, chainApp, priv, addr, []uint64{math.MaxInt64, math.MaxInt64}) + + // The block must still be produced. Without the cap, x/feemarket EndBlock + // errors on the cumulative total and that error surfaces through + // FinalizeBlock, leaving CometBFT at height H and the app at H-1. + require.NoError(t, err, "FinalizeBlock must survive the cumulative gas wanted") + require.NotNil(t, res) + require.Len(t, res.TxResults, 2) + + require.Less(t, blockGasWanted(t, chainApp), uint64(1_000_000), + "rejected txs must not contribute their declared gas to the block") + + for i, txRes := range res.TxResults { + require.NotEqual(t, abci.CodeTypeOK, txRes.Code, "tx %d must be rejected, got success", i) + require.Contains(t, txRes.Log, "exceeds the maximum allowed", + "tx %d must be rejected by the gasless gas cap, got: %s", i, txRes.Log) + } +} + +// TestGaslessTxAboveCapRejected covers a single tx one unit over the cap. +func TestGaslessTxAboveCapRejected(t *testing.T) { + chainApp, priv, addr := setupGaslessAnteApp(t) + + res, err := deliverGaslessBlock(t, chainApp, priv, addr, + []uint64{uexecutortypes.DefaultMaxGaslessTxGas + 1}) + require.NoError(t, err) + require.Len(t, res.TxResults, 1) + + require.Less(t, blockGasWanted(t, chainApp), uint64(1_000_000), + "an over-cap tx must not have its declared gas counted") + + txRes := res.TxResults[0] + require.NotEqual(t, abci.CodeTypeOK, txRes.Code, "over-cap gasless tx must be rejected") + require.Contains(t, txRes.Log, "exceeds the maximum allowed", "got: %s", txRes.Log) +} + +// TestGaslessTxAtCapAccepted pins the other side of the boundary: a tx at +// exactly the cap passes the ante chain and its gas is counted normally. +// x/feemarket EndBlock records max(gasWanted*MinGasMultiplier, gasUsed), and +// MinGasMultiplier defaults to 0.5, so a 100,000,000 declaration must show up +// as at least 50,000,000. +func TestGaslessTxAtCapAccepted(t *testing.T) { + chainApp, priv, addr := setupGaslessAnteApp(t) + + res, err := deliverGaslessBlock(t, chainApp, priv, addr, + []uint64{uexecutortypes.DefaultMaxGaslessTxGas}) + require.NoError(t, err) + require.Len(t, res.TxResults, 1) + + require.GreaterOrEqual(t, blockGasWanted(t, chainApp), uint64(50_000_000), + "a tx at the cap must reach the fee market and have its gas counted") + require.NotContains(t, res.TxResults[0].Log, "exceeds the maximum allowed", + "a tx at the cap must not be rejected by the cap") +} + +// TestGaslessTxAtUniversalValidatorGasAccepted uses the gas limit the universal +// validators now declare (universalClient/pushsigner/vote.go). The fleet must +// keep voting under the cap. +func TestGaslessTxAtUniversalValidatorGasAccepted(t *testing.T) { + chainApp, priv, addr := setupGaslessAnteApp(t) + + res, err := deliverGaslessBlock(t, chainApp, priv, addr, []uint64{100_000_000}) + require.NoError(t, err) + require.Len(t, res.TxResults, 1) + + require.GreaterOrEqual(t, blockGasWanted(t, chainApp), uint64(50_000_000), + "the universal validator's declared gas must still be accepted") + require.NotContains(t, res.TxResults[0].Log, "exceeds the maximum allowed", "got: %s", res.TxResults[0].Log) +} + +// TestGaslessCapIsAGovernanceParameter proves the cap is state, not a compiled +// constant: a governance update to uexecutor params changes what the ante +// handler accepts on the very next block. +func TestGaslessCapIsAGovernanceParameter(t *testing.T) { + const loweredCap = uint64(30_000_000) + // Comfortably under the 100,000,000 default and comfortably over the + // lowered cap, so only the parameter can decide the outcome. + const declaredGas = uint64(40_000_000) + + t.Run("default cap admits 40M", func(t *testing.T) { + chainApp, priv, addr := setupGaslessAnteApp(t) + + res, err := deliverGaslessBlock(t, chainApp, priv, addr, []uint64{declaredGas}) + require.NoError(t, err) + + require.GreaterOrEqual(t, blockGasWanted(t, chainApp), uint64(20_000_000), + "40M must be accepted under the default cap") + require.NotContains(t, res.TxResults[0].Log, "exceeds the maximum allowed", "got: %s", res.TxResults[0].Log) + }) + + t.Run("governance lowers the cap and 40M is rejected", func(t *testing.T) { + chainApp, priv, addr := setupGaslessAnteApp(t) + setGaslessCapByGovernance(t, chainApp, loweredCap) + + res, err := deliverGaslessBlock(t, chainApp, priv, addr, []uint64{declaredGas}) + require.NoError(t, err) + + require.Less(t, blockGasWanted(t, chainApp), uint64(1_000_000), + "a tx over the lowered cap must not have its gas counted") + + txRes := res.TxResults[0] + require.NotEqual(t, abci.CodeTypeOK, txRes.Code) + require.Contains(t, txRes.Log, "exceeds the maximum allowed", "got: %s", txRes.Log) + require.Contains(t, txRes.Log, "30000000", "the error must quote the governance-set cap, got: %s", txRes.Log) + }) + + t.Run("governance lowers the cap and 30M is still accepted", func(t *testing.T) { + chainApp, priv, addr := setupGaslessAnteApp(t) + setGaslessCapByGovernance(t, chainApp, loweredCap) + + res, err := deliverGaslessBlock(t, chainApp, priv, addr, []uint64{loweredCap}) + require.NoError(t, err) + + require.GreaterOrEqual(t, blockGasWanted(t, chainApp), uint64(15_000_000), + "a tx at the lowered cap must still be accepted") + require.NotContains(t, res.TxResults[0].Log, "exceeds the maximum allowed", "got: %s", res.TxResults[0].Log) + }) + + t.Run("governance cannot brick voting with a zero cap", func(t *testing.T) { + chainApp, _, _ := setupGaslessAnteApp(t) + + ctx := chainApp.BaseApp.NewUncachedContext(false, cmtproto.Header{}) + params, err := chainApp.UexecutorKeeper.GetParams(ctx) + require.NoError(t, err) + params.MaxGaslessTxGas = 0 + + _, err = uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper).UpdateParams(ctx, + &uexecutortypes.MsgUpdateParams{ + Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + Params: params, + }) + + stored, getErr := chainApp.UexecutorKeeper.GetParams(ctx) + require.NoError(t, getErr) + require.Equal(t, uexecutortypes.DefaultMaxGaslessTxGas, stored.MaxGaslessTxGas, + "a rejected update must leave the cap untouched") + require.Error(t, err, "a zero cap must be rejected") + require.Contains(t, err.Error(), "max_gasless_tx_gas") + }) +} + +// setGaslessCapByGovernance applies a params update through the module's +// MsgServer with the real gov authority, i.e. exactly what a passed proposal +// executes. +func setGaslessCapByGovernance(t *testing.T, chainApp *app.ChainApp, cap uint64) { + t.Helper() + + ctx := chainApp.BaseApp.NewUncachedContext(false, cmtproto.Header{}) + + params, err := chainApp.UexecutorKeeper.GetParams(ctx) + require.NoError(t, err) + params.MaxGaslessTxGas = cap + + _, err = uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper).UpdateParams(ctx, + &uexecutortypes.MsgUpdateParams{ + Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + Params: params, + }) + require.NoError(t, err) + + stored, err := chainApp.UexecutorKeeper.GetParams(ctx) + require.NoError(t, err) + require.Equal(t, cap, stored.MaxGaslessTxGas) +} diff --git a/test/integration/ante/vesting_blocked_test.go b/test/integration/ante/vesting_blocked_test.go new file mode 100644 index 000000000..92deba343 --- /dev/null +++ b/test/integration/ante/vesting_blocked_test.go @@ -0,0 +1,200 @@ +package ante_test + +import ( + "testing" + "time" + + abci "github.com/cometbft/cometbft/abci/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/stretchr/testify/require" + + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/testutil/mock" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/pushchain/push-chain-node/app" +) + +// testChainID must be a chain ID app.EVMAppOptions knows about, otherwise +// NewChainApp panics while configuring the EVM coin info. +var testChainID = app.ChainID + +// setupVestingAnteApp boots a chain app with a single validator and one funded +// genesis account whose private key we keep, so we can sign real txs and push +// them through the full baseapp -> ante pipeline. +func setupVestingAnteApp(t *testing.T) (*app.ChainApp, cryptotypes.PrivKey, sdk.AccAddress) { + t.Helper() + + privVal := mock.NewPV() + valPubKey, err := privVal.GetPubKey() + require.NoError(t, err) + + valSet := cmttypes.NewValidatorSet([]*cmttypes.Validator{cmttypes.NewValidator(valPubKey, 1)}) + + senderPrivKey := secp256k1.GenPrivKey() + senderAcc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), 0, 0) + senderAddr := senderAcc.GetAddress() + + balance := banktypes.Balance{ + Address: senderAddr.String(), + Coins: sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100_000_000_000_000)), + // Enough of the EVM denom to actually pay the fee, so that the tx is + // only ever rejected because of the msg type and not because it is + // underfunded. + sdk.NewCoin(app.BaseDenom, sdkmath.NewInt(1).MulRaw(1e18).MulRaw(100)), + ), + } + + chainApp := app.SetupWithGenesisValSet( + t, valSet, []authtypes.GenesisAccount{senderAcc}, testChainID, nil, balance, + ) + + return chainApp, senderPrivKey, senderAddr +} + +// disableInflation zeroes out the mint module so that the only thing that can +// change total supply across the test block is the tx under test, not block +// inflation. Written through an uncached context so it survives into +// FinalizeBlock. +func disableInflation(t *testing.T, chainApp *app.ChainApp) { + t.Helper() + + ctx := chainApp.BaseApp.NewUncachedContext(false, cmtproto.Header{}) + + params, err := chainApp.MintKeeper.Params.Get(ctx) + require.NoError(t, err) + params.InflationMin = sdkmath.LegacyZeroDec() + params.InflationMax = sdkmath.LegacyZeroDec() + params.InflationRateChange = sdkmath.LegacyZeroDec() + require.NoError(t, chainApp.MintKeeper.Params.Set(ctx, params)) + + minter, err := chainApp.MintKeeper.Minter.Get(ctx) + require.NoError(t, err) + minter.Inflation = sdkmath.LegacyZeroDec() + minter.AnnualProvisions = sdkmath.LegacyZeroDec() + require.NoError(t, chainApp.MintKeeper.Minter.Set(ctx, minter)) +} + +func totalSupply(t *testing.T, chainApp *app.ChainApp, ctx sdk.Context) sdk.Coins { + t.Helper() + + supply := sdk.NewCoins() + chainApp.BankKeeper.IterateTotalSupply(ctx, func(coin sdk.Coin) bool { + supply = supply.Add(coin) + return false + }) + + return supply +} + +// TestVestingAccountCreationBlockedEndToEnd is the chain-level regression test +// for F-2026-18201. +// +// The staking-precompile underflow attack needs a vesting account: the EVM state +// view tracks only SPENDABLE balance, while Cosmos lets a vesting account +// DELEGATE locked coins. Delegating more than the spendable balance makes the +// StateDB subtract more than it holds; x/vm/keeper/statedb.go then reconciles +// that bogus view back into bank by MINTING the difference (or by BURNING a +// victim's real coins on the wrap-transfer variant). +// +// Vesting-account creation used to be permissionless: NewAuthzLimiterDecorator +// blocked MsgCreateVestingAccount only INSIDE an authz.MsgExec, so a plain +// top-level tx went straight through - and MsgCreatePermanentLockedAccount / +// MsgCreatePeriodicVestingAccount were not blocked anywhere at all. This test +// submits each of the three as a real signed tx and asserts that it is rejected, +// that no vesting account is created, and that neither the sender's balance nor +// total native supply moves. +func TestVestingAccountCreationBlockedEndToEnd(t *testing.T) { + // The vesting amount is denominated in the EVM/staking denom, which is what + // makes the account a usable attack primitive in the first place. + amount := sdk.NewCoins(sdk.NewCoin(app.BaseDenom, sdkmath.NewInt(1).MulRaw(1e18))) + future := time.Now().Add(365 * 24 * time.Hour).Unix() + + // Comfortably above the dynamic min gas price so the tx is not rejected by + // the fee decorators instead of the blocked-msgs decorator. + fees := sdk.NewCoins(sdk.NewCoin(app.BaseDenom, + sdkmath.NewInt(1e10).MulRaw(int64(simtestutil.DefaultGenTxGas)))) + + testCases := []struct { + name string + msg func(from, to sdk.AccAddress) sdk.Msg + }{ + { + "MsgCreateVestingAccount", + func(from, to sdk.AccAddress) sdk.Msg { + return sdkvesting.NewMsgCreateVestingAccount(from, to, amount, future, false) + }, + }, + { + "MsgCreatePermanentLockedAccount", + func(from, to sdk.AccAddress) sdk.Msg { + return sdkvesting.NewMsgCreatePermanentLockedAccount(from, to, amount) + }, + }, + { + "MsgCreatePeriodicVestingAccount", + func(from, to sdk.AccAddress) sdk.Msg { + return sdkvesting.NewMsgCreatePeriodicVestingAccount(from, to, time.Now().Unix(), + []sdkvesting.Period{{Length: 3600, Amount: amount}}) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + chainApp, senderPriv, senderAddr := setupVestingAnteApp(t) + disableInflation(t, chainApp) + victimAddr := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address().Bytes()) + + ctx := chainApp.BaseApp.NewContext(true) + senderAccount := chainApp.AccountKeeper.GetAccount(ctx, senderAddr) + require.NotNil(t, senderAccount) + + supplyBefore := totalSupply(t, chainApp, ctx) + senderBalanceBefore := chainApp.BankKeeper.GetAllBalances(ctx, senderAddr) + victimBalanceBefore := chainApp.BankKeeper.GetAllBalances(ctx, victimAddr) + + res, err := app.SignAndDeliverWithoutCommit( + t, + chainApp.TxConfig(), + chainApp.BaseApp, + []sdk.Msg{tc.msg(senderAddr, victimAddr)}, + fees, + testChainID, + []uint64{senderAccount.GetAccountNumber()}, + []uint64{senderAccount.GetSequence()}, + time.Now(), + senderPriv, + ) + require.NoError(t, err, "block must still be produced") + require.Len(t, res.TxResults, 1) + + txRes := res.TxResults[0] + require.NotEqual(t, abci.CodeTypeOK, txRes.Code, + "vesting account creation must be rejected in ante, got success: %s", txRes.Log) + require.Contains(t, txRes.Log, "found blocked msg type", + "tx must be rejected by the blocked-msgs decorator, got: %s", txRes.Log) + + // The tx failed in ante, so nothing it would have done may be visible. + ctxAfter := chainApp.BaseApp.NewContext(true) + + require.Nil(t, chainApp.AccountKeeper.GetAccount(ctxAfter, victimAddr), + "no vesting account may be created") + require.Equal(t, victimBalanceBefore, chainApp.BankKeeper.GetAllBalances(ctxAfter, victimAddr), + "victim spendable balance must be unchanged") + require.Equal(t, senderBalanceBefore, chainApp.BankKeeper.GetAllBalances(ctxAfter, senderAddr), + "sender spendable balance must be unchanged") + require.Equal(t, supplyBefore, totalSupply(t, chainApp, ctxAfter), + "total native supply must be unchanged across the tx") + }) + } +} diff --git a/test/integration/uexecutor/chain_enabled_test.go b/test/integration/uexecutor/chain_enabled_test.go index ee65cecff..84d5e5df4 100644 --- a/test/integration/uexecutor/chain_enabled_test.go +++ b/test/integration/uexecutor/chain_enabled_test.go @@ -220,7 +220,7 @@ func TestExecutePayload_ChainEnabled(t *testing.T) { ms := uexecutorkeeper.NewMsgServerImpl(testApp.UexecutorKeeper) _, err := ms.ExecutePayload(ctx, &uexecutortypes.MsgExecutePayload{ - Signer: "cosmos1xpurwdecvsenyvpkxvmnge3cv93nyd34xuersef38pjnxen9xfsk2dnz8yek2drrv56qmn2ak9", + Signer: testSigner, UniversalAccountId: &uexecutortypes.UniversalAccountId{ ChainNamespace: "eip155", ChainId: "11155111", diff --git a/test/integration/uexecutor/evm_hooks_and_outbound_test.go b/test/integration/uexecutor/evm_hooks_and_outbound_test.go index 1e232c4e4..cf8a8f187 100644 --- a/test/integration/uexecutor/evm_hooks_and_outbound_test.go +++ b/test/integration/uexecutor/evm_hooks_and_outbound_test.go @@ -163,7 +163,8 @@ func TestUpdateParams(t *testing.T) { err := app.UexecutorKeeper.Params.Set(ctx, initialParams) require.NoError(t, err) - updatedParams := uexecutortypes.Params{SomeValue: !initialParams.SomeValue} + updatedParams := initialParams + updatedParams.SomeValue = !initialParams.SomeValue err = app.UexecutorKeeper.UpdateParams(ctx, updatedParams) require.NoError(t, err) diff --git a/test/integration/uexecutor/execute_inbound_gas_test.go b/test/integration/uexecutor/execute_inbound_gas_test.go index b01de0073..f946f1cd0 100644 --- a/test/integration/uexecutor/execute_inbound_gas_test.go +++ b/test/integration/uexecutor/execute_inbound_gas_test.go @@ -292,12 +292,16 @@ func TestInboundGas(t *testing.T) { "revert outbound amount must match inbound amount") require.Equal(t, inbound.AssetAddr, ob.ExternalAssetAddr, "revert outbound asset must match inbound asset") - require.Equal(t, uexecutortypes.Status_PENDING, ob.OutboundStatus, - "revert outbound should start in PENDING status") - - // Gas fields are populated from UniversalCore if chain meta is set. - // In test env without VoteChainMeta, they may be zero/empty — that's OK, - // the outbound is still created (graceful degradation). + // The UniversalCore stub deployed by the integration harness cannot + // serve getOutboundTxGasAndFees, so the revert's gas metadata is + // unresolvable here and the outbound is recorded ABORTED rather than + // queued for a signature it could never receive. The resolvable + // (PENDING) path is covered by + // x/uexecutor/keeper/build_revert_outbound_test.go. + require.Equal(t, uexecutortypes.Status_ABORTED, ob.OutboundStatus, + "a revert with unresolvable gas metadata must be ABORTED, not PENDING") + require.NotEmpty(t, ob.AbortReason, "ABORTED revert must carry a reason") + requireNotQueuedForSigning(t, chainApp, ctx, ob.Id) // When chain meta IS set, these will be populated. break } @@ -464,7 +468,10 @@ func TestInboundGas(t *testing.T) { if ob.TxType == uexecutortypes.TxType_INBOUND_REVERT { foundRevert = true require.Equal(t, inbound.SourceChain, ob.DestinationChain) - require.Equal(t, uexecutortypes.Status_PENDING, ob.OutboundStatus) + // Gas metadata is unresolvable against the harness's UniversalCore stub, + // so the revert is recorded ABORTED instead of entering the signing queue. + require.Equal(t, uexecutortypes.Status_ABORTED, ob.OutboundStatus) + requireNotQueuedForSigning(t, chainApp, ctx, ob.Id) break } } diff --git a/test/integration/uexecutor/execute_payload_test.go b/test/integration/uexecutor/execute_payload_test.go index 3a1cf313c..88e61514f 100644 --- a/test/integration/uexecutor/execute_payload_test.go +++ b/test/integration/uexecutor/execute_payload_test.go @@ -15,6 +15,12 @@ import ( "github.com/stretchr/testify/require" ) +// testSigner is the bech32 form of the 20-byte account that these fixtures have +// always resolved to on the EVM side. It replaces an older literal that decoded +// to 42 bytes - GetAddressPair used to truncate it down to exactly these bytes, +// and now rejects it outright. +const testSigner = "cosmos18pjnzwr9xdnx2vnpv5mxywfnv56xxef5cludl5" + func TestExecutePayload(t *testing.T) { app, ctx, _ := utils.SetAppWithValidators(t) @@ -100,7 +106,7 @@ func TestExecutePayload(t *testing.T) { require.NoError(t, err) msg := &uexecutortypes.MsgExecutePayload{ - Signer: "cosmos1xpurwdecvsenyvpkxvmnge3cv93nyd34xuersef38pjnxen9xfsk2dnz8yek2drrv56qmn2ak9", + Signer: testSigner, UniversalAccountId: validUA, UniversalPayload: validUP, VerificationData: "0x91987784d56359fa91c3e3e0332f4f0cffedf9c081eb12874a63b41d5b5e5c660dc827947c2ae26e658d0551ad4b2d2aa073d62691429a0ae239d2cc58055bf11c", @@ -130,7 +136,7 @@ func TestExecutePayload(t *testing.T) { } msg := &uexecutortypes.MsgExecutePayload{ - Signer: "cosmos1xpurwdecvsenyvpkxvmnge3cv93nyd34xuersef38pjnxen9xfsk2dnz8yek2drrv56qmn2ak9", + Signer: testSigner, UniversalAccountId: validUA, UniversalPayload: validUP, } @@ -160,7 +166,7 @@ func TestExecutePayload(t *testing.T) { } msg := &uexecutortypes.MsgExecutePayload{ - Signer: "cosmos1xpurwdecvsenyvpkxvmnge3cv93nyd34xuersef38pjnxen9xfsk2dnz8yek2drrv56qmn2ak9", + Signer: testSigner, UniversalAccountId: validUA, UniversalPayload: validUP, VerificationData: "0xZZZZ", @@ -261,7 +267,7 @@ func TestExecutePayload_AutoDeployOnPreFundedAddress(t *testing.T) { // Submit MsgExecutePayload directly — no standalone DeployUEAV2 call beforehand. msg := &uexecutortypes.MsgExecutePayload{ - Signer: "cosmos1xpurwdecvsenyvpkxvmnge3cv93nyd34xuersef38pjnxen9xfsk2dnz8yek2drrv56qmn2ak9", + Signer: testSigner, UniversalAccountId: validUA, UniversalPayload: validUP, VerificationData: "0x91987784d56359fa91c3e3e0332f4f0cffedf9c081eb12874a63b41d5b5e5c660dc827947c2ae26e658d0551ad4b2d2aa073d62691429a0ae239d2cc58055bf11c", @@ -333,7 +339,7 @@ func TestExecutePayload_RejectWhenUndeployedAndUnfunded(t *testing.T) { } msg := &uexecutortypes.MsgExecutePayload{ - Signer: "cosmos1xpurwdecvsenyvpkxvmnge3cv93nyd34xuersef38pjnxen9xfsk2dnz8yek2drrv56qmn2ak9", + Signer: testSigner, UniversalAccountId: validUA, UniversalPayload: validUP, VerificationData: "0x1234", diff --git a/test/integration/uexecutor/execute_stuck_inbound_test.go b/test/integration/uexecutor/execute_stuck_inbound_test.go new file mode 100644 index 000000000..e8c1f0f51 --- /dev/null +++ b/test/integration/uexecutor/execute_stuck_inbound_test.go @@ -0,0 +1,484 @@ +package integrationtest + +import ( + "fmt" + "math/big" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app" + utils "github.com/pushchain/push-chain-node/test/utils" + uexecutorkeeper "github.com/pushchain/push-chain-node/x/uexecutor/keeper" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" + uvalidatorkeeper "github.com/pushchain/push-chain-node/x/uvalidator/keeper" + uvalidatortypes "github.com/pushchain/push-chain-node/x/uvalidator/types" +) + +// TestExecuteStuckInbound_PendingUnreachable_ThresholdMet_Executes is the +// headline F-2026-18147 case for the execute hatch. +// +// RecomputeBallotQuorum preserves the votes of still-eligible voters and lowers +// the threshold, but returns PENDING without tallying what it just rebuilt. The +// shape reproduced here is the result: every remaining eligible voter has voted +// YES and the YES count already clears the recomputed threshold, so the ballot +// should have passed but AddVote rejects repeat votes and nothing can move it. +// +// The whole live validator set attested this deposit, so the correct resolution +// is to deliver the funds on Push - not to refund on the source chain, which is +// all RevertStuckInbound could do. +func TestExecuteStuckInbound_PendingUnreachable_ThresholdMet_Executes(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + }, + 2, // YES (3) already clears the recomputed threshold + ) + + recipient := common.HexToAddress(inbound.Recipient) + require.Equal(t, 0, prc20BalanceOf(t, chainApp, ctx, recipient).Sign(), + "recipient must start with no bridged balance") + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + resp, err := ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.NoError(t, err, "an unreachable PENDING ballot at threshold must be executable") + require.Equal(t, uexecutortypes.GetInboundUniversalTxKey(*inbound), resp.UtxId) + + // --- UTX assertions --- + utx, _, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, resp.UtxId) + require.NoError(t, err) + require.NotNil(t, utx.InboundTx) + require.Equal(t, inbound.TxHash, utx.InboundTx.TxHash) + + require.Len(t, utx.PcTx, 1) + require.Equal(t, "SUCCESS", utx.PcTx[0].Status, + "the execute hatch must run the deposit, not record a failure") + + // The point of this hatch: execution, not refund. + require.Empty(t, utx.OutboundTx, "an executed inbound must not create a revert outbound") + + // --- The user actually got the funds --- + amount, ok := new(big.Int).SetString(inbound.Amount, 10) + require.True(t, ok) + require.Equal(t, 0, prc20BalanceOf(t, chainApp, ctx, recipient).Cmp(amount), + "recipient balance must equal the inbound amount") + + // --- Ballot is terminal, so the hatch cannot be re-entered --- + ballotKey, err := uexecutortypes.GetInboundBallotKey(*inbound) + require.NoError(t, err) + ballot, err := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballotKey) + require.NoError(t, err) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PASSED, ballot.Status) + + // The pending audit-trail entry must be gone: the terminal hook clears it and + // the pipeline's RemovePendingInbound is a no-op on the absent key. + isPending, err := chainApp.UexecutorKeeper.IsPendingInbound(ctx, *inbound) + require.NoError(t, err) + require.False(t, isPending) +} + +// TestExecuteStuckInbound_DuplicateExecute_Rejected verifies a second call +// cannot mint twice. +// +// Two independent barriers stand in the way and the outer one wins here: the +// first call drove the ballot to PASSED, so IsUnreachablePending is already +// false. The UTX barrier underneath it is exercised by +// TestExecuteStuckInbound_AlreadyRevertedInbound_Refused, where the ballot stays +// PENDING-unreachable and only the UTX blocks the second call. +func TestExecuteStuckInbound_DuplicateExecute_Rejected(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + }, + 2, + ) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.NoError(t, err) + + amount, ok := new(big.Int).SetString(inbound.Amount, 10) + require.True(t, ok) + recipient := common.HexToAddress(inbound.Recipient) + + _, err = ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.Error(t, err, "a second execute must be refused") + require.Contains(t, err.Error(), "admin execute requires PENDING", + "the ballot is already PASSED, so the status gate refuses before the UTX gate is reached") + + require.Equal(t, 0, prc20BalanceOf(t, chainApp, ctx, recipient).Cmp(amount), + "the refused second execute must not have minted again") +} + +// TestExecuteStuckInbound_AlreadyRevertedInbound_Refused pins the same barrier +// against the sibling hatch: once RevertStuckInbound has created its UTX, the +// inbound cannot also be executed. Otherwise an operator could refund the user +// on the source chain and then mint them the same funds on Push. +func TestExecuteStuckInbound_AlreadyRevertedInbound_Refused(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + }, + 2, + ) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.RevertStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgRevertStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.NoError(t, err) + + _, err = ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.Error(t, err, "an inbound already reverted must not also be executed") + require.Contains(t, err.Error(), "already exists") + + require.Equal(t, 0, prc20BalanceOf(t, chainApp, ctx, common.HexToAddress(inbound.Recipient)).Sign(), + "a refused execute must not mint") +} + +// TestExecuteStuckInbound_ExpiredBallot_Refused keeps the two hatches apart. An +// EXPIRED ballot never reached quorum, so there is no attestation to act on and +// the refund is the honest resolution — that is RevertStuckInbound's job. +func TestExecuteStuckInbound_ExpiredBallot_Refused(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + }, + 2, + ) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.Error(t, err, "EXPIRED belongs to the revert hatch, not the execute hatch") + require.Contains(t, err.Error(), "admin execute requires PENDING") + require.Contains(t, err.Error(), "MsgRevertStuckInbound", + "the refusal must point the operator at the hatch that does apply") + + assertNoUtxOrMint(t, chainApp, ctx, inbound) +} + +// TestExecuteStuckInbound_PassedBallot_Refused guards the case where the ballot +// already finalized normally: IsUnreachablePending is false, so re-running the +// pipeline is refused even before the UTX check. +func TestExecuteStuckInbound_PassedBallot_Refused(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PASSED, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + }, + 2, + ) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "admin execute requires PENDING") + + assertNoUtxOrMint(t, chainApp, ctx, inbound) +} + +// TestExecuteStuckInbound_PendingWithUnvotedVoter_Refused is the guard against +// widening the hatch too far. +// +// The YES votes already clear the threshold, so this looks exactly like the +// headline case. It is not: one eligible voter still holds a NOT_YET_VOTED slot, +// so a single normal vote finalizes it through the real pipeline. Admin execute +// must not race that — the vote flow decides, not the admin. +func TestExecuteStuckInbound_PendingWithUnvotedVoter_Refused(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_NOT_YET_VOTED, + }, + 2, // YES (2) already meets threshold — still refused, it can finalize normally + ) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.Error(t, err, "a PENDING ballot with an unvoted eligible voter can still finalize normally") + require.Contains(t, err.Error(), "admin execute requires PENDING") + + // The ballot must be left untouched so the remaining voter can still finalize it. + ballotKey, err := uexecutortypes.GetInboundBallotKey(*inbound) + require.NoError(t, err) + ballot, err := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballotKey) + require.NoError(t, err) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, ballot.Status) + + assertNoUtxOrMint(t, chainApp, ctx, inbound) +} + +// TestExecuteStuckInbound_BelowThreshold_Refused covers the second stuck shape: +// every eligible voter has voted, so the ballot is unreachable, but the YES count +// never reached the threshold. There is no supermajority attestation to act on, +// so executing would deliver funds the validator set did not carry. That case +// belongs to the revert hatch, which accepts it. +// +// Unreachable today implies yes == len(EligibleVoters) because both inbound vote +// sites hardcode VOTE_RESULT_SUCCESS. This test seeds the FAILURE vote directly +// so the threshold check is pinned independently of that invariant. +func TestExecuteStuckInbound_BelowThreshold_Refused(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_FAILURE, + }, + 3, // YES (2) < 3 → unreachable, but never carried + ) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.Error(t, err, "a ballot that never met its threshold must not be executed") + require.Contains(t, err.Error(), "against a voting threshold of 3") + require.Contains(t, err.Error(), "MsgRevertStuckInbound") + + assertNoUtxOrMint(t, chainApp, ctx, inbound) +} + +func TestExecuteStuckInbound_AdminAuth_RejectsNonAdmin(t *testing.T) { + chainApp, ctx, inbound, _ := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + }, + 2, + ) + + const notAdmin = "push1negskcfqu09j5zvpk7nhvacnwyy2mafffy7r6a" + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: notAdmin, + Inbound: inbound, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid admin") + + assertNoUtxOrMint(t, chainApp, ctx, inbound) +} + +func TestExecuteStuckInbound_BallotNotFound(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + // no ballot seeded + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "ballot for inbound not found") +} + +func TestExecuteStuckInbound_NilInbound_Rejected(t *testing.T) { + chainApp, ctx, _, admin := setupRevertStuckInbound(t) + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: admin, + Inbound: nil, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "inbound is required") +} + +// TestExecuteStuckInbound_TamperedInbound_Refused pins the security property the +// ballot-key derivation buys: the admin can only execute the exact payload the +// validators voted on. A single changed field derives a different ballot key, +// which has no ballot at all. +func TestExecuteStuckInbound_TamperedInbound_Refused(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + }, + 2, + ) + + tampered := *inbound + tampered.Amount = "999999999" + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: admin, + Inbound: &tampered, + }) + require.Error(t, err, "a payload the validators never voted on has no ballot") + require.Contains(t, err.Error(), "ballot for inbound not found") +} + +// TestExecuteStuckInbound_RecomputeThenExecute_E2E walks the whole F-2026-18147 +// story with real universal validators and real votes: +// +// 4 UVs, threshold 3 → 2 vote YES, ballot stays PENDING → the other 2 leave the +// set → MsgRecomputeBallotQuorum rebuilds it to the 2 remaining voters with +// threshold 2 and preserves their YES votes, but returns PENDING without +// tallying → nothing can ever move the ballot → MsgExecuteStuckInbound +// finalizes it PASSED and delivers the funds. +func TestExecuteStuckInbound_RecomputeThenExecute_E2E(t *testing.T) { + chainApp, ctx, universalVals, inbound, coreVals := setupInboundBridgeTest(t, 4) + + const admin = "push1fgaewhyd9fkwtqaj9c233letwcuey6dgly9gv9" + require.NoError(t, chainApp.UvalidatorKeeper.Params.Set(ctx, uvalidatortypes.Params{Admin: admin})) + + // Two of four vote YES. Threshold is (2*4)/3+1 = 3, so the ballot stays PENDING. + for i := 0; i < 2; i++ { + valAddr, err := sdk.ValAddressFromBech32(coreVals[i].OperatorAddress) + require.NoError(t, err) + require.NoError(t, utils.ExecVoteInbound(t, ctx, chainApp, universalVals[i], + sdk.AccAddress(valAddr).String(), inbound)) + } + + // Derive the ballot key the way the keeper does — off the canonical payload. + canonical := *inbound + canonical.Canonicalize() + ballotKey, err := uexecutortypes.GetInboundBallotKey(canonical) + require.NoError(t, err) + + ballot, err := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballotKey) + require.NoError(t, err) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, ballot.Status) + require.Equal(t, int64(3), ballot.VotingThreshold) + + // The two silent validators leave the universal-validator set. + for i := 2; i < 4; i++ { + v := coreVals[i] + v.Status = stakingtypes.Unbonded + require.NoError(t, chainApp.StakingKeeper.SetValidator(ctx, v)) + } + + // Recompute: 2 eligible, threshold 2, both preserved votes YES. This is the + // bug — the recomputed ballot already satisfies its own threshold, yet it is + // returned PENDING and no vote is left to cast. + uvMs := uvalidatorkeeper.NewMsgServerImpl(chainApp.UvalidatorKeeper) + recomputeResp, err := uvMs.RecomputeBallotQuorum(sdk.WrapSDKContext(ctx), &uvalidatortypes.MsgRecomputeBallotQuorum{ + Signer: admin, + BallotId: ballotKey, + }) + require.NoError(t, err) + require.Equal(t, int64(2), recomputeResp.NewEligibleCount) + require.Equal(t, int64(2), recomputeResp.NewVotingThreshold) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, recomputeResp.NewStatus) + + stuck, err := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballotKey) + require.NoError(t, err) + require.True(t, stuck.IsUnreachablePending(), "no eligible voter is left to cast a vote") + yes, _ := stuck.CountVotes() + require.Equal(t, 2, yes, "recompute preserved both YES votes") + + // A third vote cannot rescue it: the remaining voters have already voted, and + // the departed ones are no longer eligible. + valAddr, err := sdk.ValAddressFromBech32(coreVals[2].OperatorAddress) + require.NoError(t, err) + require.Error(t, + utils.ExecVoteInbound(t, ctx, chainApp, universalVals[2], sdk.AccAddress(valAddr).String(), inbound), + "the ballot is genuinely stuck, not merely waiting") + + recipient := common.HexToAddress(inbound.Recipient) + require.Equal(t, 0, prc20BalanceOf(t, chainApp, ctx, recipient).Sign()) + + // The escape hatch. + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + resp, err := ms.ExecuteStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.NoError(t, err) + require.Equal(t, uexecutortypes.GetInboundUniversalTxKey(canonical), resp.UtxId) + + utx, _, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, resp.UtxId) + require.NoError(t, err) + require.Len(t, utx.PcTx, 1) + require.Equal(t, "SUCCESS", utx.PcTx[0].Status) + require.Empty(t, utx.OutboundTx, "the user is paid on Push, not refunded on the source chain") + + amount, ok := new(big.Int).SetString(inbound.Amount, 10) + require.True(t, ok) + require.Equal(t, 0, prc20BalanceOf(t, chainApp, ctx, recipient).Cmp(amount)) + + final, err := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballotKey) + require.NoError(t, err) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PASSED, final.Status) + + isPending, err := chainApp.UexecutorKeeper.IsPendingInbound(ctx, canonical) + require.NoError(t, err) + require.False(t, isPending, "the pending audit-trail entry must be cleared") +} + +// assertNoUtxOrMint checks a refusal was total: no UniversalTx was written and +// nothing was minted to the recipient. +func assertNoUtxOrMint(t *testing.T, chainApp *app.ChainApp, ctx sdk.Context, inbound *uexecutortypes.Inbound) { + t.Helper() + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + has, err := chainApp.UexecutorKeeper.HasUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.False(t, has, fmt.Sprintf("a refused execute must not leave a UniversalTx behind (%s)", utxKey)) + + require.Equal(t, 0, prc20BalanceOf(t, chainApp, ctx, common.HexToAddress(inbound.Recipient)).Sign(), + "a refused execute must not mint") +} diff --git a/test/integration/uexecutor/execute_stuck_outbound_test.go b/test/integration/uexecutor/execute_stuck_outbound_test.go new file mode 100644 index 000000000..936673c3c --- /dev/null +++ b/test/integration/uexecutor/execute_stuck_outbound_test.go @@ -0,0 +1,466 @@ +package integrationtest + +import ( + "math/big" + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app" + utils "github.com/pushchain/push-chain-node/test/utils" + chainutils "github.com/pushchain/push-chain-node/utils" + uexecutorkeeper "github.com/pushchain/push-chain-node/x/uexecutor/keeper" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" + uvalidatortypes "github.com/pushchain/push-chain-node/x/uvalidator/types" +) + +const stuckOutboundAdmin = "push1fgaewhyd9fkwtqaj9c233letwcuey6dgly9gv9" + +// observedTxHash is deliberately mixed-case: the vote path lowercases an EVM tx +// hash before it hashes the observation into a ballot key, so a hatch that +// canonicalized later (or not at all) would derive a key no ballot sits under. +const observedTxHash = "0xAABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899" + +// setupStuckOutbound builds a chain app carrying one PENDING outbound (created +// by a real inbound-initiated withdraw) and sets the uvalidator admin. +func setupStuckOutbound(t *testing.T) ( + chainApp *app.ChainApp, + ctx sdk.Context, + utxId string, + outbound *uexecutortypes.OutboundTx, + admin string, +) { + t.Helper() + chainApp, ctx, _, utxId, outbound, _ = setupOutboundVotingTest(t, 4) + + admin = stuckOutboundAdmin + require.NoError(t, chainApp.UvalidatorKeeper.Params.Set(ctx, uvalidatortypes.Params{Admin: admin})) + + require.Equal(t, uexecutortypes.Status_PENDING, outbound.OutboundStatus) + return chainApp, ctx, utxId, outbound, admin +} + +// stuckObservation is the destination-chain observation the validators voted on. +func stuckObservation(success bool, errorMsg, gasFeeUsed string) uexecutortypes.OutboundObservation { + return uexecutortypes.OutboundObservation{ + Success: success, + ErrorMsg: errorMsg, + TxHash: observedTxHash, + BlockHeight: 42, + GasFeeUsed: gasFeeUsed, + } +} + +// outboundBallotKeyFor derives the ballot key the keeper will derive, i.e. over +// the canonicalized observation. +func outboundBallotKeyFor(t *testing.T, utxId string, outbound *uexecutortypes.OutboundTx, obs uexecutortypes.OutboundObservation) string { + t.Helper() + obs.TxHash = chainutils.LenientCanonicalizeTxHash(outbound.DestinationChain, obs.TxHash) + obs.GasFeeUsed = strings.TrimSpace(obs.GasFeeUsed) + obs.ErrorMsg = strings.TrimSpace(obs.ErrorMsg) + key, err := uexecutortypes.GetOutboundBallotKey(utxId, outbound.Id, obs) + require.NoError(t, err) + return key +} + +// seedOutboundBallot stores an outbound ballot under exactly that key, with a +// real eligible-voter list and per-voter vote slots — the F-2026-18147 shapes +// all turn on whether any eligible voter still holds a NOT_YET_VOTED slot. +func seedOutboundBallot( + t *testing.T, + chainApp *app.ChainApp, + ctx sdk.Context, + utxId string, + outbound *uexecutortypes.OutboundTx, + obs uexecutortypes.OutboundObservation, + status uvalidatortypes.BallotStatus, + voters []string, + votes []uvalidatortypes.VoteResult, + threshold int64, +) string { + t.Helper() + require.Len(t, votes, len(voters), "each eligible voter needs exactly one vote slot") + + ballotKey := outboundBallotKeyFor(t, utxId, outbound, obs) + require.NoError(t, chainApp.UvalidatorKeeper.Ballots.Set(ctx, ballotKey, uvalidatortypes.Ballot{ + Id: ballotKey, + BallotType: uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_OUTBOUND_TX, + EligibleVoters: voters, + Votes: votes, + VotingThreshold: threshold, + Status: status, + BlockHeightCreated: 1, + BlockHeightExpiry: 100_000_000, + })) + return ballotKey +} + +func allVotedYes() []uvalidatortypes.VoteResult { + return []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + } +} + +// revertRecipientOf mirrors handleFailedOutbound's choice of who gets the +// bridged tokens back. +func revertRecipientOf(outbound *uexecutortypes.OutboundTx) common.Address { + if outbound.RevertInstructions != nil && outbound.RevertInstructions.FundRecipient != "" { + return common.HexToAddress(outbound.RevertInstructions.FundRecipient) + } + return common.HexToAddress(outbound.Sender) +} + +func executeStuckOutbound( + t *testing.T, + chainApp *app.ChainApp, + ctx sdk.Context, + signer, utxId, outboundId string, + obs uexecutortypes.OutboundObservation, +) (*uexecutortypes.MsgExecuteStuckOutboundResponse, error) { + t.Helper() + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + return ms.ExecuteStuckOutbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckOutbound{ + Signer: signer, + TxId: outboundId, + UtxId: utxId, + ObservedTx: &obs, + }) +} + +func loadOutbound(t *testing.T, chainApp *app.ChainApp, ctx sdk.Context, utxId, outboundId string) *uexecutortypes.OutboundTx { + t.Helper() + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxId) + require.NoError(t, err) + require.True(t, found) + for _, ob := range utx.OutboundTx { + if ob.Id == outboundId { + return ob + } + } + t.Fatalf("outbound %s not found in utx %s", outboundId, utxId) + return nil +} + +// TestExecuteStuckOutbound_ExpiredBallot_Success_Settles is the plain expiry +// case: quorum never formed, so the outbound sat PENDING forever even though the +// destination-chain tx landed. The hatch settles it against the observation. +func TestExecuteStuckOutbound_ExpiredBallot_Success_Settles(t *testing.T) { + chainApp, ctx, utxId, ob, admin := setupStuckOutbound(t) + + // gas_fee_used == GasFee → no excess, so nothing to refund. + obs := stuckObservation(true, "", ob.GasFee) + ballotKey := seedOutboundBallot(t, chainApp, ctx, utxId, ob, obs, + uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, threeVoters(), allVotedYes(), 3) + + recipient := revertRecipientOf(ob) + before := prc20BalanceOf(t, chainApp, ctx, recipient) + + resp, err := executeStuckOutbound(t, chainApp, ctx, admin, utxId, ob.Id, obs) + require.NoError(t, err, "an EXPIRED outbound ballot must be settleable") + require.Equal(t, ob.Id, resp.OutboundId) + + settled := loadOutbound(t, chainApp, ctx, utxId, ob.Id) + require.Equal(t, uexecutortypes.Status_OBSERVED, settled.OutboundStatus) + require.NotNil(t, settled.ObservedTx) + require.True(t, settled.ObservedTx.Success) + require.Equal(t, strings.ToLower(observedTxHash), settled.ObservedTx.TxHash, + "the stored observation must be the canonical one that was hashed into the ballot key") + + // A successful outbound mints nothing back and refunds no gas. + require.Nil(t, settled.PcRevertExecution, "a successful settlement must not re-mint") + require.Nil(t, settled.PcRefundExecution, "no excess gas, so no refund") + require.Equal(t, 0, prc20BalanceOf(t, chainApp, ctx, recipient).Cmp(before), + "a successful settlement must not move the recipient's balance") + + // Pending index cleared, so the outbound leaves the signing queue. + has, err := chainApp.UexecutorKeeper.PendingOutbounds.Has(ctx, ob.Id) + require.NoError(t, err) + require.False(t, has) + + // EXPIRED is terminal already; MarkBallotFinalized only accepts + // PASSED/REJECTED, so the record is deliberately left alone. + ballot, err := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballotKey) + require.NoError(t, err) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, ballot.Status) +} + +// TestExecuteStuckOutbound_ExpiredBallot_Failure_RevertsAndRefunds pins the +// other half of the same message: the outcome follows observed_tx.success, so a +// failed observation mints the bridged tokens back and refunds the excess gas — +// no separate revert message is needed. +func TestExecuteStuckOutbound_ExpiredBallot_Failure_RevertsAndRefunds(t *testing.T) { + chainApp, ctx, utxId, ob, admin := setupStuckOutbound(t) + + // gas_fee_used (50) < GasFee (111) → excess gas must be refunded too. + obs := stuckObservation(false, "execution reverted", "50") + seedOutboundBallot(t, chainApp, ctx, utxId, ob, obs, + uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, threeVoters(), allVotedYes(), 3) + + recipient := revertRecipientOf(ob) + before := prc20BalanceOf(t, chainApp, ctx, recipient) + + _, err := executeStuckOutbound(t, chainApp, ctx, admin, utxId, ob.Id, obs) + require.NoError(t, err) + + settled := loadOutbound(t, chainApp, ctx, utxId, ob.Id) + require.Equal(t, uexecutortypes.Status_REVERTED, settled.OutboundStatus) + + require.NotNil(t, settled.PcRevertExecution, "a failed outbound must mint the bridged funds back") + require.Equal(t, "SUCCESS", settled.PcRevertExecution.Status) + + amount, ok := new(big.Int).SetString(ob.Amount, 10) + require.True(t, ok) + require.Equal(t, 0, prc20BalanceOf(t, chainApp, ctx, recipient).Cmp(new(big.Int).Add(before, amount)), + "the revert recipient must be credited the full outbound amount") + + require.NotNil(t, settled.PcRefundExecution, "excess gas must be refunded on failure too") + require.NotEmpty(t, settled.PcRefundExecution.Status) +} + +// TestExecuteStuckOutbound_PendingUnreachable_ThresholdMet_Settles is the +// F-2026-18147 shape on the outbound side: every eligible voter has voted YES +// and the YES count already clears the stored threshold, but the ballot was +// returned PENDING and AddVote rejects repeat votes, so nothing can move it. +func TestExecuteStuckOutbound_PendingUnreachable_ThresholdMet_Settles(t *testing.T) { + chainApp, ctx, utxId, ob, admin := setupStuckOutbound(t) + + obs := stuckObservation(true, "", ob.GasFee) + ballotKey := seedOutboundBallot(t, chainApp, ctx, utxId, ob, obs, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, threeVoters(), allVotedYes(), + 2) // YES (3) already clears the recomputed threshold + + resp, err := executeStuckOutbound(t, chainApp, ctx, admin, utxId, ob.Id, obs) + require.NoError(t, err, "an unreachable PENDING ballot at threshold must be settleable") + require.Equal(t, ob.Id, resp.OutboundId) + + settled := loadOutbound(t, chainApp, ctx, utxId, ob.Id) + require.Equal(t, uexecutortypes.Status_OBSERVED, settled.OutboundStatus) + require.NotNil(t, settled.ObservedTx) + + // Unlike EXPIRED, this ballot was not terminal, so the hatch drives it there. + ballot, err := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballotKey) + require.NoError(t, err) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PASSED, ballot.Status) +} + +// TestExecuteStuckOutbound_PendingUnreachable_BelowThreshold_Refused covers the +// other stuck shape: nothing can move the ballot, but the validators never +// carried it. Settling would act on an observation the set did not attest. +func TestExecuteStuckOutbound_PendingUnreachable_BelowThreshold_Refused(t *testing.T) { + chainApp, ctx, utxId, ob, admin := setupStuckOutbound(t) + + obs := stuckObservation(true, "", ob.GasFee) + seedOutboundBallot(t, chainApp, ctx, utxId, ob, obs, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_FAILURE, + }, + 3) // YES (2) < 3 → unreachable, but never carried + + _, err := executeStuckOutbound(t, chainApp, ctx, admin, utxId, ob.Id, obs) + require.Error(t, err, "a ballot that never met its threshold must not be settled") + require.Contains(t, err.Error(), "against a voting threshold of 3") + + assertOutboundUntouched(t, chainApp, ctx, utxId, ob.Id) +} + +// TestExecuteStuckOutbound_PendingWithUnvotedVoter_Refused is the guard against +// widening the hatch: the YES votes already clear the threshold, but one +// eligible voter still holds a NOT_YET_VOTED slot, so a single normal vote +// finalizes it through the real pipeline. Admin settle must not race that. +func TestExecuteStuckOutbound_PendingWithUnvotedVoter_Refused(t *testing.T) { + chainApp, ctx, utxId, ob, admin := setupStuckOutbound(t) + + obs := stuckObservation(true, "", ob.GasFee) + ballotKey := seedOutboundBallot(t, chainApp, ctx, utxId, ob, obs, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_NOT_YET_VOTED, + }, + 2) + + _, err := executeStuckOutbound(t, chainApp, ctx, admin, utxId, ob.Id, obs) + require.Error(t, err, "a PENDING ballot with an unvoted eligible voter can still finalize normally") + require.Contains(t, err.Error(), "admin execute requires PENDING") + + // The ballot must be left votable so the remaining voter can finalize it. + ballot, err := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballotKey) + require.NoError(t, err) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, ballot.Status) + + assertOutboundUntouched(t, chainApp, ctx, utxId, ob.Id) +} + +// TestExecuteStuckOutbound_AlreadySettled_Refused is the idempotency barrier. An +// EXPIRED ballot is left untouched by design, so the ballot gate lets a second +// call through and only the outbound's own status stops it — without which the +// admin could re-mint the same funds repeatedly. +func TestExecuteStuckOutbound_AlreadySettled_Refused(t *testing.T) { + chainApp, ctx, utxId, ob, admin := setupStuckOutbound(t) + + obs := stuckObservation(false, "execution reverted", ob.GasFee) + seedOutboundBallot(t, chainApp, ctx, utxId, ob, obs, + uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, threeVoters(), allVotedYes(), 3) + + _, err := executeStuckOutbound(t, chainApp, ctx, admin, utxId, ob.Id, obs) + require.NoError(t, err) + + recipient := revertRecipientOf(ob) + afterFirst := prc20BalanceOf(t, chainApp, ctx, recipient) + + _, err = executeStuckOutbound(t, chainApp, ctx, admin, utxId, ob.Id, obs) + require.Error(t, err, "a second settle must be refused") + require.Contains(t, err.Error(), "already finalized") + + require.Equal(t, 0, prc20BalanceOf(t, chainApp, ctx, recipient).Cmp(afterFirst), + "the refused second settle must not have minted again") +} + +// TestExecuteStuckOutbound_VotedOutbound_Refused covers the normal-flow overlap: +// once the validators settled the outbound themselves, its ballot is PASSED and +// the hatch has nothing to do. +func TestExecuteStuckOutbound_VotedOutbound_Refused(t *testing.T) { + chainApp, ctx, vals, utxId, ob, coreVals := setupOutboundVotingTest(t, 4) + require.NoError(t, chainApp.UvalidatorKeeper.Params.Set(ctx, uvalidatortypes.Params{Admin: stuckOutboundAdmin})) + + for i := 0; i < 3; i++ { + valAddr, err := sdk.ValAddressFromBech32(coreVals[i].OperatorAddress) + require.NoError(t, err) + require.NoError(t, utils.ExecVoteOutbound( + t, ctx, chainApp, vals[i], sdk.AccAddress(valAddr).String(), utxId, ob, true, "", ob.GasFee)) + } + require.Equal(t, uexecutortypes.Status_OBSERVED, loadOutbound(t, chainApp, ctx, utxId, ob.Id).OutboundStatus) + + // The vote path's own observation, so the ballot key resolves to the PASSED ballot. + obs := uexecutortypes.OutboundObservation{ + Success: true, + TxHash: "0xobserved-" + ob.Id, + BlockHeight: 1, + GasFeeUsed: ob.GasFee, + } + _, err := executeStuckOutbound(t, chainApp, ctx, stuckOutboundAdmin, utxId, ob.Id, obs) + require.Error(t, err, "a ballot the validators finalized is not stuck") + require.Contains(t, err.Error(), "admin execute requires PENDING") +} + +// TestExecuteStuckOutbound_TamperedObservation_Refused pins the security +// property the ballot-key derivation buys: the admin can only settle against the +// exact observation the validators voted on. One changed field derives a +// different key, which has no ballot at all. +func TestExecuteStuckOutbound_TamperedObservation_Refused(t *testing.T) { + chainApp, ctx, utxId, ob, admin := setupStuckOutbound(t) + + obs := stuckObservation(true, "", ob.GasFee) + seedOutboundBallot(t, chainApp, ctx, utxId, ob, obs, + uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, threeVoters(), allVotedYes(), 3) + + tampered := obs + tampered.BlockHeight = obs.BlockHeight + 1 + + _, err := executeStuckOutbound(t, chainApp, ctx, admin, utxId, ob.Id, tampered) + require.Error(t, err, "an observation the validators never voted on has no ballot") + require.Contains(t, err.Error(), "ballot for outbound not found") + + assertOutboundUntouched(t, chainApp, ctx, utxId, ob.Id) +} + +func TestExecuteStuckOutbound_BallotNotFound(t *testing.T) { + chainApp, ctx, utxId, ob, admin := setupStuckOutbound(t) + // no ballot seeded + _, err := executeStuckOutbound(t, chainApp, ctx, admin, utxId, ob.Id, stuckObservation(true, "", ob.GasFee)) + require.Error(t, err) + require.Contains(t, err.Error(), "ballot for outbound not found") +} + +func TestExecuteStuckOutbound_AdminAuth_RejectsNonAdmin(t *testing.T) { + chainApp, ctx, utxId, ob, _ := setupStuckOutbound(t) + + obs := stuckObservation(true, "", ob.GasFee) + seedOutboundBallot(t, chainApp, ctx, utxId, ob, obs, + uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, threeVoters(), allVotedYes(), 3) + + const notAdmin = "push1negskcfqu09j5zvpk7nhvacnwyy2mafffy7r6a" + _, err := executeStuckOutbound(t, chainApp, ctx, notAdmin, utxId, ob.Id, obs) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid admin") + + assertOutboundUntouched(t, chainApp, ctx, utxId, ob.Id) +} + +func TestExecuteStuckOutbound_NilObservedTx_Rejected(t *testing.T) { + chainApp, ctx, utxId, ob, admin := setupStuckOutbound(t) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.ExecuteStuckOutbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgExecuteStuckOutbound{ + Signer: admin, + TxId: ob.Id, + UtxId: utxId, + ObservedTx: nil, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "observed_tx is required") +} + +func TestExecuteStuckOutbound_UnknownOutbound_Rejected(t *testing.T) { + chainApp, ctx, utxId, ob, admin := setupStuckOutbound(t) + + const unknown = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + _, err := executeStuckOutbound(t, chainApp, ctx, admin, utxId, unknown, stuckObservation(true, "", ob.GasFee)) + require.Error(t, err) + require.Contains(t, err.Error(), "not found") + + _, err = executeStuckOutbound(t, chainApp, ctx, admin, unknown, ob.Id, stuckObservation(true, "", ob.GasFee)) + require.Error(t, err) + require.Contains(t, err.Error(), "UniversalTx not found") +} + +// TestExecuteStuckOutbound_PrefixedIds_Settles mirrors the vote path: UVs (and +// the operators reading their logs) carry 0x-prefixed IDs, and the handler has +// to strip them exactly once before the keeper lookup. +func TestExecuteStuckOutbound_PrefixedIds_Settles(t *testing.T) { + chainApp, ctx, utxId, ob, admin := setupStuckOutbound(t) + + obs := stuckObservation(true, "", ob.GasFee) + seedOutboundBallot(t, chainApp, ctx, utxId, ob, obs, + uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, threeVoters(), allVotedYes(), 3) + + resp, err := executeStuckOutbound(t, chainApp, ctx, admin, "0x"+utxId, "0x"+ob.Id, obs) + require.NoError(t, err) + require.Equal(t, ob.Id, resp.OutboundId) + + require.Equal(t, uexecutortypes.Status_OBSERVED, loadOutbound(t, chainApp, ctx, utxId, ob.Id).OutboundStatus) +} + +// The keeper re-runs that validation, so the malformed value is refused even on +// a direct keeper call that never passed through ValidateBasic. +func TestExecuteStuckOutbound_MalformedGasFeeUsed_RefusedByKeeper(t *testing.T) { + chainApp, ctx, utxId, ob, admin := setupStuckOutbound(t) + + _, err := executeStuckOutbound(t, chainApp, ctx, admin, utxId, ob.Id, stuckObservation(true, "", "not-a-number")) + require.Error(t, err) + require.Contains(t, err.Error(), "observed_tx.gas_fee_used must be a valid uint256") + + assertOutboundUntouched(t, chainApp, ctx, utxId, ob.Id) +} + +// assertOutboundUntouched checks a refusal was total: the outbound is still +// PENDING and still queued for signing. +func assertOutboundUntouched(t *testing.T, chainApp *app.ChainApp, ctx sdk.Context, utxId, outboundId string) { + t.Helper() + ob := loadOutbound(t, chainApp, ctx, utxId, outboundId) + require.Equal(t, uexecutortypes.Status_PENDING, ob.OutboundStatus, "a refused settle must not move the outbound") + require.Nil(t, ob.ObservedTx) + + has, err := chainApp.UexecutorKeeper.PendingOutbounds.Has(ctx, outboundId) + require.NoError(t, err) + require.True(t, has, "a refused settle must leave the outbound queued") +} diff --git a/test/integration/uexecutor/gas_refund_test.go b/test/integration/uexecutor/gas_refund_test.go index a5d238f2b..237267a90 100644 --- a/test/integration/uexecutor/gas_refund_test.go +++ b/test/integration/uexecutor/gas_refund_test.go @@ -38,3 +38,30 @@ func TestInboundRevertGasNotRefunded(t *testing.T) { require.Nil(t, utx.OutboundTx[0].PcRefundExecution, "INBOUND_REVERT must not attempt a gas refund — the user was never charged for it") } + +// The SVM gateway dropped gas_used from RevertUniversalTx, so a revert now votes +// "0". That has to settle exactly like any other revert: accepted by the vote +// handler and refunding nothing. "0" rather than "" matters — the vote handler +// rejects an empty gas_fee_used, and the value feeds the outbound ballot key. +func TestInboundRevertSettlesWithZeroGasFeeUsed(t *testing.T) { + chainApp, ctx, vals, utxId, ob, coreVals := setupOutboundVotingTest(t, 4) + + ob.TxType = uexecutortypes.TxType_INBOUND_REVERT + ob.GasFee = "1000" + ob.GasToken = "0x000000000000000000000000000000000000C0dE" + require.NoError(t, chainApp.UexecutorKeeper.UpdateOutbound(ctx, utxId, *ob)) + + for i := 0; i < 3; i++ { + valAddr, err := sdk.ValAddressFromBech32(coreVals[i].OperatorAddress) + require.NoError(t, err) + require.NoError(t, utils.ExecVoteOutbound( + t, ctx, chainApp, vals[i], sdk.AccAddress(valAddr).String(), utxId, ob, true, "", "0"), + "a revert voting gas_fee_used=0 must be accepted") + } + + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxId) + require.NoError(t, err) + require.True(t, found) + require.Nil(t, utx.OutboundTx[0].PcRefundExecution, + "a revert refunds nothing regardless of the reported gas fee") +} diff --git a/test/integration/uexecutor/gasless_module_sender_test.go b/test/integration/uexecutor/gasless_module_sender_test.go new file mode 100644 index 000000000..8ecbe7133 --- /dev/null +++ b/test/integration/uexecutor/gasless_module_sender_test.go @@ -0,0 +1,135 @@ +package integrationtest + +import ( + "testing" + + "cosmossdk.io/math" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + utils "github.com/pushchain/push-chain-node/test/utils" + "github.com/pushchain/push-chain-node/types" + uexecutorkeeper "github.com/pushchain/push-chain-node/x/uexecutor/keeper" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" + uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types" +) + +// TestGaslessExecutePayloadWithModuleSender is the invariant guard for +// F-2026-18197. +// +// The fix hardens x/vm's Keeper.EthereumTx to require that msg.From is the +// ECDSA signer of the raw transaction, so that an MsgEthereumTx smuggled in via +// a nested-message dispatcher can no longer execute as somebody else. Push's +// gasless / module-sender flows must be completely unaffected by that, and they +// are - because they never reach that msg server. MsgExecutePayload runs the +// payload through CallEVM / DerivedEVMCall, which go straight to +// ApplyMessageWithConfig; no MsgEthereumTx is ever constructed. +// +// This test pins that down end to end: a gasless MsgExecutePayload, whose EVM +// caller is the uexecutor module account, still executes successfully. +func TestGaslessExecutePayloadWithModuleSender(t *testing.T) { + app, ctx, _ := utils.SetAppWithValidators(t) + + // The uexecutor module account is derived from a name, not from a key pair. + // It can never produce an ECDSA signature, so if a module operation ever + // routed through MsgEthereumTx the new VerifySender check would reject it + // 100% of the time. That is why module-driven EVM calls must keep using the + // ApplyMessage* path, and why this test exists. + moduleAcc := app.AccountKeeper.GetModuleAccount(ctx, uexecutortypes.ModuleName) + require.NotNil(t, moduleAcc) + require.Nil(t, moduleAcc.GetPubKey(), + "the uexecutor module account must have no public key - it cannot sign an MsgEthereumTx") + + app.UregistryKeeper.AddChainConfig(ctx, &uregistrytypes.ChainConfig{ + Chain: "eip155:11155111", + VmType: uregistrytypes.VmType_EVM, + PublicRpcUrl: "https://sepolia.drpc.org", + GatewayAddress: "0x28E0F09bE2321c1420Dc60Ee146aACbD68B335Fe", + BlockConfirmation: &uregistrytypes.BlockConfirmation{ + FastInbound: 5, + StandardInbound: 12, + }, + GatewayMethods: []*uregistrytypes.GatewayMethods{{ + Name: "addFunds", + Identifier: "", + EventIdentifier: "0xb28f49668e7e76dc96d7aabe5b7f63fecfbd1c3574774c05e8204e749fd96fbd", + }}, + Enabled: &uregistrytypes.ChainEnabled{ + IsInboundEnabled: true, + IsOutboundEnabled: true, + }, + }) + + params := app.FeeMarketKeeper.GetParams(ctx) + params.BaseFee = math.LegacyNewDec(1000000000) + app.FeeMarketKeeper.SetParams(ctx, params) + + ms := uexecutorkeeper.NewMsgServerImpl(app.UexecutorKeeper) + + universalAccount := &uexecutortypes.UniversalAccountId{ + ChainNamespace: "eip155", + ChainId: "11155111", + Owner: "0x778d3206374f8ac265728e18e3fe2ae6b93e4ce4", + } + payload := &uexecutortypes.UniversalPayload{ + To: "0x527F3692F5C53CfA83F7689885995606F93b6164", + Value: "0", + Data: "0x2ba2ed980000000000000000000000000000000000000000000000000000000000000312", + GasLimit: "21000000", + MaxFeePerGas: "1000000000", + MaxPriorityFeePerGas: "200000000", + Nonce: "1", + Deadline: "0", + VType: uexecutortypes.VerificationType(0), + } + + evmFrom := common.HexToAddress("0x1000000000000000000000000000000000000001") + + err := app.BankKeeper.MintCoins( + ctx, + uexecutortypes.ModuleName, + sdk.NewCoins(sdk.NewCoin(types.BaseDenom, sdkmath.NewInt(2_000_000_000_000_000))), + ) + require.NoError(t, err) + + err = app.BankKeeper.SendCoinsFromModuleToAccount( + ctx, + uexecutortypes.ModuleName, + sdk.AccAddress(evmFrom.Bytes()), + sdk.NewCoins(sdk.NewCoin(types.BaseDenom, sdkmath.NewInt(1_000_000_000_000_000))), + ) + require.NoError(t, err) + + _, err = app.UexecutorKeeper.DeployUEAV2(ctx, evmFrom, universalAccount) + require.NoError(t, err) + + ueaAddr, _, err := app.UexecutorKeeper.CallFactoryToGetUEAAddressForOrigin( + ctx, evmFrom, utils.GetDefaultAddresses().FactoryAddr, universalAccount, + ) + require.NoError(t, err) + + err = app.BankKeeper.SendCoinsFromModuleToAccount( + ctx, + uexecutortypes.ModuleName, + sdk.AccAddress(ueaAddr.Bytes()), + sdk.NewCoins(sdk.NewCoin(types.BaseDenom, sdkmath.NewInt(1_000_000_000_000_000))), + ) + require.NoError(t, err) + + // The gasless message itself: signer is a relayer, the EVM caller is the + // uexecutor module. This must still succeed after the x/vm change. + // testSigner (execute_payload_test.go) is a valid 20-byte account. The + // literal this test originally carried decoded to 42 bytes, which + // F-2026-18200's signer-length guard rejects in GetAddressPair before + // ExecutePayload does any work - so the test failed on an address that was + // never the thing under test. + _, err = ms.ExecutePayload(ctx, &uexecutortypes.MsgExecutePayload{ + Signer: testSigner, + UniversalAccountId: universalAccount, + UniversalPayload: payload, + VerificationData: "0x91987784d56359fa91c3e3e0332f4f0cffedf9c081eb12874a63b41d5b5e5c660dc827947c2ae26e658d0551ad4b2d2aa073d62691429a0ae239d2cc58055bf11c", + }) + require.NoError(t, err, "gasless module-sender MsgExecutePayload must still execute end to end") +} diff --git a/test/integration/uexecutor/inbound_cea_contract_outbound_test.go b/test/integration/uexecutor/inbound_cea_contract_outbound_test.go new file mode 100644 index 000000000..28a79b7f4 --- /dev/null +++ b/test/integration/uexecutor/inbound_cea_contract_outbound_test.go @@ -0,0 +1,478 @@ +package integrationtest + +import ( + "fmt" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + authz "github.com/cosmos/cosmos-sdk/x/authz" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app" + utils "github.com/pushchain/push-chain-node/test/utils" + uexecutorkeeper "github.com/pushchain/push-chain-node/x/uexecutor/keeper" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" + uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types" + uvalidatortypes "github.com/pushchain/push-chain-node/x/uvalidator/types" +) + +// F-2026-18195 (defect 2). On the isCEA smart-contract branch the callback may +// itself call UniversalGatewayPC: that burns the PRC20 and emits a +// UniversalTxOutbound log. DerivedEVMCall skips PostTxProcessing, so the EVM +// hook never sees those logs — the handler itself has to attach them. Before +// the fix it returned right after recording the PcTx, leaving burned supply +// with no OutboundTx and no PendingOutbounds row (and a SUCCESS PcTx, so +// neither rescue nor remint were eligible). +// +// The attach now runs inside the same CacheContext as the callback, before +// writeCache(), so the burn and the outbound rows commit together or not at all. + +// gatewayCallingRecipientAddr hosts the mock recipient that re-enters +// UniversalGatewayPC during its callback. 0xD0-0xFF is outside the reserved +// system-contract ranges (see x/uregistry/types/constants.go). +var gatewayCallingRecipientAddr = common.HexToAddress("0x00000000000000000000000000000000000000D5") + +// gatewayWithdrawCalldata is the ABI-encoded UniversalGatewayPC withdraw call +// used by TestInboundInitiatedOutbound — recipient 0x1234..5678, PRC20 0x..0e06, +// amount 1000000. The test gateway answers it by emitting UniversalTxOutbound. +const gatewayWithdrawCalldata = "b3ca1fbc" + + "0000000000000000000000000000000000000000000000000000000000000020" + + "00000000000000000000000000000000000000000000000000000000000000c0" + + "0000000000000000000000000000000000000000000000000000000000000e06" + + "00000000000000000000000000000000000000000000000000000000000f4240" + + "000000000000000000000000000000000000000000000000000000000007a120" + + "0000000000000000000000000000000000000000000000000000000000000100" + + "0000000000000000000000001234567890abcdef1234567890abcdef12345678" + + "0000000000000000000000000000000000000000000000000000000000000014" + + "1234567890abcdef1234567890abcdef12345678000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + +// expectedOutboundRecipient / expectedOutboundPRC20 mirror gatewayWithdrawCalldata. +const ( + expectedOutboundRecipient = "0x1234567890abcdef1234567890abcdef12345678" + expectedOutboundPRC20 = "0x0000000000000000000000000000000000000e06" + expectedOutboundAmount = "1000000" +) + +// gatewayCallingRecipientCode assembles runtime bytecode for a recipient that, +// on any call: +// +// 1. SSTOREs 1 into slot 0 — a witness that the callback body ran AND committed; +// 2. CALLs UniversalGatewayPC (0x..C1) with gatewayWithdrawCalldata, so the +// callback emits a UniversalTxOutbound log from the gateway address; +// 3. bubbles a gateway failure up as a REVERT. +// +// Assembly (all self-references are computed, not hard-coded): +// +// PUSH1 0x01; PUSH1 0x00; SSTORE storage[0] = 1 +// PUSH2 len; PUSH2 off; PUSH1 0x00; CODECOPY mem[0:len] = code[off:off+len] +// PUSH1 0x00; PUSH1 0x00 retSize, retOffset +// PUSH2 len; PUSH1 0x00 argsSize, argsOffset +// PUSH1 0x00; PUSH1 0xC1; GAS; CALL value, gateway, gas +// PUSH1 ok; JUMPI taken when CALL succeeded +// PUSH1 0x00; PUSH1 0x00; REVERT gateway call failed +// JUMPDEST; STOP +// +func gatewayCallingRecipientCode(t *testing.T) string { + t.Helper() + + blobLen := len(gatewayWithdrawCalldata) / 2 + + // The prologue below is a fixed 39 bytes; the blob is appended right after + // it, and the success JUMPDEST is its second-to-last byte. + const prologueLen = 39 + const okJumpDest = prologueLen - 2 + + prologue := "6001600055" + // PUSH1 1, PUSH1 0, SSTORE + fmt.Sprintf("61%04x", blobLen) + // PUSH2 blobLen (CODECOPY size) + fmt.Sprintf("61%04x", prologueLen) + // PUSH2 prologueLen (CODECOPY code offset) + "6000" + // PUSH1 0 (CODECOPY dest offset) + "39" + // CODECOPY + "6000" + // PUSH1 0 retSize + "6000" + // PUSH1 0 retOffset + fmt.Sprintf("61%04x", blobLen) + // PUSH2 blobLen argsSize + "6000" + // PUSH1 0 argsOffset + "6000" + // PUSH1 0 value + "60c1" + // PUSH1 0xC1 UniversalGatewayPC + "5a" + // GAS + "f1" + // CALL + fmt.Sprintf("60%02x", okJumpDest) + // PUSH1 okJumpDest + "57" + // JUMPI + "6000" + // PUSH1 0 revert offset + "6000" + // PUSH1 0 revert size + "fd" + // REVERT + "5b" + // JUMPDEST (okJumpDest) + "00" // STOP + + require.Equal(t, prologueLen, len(prologue)/2, "prologue length drifted; okJumpDest/CODECOPY offset are stale") + + return prologue + gatewayWithdrawCalldata +} + +// deployGatewayCallingRecipient installs the contract above and funds it with +// upc so DeductGasFeesFromReceipt succeeds and execution reaches the attach. +func deployGatewayCallingRecipient(t *testing.T, chainApp *app.ChainApp, ctx sdk.Context) common.Address { + t.Helper() + + addr := utils.DeployContract(t, chainApp, ctx, gatewayCallingRecipientAddr, gatewayCallingRecipientCode(t)) + + fundCoins := sdk.NewCoins(sdk.NewInt64Coin("upc", 1_000_000_000)) + require.NoError(t, chainApp.BankKeeper.MintCoins(ctx, utils.MintModule, fundCoins)) + require.NoError(t, chainApp.BankKeeper.SendCoinsFromModuleToAccount( + ctx, utils.MintModule, sdk.AccAddress(addr.Bytes()), fundCoins)) + + return addr +} + +// setupCEAContractOutboundTest mirrors setupInboundCEASmartContractTest but the +// recipient re-enters the gateway, and chain outbound can be disabled to force +// the attach to fail. +func setupCEAContractOutboundTest( + t *testing.T, + numVals int, + outboundEnabled bool, +) (*app.ChainApp, sdk.Context, []string, []stakingtypes.Validator, common.Address) { + t.Helper() + + chainApp, ctx, _, validators := utils.SetAppWithMultipleValidators(t, numVals) + + chainConfigTest := uregistrytypes.ChainConfig{ + Chain: "eip155:11155111", + VmType: uregistrytypes.VmType_EVM, + PublicRpcUrl: "https://sepolia.drpc.org", + GatewayAddress: "0x28E0F09bE2321c1420Dc60Ee146aACbD68B335Fe", + BlockConfirmation: &uregistrytypes.BlockConfirmation{ + FastInbound: 5, + StandardInbound: 12, + }, + GatewayMethods: []*uregistrytypes.GatewayMethods{{ + Name: "addFunds", + Identifier: "", + EventIdentifier: "0xb28f49668e7e76dc96d7aabe5b7f63fecfbd1c3574774c05e8204e749fd96fbd", + ConfirmationType: 5, + }}, + Enabled: &uregistrytypes.ChainEnabled{ + IsInboundEnabled: true, + IsOutboundEnabled: outboundEnabled, + }, + } + + prc20Address := utils.GetDefaultAddresses().PRC20USDCAddr + usdcAddress := utils.GetDefaultAddresses().ExternalUSDCAddr + + tokenConfigTest := uregistrytypes.TokenConfig{ + Chain: "eip155:11155111", + Address: usdcAddress.String(), + Name: "USD Coin", + Symbol: "USDC", + Decimals: 6, + Enabled: true, + LiquidityCap: "1000000000000000000000000", + TokenType: 1, + NativeRepresentation: &uregistrytypes.NativeRepresentation{ + Denom: "", + ContractAddress: prc20Address.String(), + }, + } + + chainApp.UregistryKeeper.AddChainConfig(ctx, &chainConfigTest) + chainApp.UregistryKeeper.AddTokenConfig(ctx, &tokenConfigTest) + + universalVals := make([]string, len(validators)) + for i, val := range validators { + network := uvalidatortypes.NetworkInfo{PeerId: fmt.Sprintf("temp%d", i+1), MultiAddrs: []string{"temp"}} + require.NoError(t, chainApp.UvalidatorKeeper.AddUniversalValidator(ctx, val.OperatorAddress, network)) + universalVals[i] = sdk.AccAddress([]byte(fmt.Sprintf("universal-validator-%d", i))).String() + } + + for i, val := range validators { + accAddr, err := sdk.ValAddressFromBech32(val.OperatorAddress) + require.NoError(t, err) + + coreValAddr := sdk.AccAddress(accAddr) + uniValAddr := sdk.MustAccAddressFromBech32(universalVals[i]) + + auth := authz.NewGenericAuthorization(sdk.MsgTypeURL(&uexecutortypes.MsgVoteInbound{})) + exp := ctx.BlockTime().Add(time.Hour) + require.NoError(t, chainApp.AuthzKeeper.SaveGrant(ctx, uniValAddr, coreValAddr, auth, &exp)) + } + + recipient := deployGatewayCallingRecipient(t, chainApp, ctx) + + return chainApp, ctx, universalVals, validators, recipient +} + +// ceaContractInbound builds an isCEA inbound targeting a contract recipient. +func ceaContractInbound( + txHash string, + recipient common.Address, + txType uexecutortypes.TxType, + amount string, +) *uexecutortypes.Inbound { + testAddress := utils.GetDefaultAddresses().DefaultTestAddr + usdcAddress := utils.GetDefaultAddresses().ExternalUSDCAddr + + return &uexecutortypes.Inbound{ + SourceChain: "eip155:11155111", + TxHash: txHash, + Sender: testAddress, + Recipient: recipient.String(), + Amount: amount, + AssetAddr: usdcAddress.String(), + LogIndex: "1", + TxType: txType, + UniversalPayload: &uexecutortypes.UniversalPayload{ + To: recipient.String(), + Value: "0", + Data: "0xdeadbeef", + GasLimit: "21000000", + MaxFeePerGas: "1000000000", + MaxPriorityFeePerGas: "200000000", + Nonce: "1", + Deadline: "9999999999", + VType: uexecutortypes.VerificationType(1), + }, + VerificationData: "", + IsCEA: true, + RevertInstructions: &uexecutortypes.RevertInstructions{ + FundRecipient: testAddress, + }, + } +} + +func reachInboundQuorum( + t *testing.T, + ctx sdk.Context, + chainApp *app.ChainApp, + universalVals []string, + coreVals []stakingtypes.Validator, + inbound *uexecutortypes.Inbound, +) { + t.Helper() + + for i := 0; i < 3; i++ { + valAddr, err := sdk.ValAddressFromBech32(coreVals[i].OperatorAddress) + require.NoError(t, err) + require.NoError(t, utils.ExecVoteInbound(t, ctx, chainApp, universalVals[i], sdk.AccAddress(valAddr).String(), inbound)) + } +} + +// lastPcTx returns the executeUniversalTx PcTx, which is always the final one +// recorded on the smart-contract branch. +func lastPcTx(t *testing.T, utx uexecutortypes.UniversalTx) *uexecutortypes.PCTx { + t.Helper() + require.NotEmpty(t, utx.PcTx, "at least one PcTx must be recorded") + return utx.PcTx[len(utx.PcTx)-1] +} + +func TestInboundCEAContractCallbackOutbound(t *testing.T) { + slot := common.Hash{} + + // --- FUNDS_AND_PAYLOAD: the defect and its fix ------------------------- + + t.Run("FUNDS_AND_PAYLOAD contract callback gateway burn creates OutboundTx and PendingOutbounds", func(t *testing.T) { + chainApp, ctx, vals, coreVals, recipient := setupCEAContractOutboundTest(t, 4, true) + + inbound := ceaContractInbound("0xsc-outbound-funds-01", recipient, uexecutortypes.TxType_FUNDS_AND_PAYLOAD, "1000000") + reachInboundQuorum(t, ctx, chainApp, vals, coreVals, inbound) + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.True(t, found) + + callPcTx := lastPcTx(t, utx) + require.Equal(t, "SUCCESS", callPcTx.Status, "executeUniversalTx should succeed: %s", callPcTx.ErrorMsg) + + // The callback committed (proves writeCache ran). + require.Equal(t, common.BigToHash(common.Big1), chainApp.EVMKeeper.GetState(ctx, recipient, slot), + "recipient slot 0 must be 1 (callback committed)") + + // THE PRIMARY ASSERTION: the nested gateway burn produced an outbound. + require.Len(t, utx.OutboundTx, 1, "the gateway call inside the callback must produce exactly one OutboundTx") + + out := utx.OutboundTx[0] + require.Equal(t, "eip155:11155111", out.DestinationChain) + require.Equal(t, expectedOutboundRecipient, out.Recipient) + require.Equal(t, expectedOutboundAmount, out.Amount) + require.Equal(t, expectedOutboundPRC20, out.Prc20AssetAddr) + require.Equal(t, uexecutortypes.Status_PENDING, out.OutboundStatus) + require.NotEqual(t, uexecutortypes.TxType_INBOUND_REVERT, out.TxType, + "the isCEA route must never auto-revert; this outbound comes from the callback") + + // ... and a PendingOutbounds row, so it is actually signed and delivered. + entry, err := chainApp.UexecutorKeeper.PendingOutbounds.Get(ctx, out.Id) + require.NoError(t, err, "outbound must be indexed in PendingOutbounds") + require.Equal(t, out.Id, entry.OutboundId) + require.Equal(t, utxKey, entry.UniversalTxId) + }) + + t.Run("FUNDS_AND_PAYLOAD attach failure rolls the callback back and records FAILED PcTx", func(t *testing.T) { + // Outbound disabled for the destination chain → BuildOutboundsFromReceipt + // errors, which is the attach failure we need to exercise. + chainApp, ctx, vals, coreVals, recipient := setupCEAContractOutboundTest(t, 4, false) + + recipientAcc := sdk.AccAddress(recipient.Bytes()) + balanceBefore := chainApp.BankKeeper.GetBalance(ctx, recipientAcc, "upc") + + inbound := ceaContractInbound("0xsc-outbound-funds-02", recipient, uexecutortypes.TxType_FUNDS_AND_PAYLOAD, "1000000") + reachInboundQuorum(t, ctx, chainApp, vals, coreVals, inbound) + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.True(t, found) + + callPcTx := lastPcTx(t, utx) + require.Equal(t, "FAILED", callPcTx.Status, "attach failure must surface on the PcTx, not be swallowed") + require.Contains(t, callPcTx.ErrorMsg, "outbound attach failed") + require.Contains(t, callPcTx.ErrorMsg, "outbound is disabled for chain") + + // The whole callback — including the gateway burn — was rolled back. + require.Equal(t, common.Hash{}, chainApp.EVMKeeper.GetState(ctx, recipient, slot), + "recipient slot 0 must stay 0 (callback rolled back with the attach failure)") + require.Empty(t, utx.OutboundTx, "no outbound may be recorded when the attach failed") + + querier := uexecutorkeeper.NewQuerier(chainApp.UexecutorKeeper) + resp, err := querier.AllPendingOutbounds(ctx, &uexecutortypes.QueryAllPendingOutboundsRequest{}) + require.NoError(t, err) + require.Empty(t, resp.Entries, "no PendingOutbounds row may survive a rolled-back callback") + + // No gas fee was collected either — the cache holding it was discarded. + require.Equal(t, balanceBefore.Amount, chainApp.BankKeeper.GetBalance(ctx, recipientAcc, "upc").Amount, + "no fee may be collected when the cache is discarded") + + // The deposit happens before the cache scope and stays committed, so the + // principal is still with the recipient the sender nominated. + require.Equal(t, "SUCCESS", utx.PcTx[0].Status, "deposit is outside the cache scope and stays committed") + }) + + // --- GAS_AND_PAYLOAD: the same branch in the sibling handler ----------- + // + // Amount is 0 so the handler skips gasAndPayloadDepositAutoSwap, which + // needs a live Uniswap quoter/router that the integration harness does not + // deploy. isSmartContract is set from the recipient's code hash regardless + // of amount, so the contract branch under test is still exercised. + + t.Run("GAS_AND_PAYLOAD contract callback gateway burn creates OutboundTx and PendingOutbounds", func(t *testing.T) { + chainApp, ctx, vals, coreVals, recipient := setupCEAContractOutboundTest(t, 4, true) + + inbound := ceaContractInbound("0xsc-outbound-gas-01", recipient, uexecutortypes.TxType_GAS_AND_PAYLOAD, "0") + reachInboundQuorum(t, ctx, chainApp, vals, coreVals, inbound) + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.True(t, found) + + callPcTx := lastPcTx(t, utx) + require.Equal(t, "SUCCESS", callPcTx.Status, "executeUniversalTx should succeed: %s", callPcTx.ErrorMsg) + + require.Equal(t, common.BigToHash(common.Big1), chainApp.EVMKeeper.GetState(ctx, recipient, slot), + "recipient slot 0 must be 1 (callback committed)") + + require.Len(t, utx.OutboundTx, 1, "the gateway call inside the callback must produce exactly one OutboundTx") + + out := utx.OutboundTx[0] + require.Equal(t, "eip155:11155111", out.DestinationChain) + require.Equal(t, expectedOutboundRecipient, out.Recipient) + require.Equal(t, expectedOutboundAmount, out.Amount) + require.Equal(t, expectedOutboundPRC20, out.Prc20AssetAddr) + require.Equal(t, uexecutortypes.Status_PENDING, out.OutboundStatus) + + entry, err := chainApp.UexecutorKeeper.PendingOutbounds.Get(ctx, out.Id) + require.NoError(t, err, "outbound must be indexed in PendingOutbounds") + require.Equal(t, utxKey, entry.UniversalTxId) + }) + + t.Run("GAS_AND_PAYLOAD attach failure rolls the callback back and records FAILED PcTx", func(t *testing.T) { + chainApp, ctx, vals, coreVals, recipient := setupCEAContractOutboundTest(t, 4, false) + + recipientAcc := sdk.AccAddress(recipient.Bytes()) + balanceBefore := chainApp.BankKeeper.GetBalance(ctx, recipientAcc, "upc") + + inbound := ceaContractInbound("0xsc-outbound-gas-02", recipient, uexecutortypes.TxType_GAS_AND_PAYLOAD, "0") + reachInboundQuorum(t, ctx, chainApp, vals, coreVals, inbound) + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.True(t, found) + + callPcTx := lastPcTx(t, utx) + require.Equal(t, "FAILED", callPcTx.Status, "attach failure must surface on the PcTx, not be swallowed") + require.Contains(t, callPcTx.ErrorMsg, "outbound attach failed") + require.Contains(t, callPcTx.ErrorMsg, "outbound is disabled for chain") + + require.Equal(t, common.Hash{}, chainApp.EVMKeeper.GetState(ctx, recipient, slot), + "recipient slot 0 must stay 0 (callback rolled back with the attach failure)") + require.Empty(t, utx.OutboundTx, "no outbound may be recorded when the attach failed") + + querier := uexecutorkeeper.NewQuerier(chainApp.UexecutorKeeper) + resp, err := querier.AllPendingOutbounds(ctx, &uexecutortypes.QueryAllPendingOutboundsRequest{}) + require.NoError(t, err) + require.Empty(t, resp.Entries, "no PendingOutbounds row may survive a rolled-back callback") + + require.Equal(t, balanceBefore.Amount, chainApp.BankKeeper.GetBalance(ctx, recipientAcc, "upc").Amount, + "no fee may be collected when the cache is discarded") + }) + + // --- regression: the UEA branch is untouched -------------------------- + + t.Run("UEA branch still attaches its outbound and indexes it", func(t *testing.T) { + chainApp, ctx, vals, inbound, coreVals, _ := setupInboundInitiatedOutboundTest(t, 4) + reachInboundQuorum(t, ctx, chainApp, vals, coreVals, inbound) + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.True(t, found) + + require.Len(t, utx.OutboundTx, 1, "the UEA payload's gateway call must still produce exactly one OutboundTx") + + out := utx.OutboundTx[0] + require.Equal(t, expectedOutboundRecipient, out.Recipient) + require.Equal(t, expectedOutboundAmount, out.Amount) + require.Equal(t, uexecutortypes.Status_PENDING, out.OutboundStatus) + + entry, err := chainApp.UexecutorKeeper.PendingOutbounds.Get(ctx, out.Id) + require.NoError(t, err, "UEA-branch outbound must still be indexed in PendingOutbounds") + require.Equal(t, utxKey, entry.UniversalTxId) + }) + + // --- regression: callbacks that emit nothing are untouched ------------- + + t.Run("contract callback without a gateway call still succeeds with no outbound rows", func(t *testing.T) { + chainApp, ctx, vals, coreVals, _ := setupCEAContractOutboundTest(t, 4, true) + + // Plain STOP recipient: the callback runs, emits no logs at all. + plain := deployMockRecipientContract(t, chainApp, ctx) + fundCoins := sdk.NewCoins(sdk.NewInt64Coin("upc", 1_000_000_000)) + require.NoError(t, chainApp.BankKeeper.MintCoins(ctx, utils.MintModule, fundCoins)) + require.NoError(t, chainApp.BankKeeper.SendCoinsFromModuleToAccount( + ctx, utils.MintModule, sdk.AccAddress(plain.Bytes()), fundCoins)) + + inbound := ceaContractInbound("0xsc-outbound-noop-01", plain, uexecutortypes.TxType_FUNDS_AND_PAYLOAD, "1000000") + reachInboundQuorum(t, ctx, chainApp, vals, coreVals, inbound) + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.True(t, found) + + require.Equal(t, "SUCCESS", utx.PcTx[0].Status, "deposit should still succeed") + callPcTx := lastPcTx(t, utx) + require.Equal(t, "SUCCESS", callPcTx.Status, "callback should still succeed: %s", callPcTx.ErrorMsg) + require.Empty(t, callPcTx.ErrorMsg) + + require.Empty(t, utx.OutboundTx, "a callback that emits nothing must not gain an outbound") + + querier := uexecutorkeeper.NewQuerier(chainApp.UexecutorKeeper) + resp, err := querier.AllPendingOutbounds(ctx, &uexecutortypes.QueryAllPendingOutboundsRequest{}) + require.NoError(t, err) + require.Empty(t, resp.Entries, "no spurious PendingOutbounds row") + }) +} diff --git a/test/integration/uexecutor/inbound_multicall_outbound_atomicity_test.go b/test/integration/uexecutor/inbound_multicall_outbound_atomicity_test.go new file mode 100644 index 000000000..e3075ec42 --- /dev/null +++ b/test/integration/uexecutor/inbound_multicall_outbound_atomicity_test.go @@ -0,0 +1,471 @@ +package integrationtest + +import ( + "fmt" + "math/big" + "strings" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + authz "github.com/cosmos/cosmos-sdk/x/authz" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app" + utils "github.com/pushchain/push-chain-node/test/utils" + uexecutorkeeper "github.com/pushchain/push-chain-node/x/uexecutor/keeper" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" + uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types" + uvalidatortypes "github.com/pushchain/push-chain-node/x/uvalidator/types" +) + +// F-2026-18825. A UEA payload that calls UniversalGatewayPC burns PRC20 and +// emits UniversalTxOutbound. ExecutePayloadV2 used to commit that burn via +// writeCache() and only then hand the receipt back, leaving the two inbound +// handlers to attach the outbounds afterwards, outside any cache. +// BuildOutboundsFromReceipt is all-or-nothing, so a multicall carrying one +// invalid leg (unregistered PRC20, disabled chain) discarded every valid +// outbound alongside it — while the burns for all of them stayed committed. +// The handlers then stashed the failure in UniversalTx.RevertError (9 writes / +// 0 reads chain-wide), marked the payload PcTx SUCCESS and returned nil, so +// nothing on chain recorded that anything had gone wrong. +// +// The attach now runs inside ExecutePayloadV2's existing CacheContext, before +// writeCache(): the burn and the OutboundTx / PendingOutbounds rows commit +// together or not at all, and the failure surfaces as a FAILED PcTx. +// +// The vote tx must still succeed either way — the handler runs inside +// MsgVoteInbound, and returning an error there would lose the validator's vote. +// That constraint is why the fix is atomicity rather than error propagation. + +// unregisteredPRC20 is a PRC20 address with no TokenConfig registered against +// it, which is what makes the sibling leg of the multicall invalid. +var unregisteredPRC20 = common.HexToAddress("0x0000000000000000000000000000000000000e0f") + +// gatewayNonceSlot is UniversalGatewayPC storage slot 2 (its outbound nonce). +// The mock gateway bumps it on every withdraw, so it doubles as a witness for +// whether the payload's EVM state was committed or rolled back. +var gatewayNonceSlot = common.BigToHash(big.NewInt(2)) + +// ueaMulticallSelector is bytes4(keccak256("UEA_MULTICALL")), the magic prefix +// UEA_EVM._isMulticall() looks for before decoding payload.data as Multicall[]. +func ueaMulticallSelector(t *testing.T) []byte { + t.Helper() + sel := crypto.Keccak256([]byte("UEA_MULTICALL"))[:4] + // Guards against the deployed UEA_EVM_BYTECODE drifting away from the + // selector this test builds payloads with. + require.Equal(t, "0x2cc2842d", hexutil.Encode(sel), "UEA multicall selector drifted") + return sel +} + +// multicallLeg mirrors the Solidity `Multicall { address to; uint256 value; +// bytes data; }` struct the UEA decodes out of a multicall payload. +type multicallLeg struct { + To common.Address + Value *big.Int + Data []byte +} + +// encodeUEAMulticall builds payload.data for a UEA multicall: the magic +// selector followed by an ABI-encoded Multicall[]. +func encodeUEAMulticall(t *testing.T, legs []multicallLeg) string { + t.Helper() + + tupleArray, err := abi.NewType("tuple[]", "", []abi.ArgumentMarshaling{ + {Name: "to", Type: "address"}, + {Name: "value", Type: "uint256"}, + {Name: "data", Type: "bytes"}, + }) + require.NoError(t, err) + + encoded, err := abi.Arguments{{Type: tupleArray}}.Pack(legs) + require.NoError(t, err) + + return hexutil.Encode(append(ueaMulticallSelector(t), encoded...)) +} + +// gatewayWithdrawCalldata is the UniversalGatewayPC withdraw call used across +// the outbound tests (see TestInboundInitiatedOutbound), with the burned PRC20 +// left as a parameter so a leg can be made invalid. Word 2 of the argument +// block is the token the gateway reports in its UniversalTxOutbound event; the +// happy-path assertions below pin that mapping down. +func gatewayWithdrawCalldataFor(t *testing.T, prc20 common.Address) []byte { + t.Helper() + + words := []string{ + "0000000000000000000000000000000000000000000000000000000000000020", + "00000000000000000000000000000000000000000000000000000000000000c0", + hexutil.Encode(common.LeftPadBytes(prc20.Bytes(), 32))[2:], // PRC20 to burn + "00000000000000000000000000000000000000000000000000000000000f4240", // amount: 1000000 + "000000000000000000000000000000000000000000000000000000000007a120", + "0000000000000000000000000000000000000000000000000000000000000100", + "0000000000000000000000001234567890abcdef1234567890abcdef12345678", + "0000000000000000000000000000000000000000000000000000000000000014", + "1234567890abcdef1234567890abcdef12345678000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + } + + data, err := hexutil.Decode("0xb3ca1fbc" + strings.Join(words, "")) + require.NoError(t, err) + return data +} + +// multicallToGateway builds a UEA multicall payload whose legs each burn one of +// the given PRC20s through UniversalGatewayPC. +func multicallToGateway(t *testing.T, prc20s ...common.Address) string { + t.Helper() + + gateway := utils.GetDefaultAddresses().UniversalGatewayPCAddr + legs := make([]multicallLeg, 0, len(prc20s)) + for _, prc20 := range prc20s { + legs = append(legs, multicallLeg{ + To: gateway, + Value: big.NewInt(0), + Data: gatewayWithdrawCalldataFor(t, prc20), + }) + } + + return encodeUEAMulticall(t, legs) +} + +// setupMulticallOutboundTest registers eip155:11155111 with outbound enabled, +// registers PRC20USDC against it, deploys the UEA for DefaultTestAddr and funds +// it with upc so gas-fee deduction never masks the behaviour under test. +func setupMulticallOutboundTest( + t *testing.T, + numVals int, +) (*app.ChainApp, sdk.Context, []string, []stakingtypes.Validator, common.Address) { + t.Helper() + + chainApp, ctx, _, validators := utils.SetAppWithMultipleValidators(t, numVals) + + testAddress := utils.GetDefaultAddresses().DefaultTestAddr + prc20Address := utils.GetDefaultAddresses().PRC20USDCAddr + usdcAddress := utils.GetDefaultAddresses().ExternalUSDCAddr + + chainApp.UregistryKeeper.AddChainConfig(ctx, &uregistrytypes.ChainConfig{ + Chain: "eip155:11155111", + VmType: uregistrytypes.VmType_EVM, + PublicRpcUrl: "https://sepolia.drpc.org", + GatewayAddress: "0x28E0F09bE2321c1420Dc60Ee146aACbD68B335Fe", + BlockConfirmation: &uregistrytypes.BlockConfirmation{ + FastInbound: 5, + StandardInbound: 12, + }, + GatewayMethods: []*uregistrytypes.GatewayMethods{{ + Name: "addFunds", + Identifier: "", + EventIdentifier: "0xb28f49668e7e76dc96d7aabe5b7f63fecfbd1c3574774c05e8204e749fd96fbd", + ConfirmationType: 5, + }}, + Enabled: &uregistrytypes.ChainEnabled{ + IsInboundEnabled: true, + IsOutboundEnabled: true, + }, + }) + + chainApp.UregistryKeeper.AddTokenConfig(ctx, &uregistrytypes.TokenConfig{ + Chain: "eip155:11155111", + Address: usdcAddress.String(), + Name: "USD Coin", + Symbol: "USDC", + Decimals: 6, + Enabled: true, + LiquidityCap: "1000000000000000000000000", + TokenType: 1, + NativeRepresentation: &uregistrytypes.NativeRepresentation{ + Denom: "", + ContractAddress: prc20Address.String(), + }, + }) + + universalVals := make([]string, len(validators)) + for i, val := range validators { + network := uvalidatortypes.NetworkInfo{PeerId: fmt.Sprintf("temp%d", i+1), MultiAddrs: []string{"temp"}} + require.NoError(t, chainApp.UvalidatorKeeper.AddUniversalValidator(ctx, val.OperatorAddress, network)) + universalVals[i] = sdk.AccAddress([]byte(fmt.Sprintf("universal-validator-%d", i))).String() + } + + for i, val := range validators { + accAddr, err := sdk.ValAddressFromBech32(val.OperatorAddress) + require.NoError(t, err) + + coreValAddr := sdk.AccAddress(accAddr) + uniValAddr := sdk.MustAccAddressFromBech32(universalVals[i]) + + auth := authz.NewGenericAuthorization(sdk.MsgTypeURL(&uexecutortypes.MsgVoteInbound{})) + exp := ctx.BlockTime().Add(time.Hour) + require.NoError(t, chainApp.AuthzKeeper.SaveGrant(ctx, uniValAddr, coreValAddr, auth, &exp)) + } + + ueModuleAccAddress, _ := chainApp.UexecutorKeeper.GetUeModuleAddress(ctx) + receipt, err := chainApp.UexecutorKeeper.DeployUEAV2(ctx, ueModuleAccAddress, &uexecutortypes.UniversalAccountId{ + ChainNamespace: "eip155", + ChainId: "11155111", + Owner: testAddress, + }) + require.NoError(t, err) + ueaAddr := common.BytesToAddress(receipt.Ret) + + fundCoins := sdk.NewCoins(sdk.NewInt64Coin("upc", 1_000_000_000)) + require.NoError(t, chainApp.BankKeeper.MintCoins(ctx, utils.MintModule, fundCoins)) + require.NoError(t, chainApp.BankKeeper.SendCoinsFromModuleToAccount( + ctx, utils.MintModule, sdk.AccAddress(ueaAddr.Bytes()), fundCoins)) + + return chainApp, ctx, universalVals, validators, ueaAddr +} + +// multicallInbound builds a non-CEA inbound whose payload is the given +// multicall. The UE module is the caller of executeUniversalTx, so UEA_EVM +// skips signature verification and VerificationData is irrelevant here. +func multicallInbound(txHash string, txType uexecutortypes.TxType, amount, payloadData string) *uexecutortypes.Inbound { + return &uexecutortypes.Inbound{ + SourceChain: "eip155:11155111", + TxHash: txHash, + Sender: utils.GetDefaultAddresses().DefaultTestAddr, + Recipient: "", + Amount: amount, + AssetAddr: utils.GetDefaultAddresses().ExternalUSDCAddr.String(), + LogIndex: "1", + TxType: txType, + UniversalPayload: &uexecutortypes.UniversalPayload{ + To: utils.GetDefaultAddresses().UniversalGatewayPCAddr.Hex(), + Value: "0", + Data: payloadData, + GasLimit: "21000000", + MaxFeePerGas: "1000000000", + MaxPriorityFeePerGas: "200000000", + Nonce: "0", + Deadline: "0", + VType: uexecutortypes.VerificationType(1), + }, + VerificationData: "", + } +} + +// payloadPcTx returns the payload PcTx, which is always the last one recorded. +func payloadPcTx(t *testing.T, utx uexecutortypes.UniversalTx) *uexecutortypes.PCTx { + t.Helper() + require.NotEmpty(t, utx.PcTx, "at least one PcTx must be recorded") + return utx.PcTx[len(utx.PcTx)-1] +} + +func requireNoPendingOutbounds(t *testing.T, ctx sdk.Context, chainApp *app.ChainApp, msg string) { + t.Helper() + querier := uexecutorkeeper.NewQuerier(chainApp.UexecutorKeeper) + resp, err := querier.AllPendingOutbounds(ctx, &uexecutortypes.QueryAllPendingOutboundsRequest{}) + require.NoError(t, err) + require.Empty(t, resp.Entries, msg) +} + +func TestInboundMulticallOutboundAtomicity(t *testing.T) { + prc20 := utils.GetDefaultAddresses().PRC20USDCAddr + gateway := utils.GetDefaultAddresses().UniversalGatewayPCAddr + + // --- the headline case ------------------------------------------------ + + t.Run("FUNDS_AND_PAYLOAD one invalid sibling rolls the whole payload back", func(t *testing.T) { + chainApp, ctx, vals, coreVals, ueaAddr := setupMulticallOutboundTest(t, 4) + + ueaAcc := sdk.AccAddress(ueaAddr.Bytes()) + upcBefore := chainApp.BankKeeper.GetBalance(ctx, ueaAcc, "upc") + + // One valid outbound and one unregistered-PRC20 sibling, in that order, + // so the valid one is already accumulated when the invalid one fails. + inbound := multicallInbound("0xmulticall-funds-01", uexecutortypes.TxType_FUNDS_AND_PAYLOAD, "1000000", + multicallToGateway(t, prc20, unregisteredPRC20)) + voteToQuorum(t, ctx, chainApp, vals, coreVals, inbound) + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.True(t, found) + + // Nothing the payload did survives — including the burn behind the + // valid leg, witnessed by the gateway's outbound nonce. + require.Equal(t, common.Hash{}, chainApp.EVMKeeper.GetState(ctx, gateway, gatewayNonceSlot), + "the gateway burn must roll back with the failed attach") + require.Empty(t, utx.OutboundTx, "a partially-valid multicall must not leave a partial OutboundTx") + requireNoPendingOutbounds(t, ctx, chainApp, "a partially-valid multicall must not leave a PendingOutbounds row") + require.Equal(t, upcBefore.Amount, chainApp.BankKeeper.GetBalance(ctx, ueaAcc, "upc").Amount, + "no gas fee may be collected for a payload that was discarded") + + // The failure is recorded, not swallowed. + pcTx := payloadPcTx(t, utx) + require.Equal(t, "FAILED", pcTx.Status, "the payload PcTx must not report SUCCESS") + require.Contains(t, pcTx.ErrorMsg, "outbound attach failed") + require.Contains(t, strings.ToLower(pcTx.ErrorMsg), strings.ToLower(unregisteredPRC20.Hex()), + "the PcTx must name the leg that could not be resolved") + require.Empty(t, utx.RevertError, "RevertError must no longer be used to swallow attach failures") + + // The deposit happens before the payload cache, so the bridged funds + // stay credited to the UEA and the user can simply retry. + require.Equal(t, "SUCCESS", utx.PcTx[0].Status, "the deposit stays committed") + require.Equal(t, "1000000", prc20BalanceOf(t, chainApp, ctx, ueaAddr).String(), + "the bridged principal must remain with the UEA") + }) + + t.Run("GAS_AND_PAYLOAD one invalid sibling rolls the whole payload back", func(t *testing.T) { + chainApp, ctx, vals, coreVals, ueaAddr := setupMulticallOutboundTest(t, 4) + + ueaAcc := sdk.AccAddress(ueaAddr.Bytes()) + upcBefore := chainApp.BankKeeper.GetBalance(ctx, ueaAcc, "upc") + + // Amount 0 skips gasAndPayloadDepositAutoSwap, which needs a live + // Uniswap quoter/router the integration harness does not deploy. The + // UEA payload branch under test is reached either way. + inbound := multicallInbound("0xmulticall-gas-01", uexecutortypes.TxType_GAS_AND_PAYLOAD, "0", + multicallToGateway(t, prc20, unregisteredPRC20)) + voteToQuorum(t, ctx, chainApp, vals, coreVals, inbound) + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.True(t, found) + + require.Equal(t, common.Hash{}, chainApp.EVMKeeper.GetState(ctx, gateway, gatewayNonceSlot), + "the gateway burn must roll back with the failed attach") + require.Empty(t, utx.OutboundTx, "a partially-valid multicall must not leave a partial OutboundTx") + requireNoPendingOutbounds(t, ctx, chainApp, "a partially-valid multicall must not leave a PendingOutbounds row") + require.Equal(t, upcBefore.Amount, chainApp.BankKeeper.GetBalance(ctx, ueaAcc, "upc").Amount, + "no gas fee may be collected for a payload that was discarded") + + pcTx := payloadPcTx(t, utx) + require.Equal(t, "FAILED", pcTx.Status, "the payload PcTx must not report SUCCESS") + require.Contains(t, pcTx.ErrorMsg, "outbound attach failed") + require.Contains(t, strings.ToLower(pcTx.ErrorMsg), strings.ToLower(unregisteredPRC20.Hex()), + "the PcTx must name the leg that could not be resolved") + require.Empty(t, utx.RevertError, "RevertError must no longer be used to swallow attach failures") + }) + + // --- happy path: every leg valid -------------------------------------- + + t.Run("FUNDS_AND_PAYLOAD all-valid multicall attaches every outbound", func(t *testing.T) { + chainApp, ctx, vals, coreVals, _ := setupMulticallOutboundTest(t, 4) + + inbound := multicallInbound("0xmulticall-funds-02", uexecutortypes.TxType_FUNDS_AND_PAYLOAD, "1000000", + multicallToGateway(t, prc20, prc20)) + voteToQuorum(t, ctx, chainApp, vals, coreVals, inbound) + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.True(t, found) + + pcTx := payloadPcTx(t, utx) + require.Equal(t, "SUCCESS", pcTx.Status, "payload should succeed: %s", pcTx.ErrorMsg) + + require.Equal(t, common.BigToHash(big.NewInt(2)), chainApp.EVMKeeper.GetState(ctx, gateway, gatewayNonceSlot), + "both gateway burns must be committed") + require.Len(t, utx.OutboundTx, 2, "each valid leg must produce an OutboundTx") + + seen := map[string]bool{} + for _, out := range utx.OutboundTx { + require.Equal(t, "eip155:11155111", out.DestinationChain) + require.Equal(t, common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"), common.HexToAddress(out.Recipient)) + require.Equal(t, "1000000", out.Amount) + require.Equal(t, prc20, common.HexToAddress(out.Prc20AssetAddr)) + require.Equal(t, utils.GetDefaultAddresses().ExternalUSDCAddr, common.HexToAddress(out.ExternalAssetAddr)) + require.Equal(t, uexecutortypes.Status_PENDING, out.OutboundStatus) + + require.False(t, seen[out.Id], "each leg must get its own outbound id") + seen[out.Id] = true + + entry, err := chainApp.UexecutorKeeper.PendingOutbounds.Get(ctx, out.Id) + require.NoError(t, err, "every outbound must be indexed in PendingOutbounds") + require.Equal(t, utxKey, entry.UniversalTxId) + } + require.Empty(t, utx.RevertError) + }) + + t.Run("GAS_AND_PAYLOAD all-valid multicall attaches every outbound", func(t *testing.T) { + chainApp, ctx, vals, coreVals, _ := setupMulticallOutboundTest(t, 4) + + inbound := multicallInbound("0xmulticall-gas-02", uexecutortypes.TxType_GAS_AND_PAYLOAD, "0", + multicallToGateway(t, prc20, prc20)) + voteToQuorum(t, ctx, chainApp, vals, coreVals, inbound) + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.True(t, found) + + pcTx := payloadPcTx(t, utx) + require.Equal(t, "SUCCESS", pcTx.Status, "payload should succeed: %s", pcTx.ErrorMsg) + + require.Equal(t, common.BigToHash(big.NewInt(2)), chainApp.EVMKeeper.GetState(ctx, gateway, gatewayNonceSlot), + "both gateway burns must be committed") + require.Len(t, utx.OutboundTx, 2, "each valid leg must produce an OutboundTx") + + for _, out := range utx.OutboundTx { + require.Equal(t, uexecutortypes.Status_PENDING, out.OutboundStatus) + entry, err := chainApp.UexecutorKeeper.PendingOutbounds.Get(ctx, out.Id) + require.NoError(t, err, "every outbound must be indexed in PendingOutbounds") + require.Equal(t, utxKey, entry.UniversalTxId) + } + require.Empty(t, utx.RevertError) + }) + + // --- regression: payloads that emit no gateway outbound ---------------- + + t.Run("payload without a gateway call still succeeds with no outbound rows", func(t *testing.T) { + chainApp, ctx, vals, coreVals, ueaAddr := setupMulticallOutboundTest(t, 4) + + // A plain PRC20 transfer from the UEA: real EVM work, zero gateway logs. + inbound := multicallInbound("0xmulticall-noop-01", uexecutortypes.TxType_FUNDS_AND_PAYLOAD, "1000000", + "0xa9059cbb000000000000000000000000527f3692f5c53cfa83f7689885995606f93b616400000000000000000000000000000000000000000000000000000000000f4240") + inbound.UniversalPayload.To = utils.GetDefaultAddresses().PRC20USDCAddr.Hex() + voteToQuorum(t, ctx, chainApp, vals, coreVals, inbound) + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.True(t, found) + + pcTx := payloadPcTx(t, utx) + require.Equal(t, "SUCCESS", pcTx.Status, "payload should succeed: %s", pcTx.ErrorMsg) + require.Empty(t, pcTx.ErrorMsg) + + require.Empty(t, utx.OutboundTx, "a payload that emits no gateway event must not gain an outbound") + requireNoPendingOutbounds(t, ctx, chainApp, "no spurious PendingOutbounds row") + require.Empty(t, utx.RevertError) + + // The transfer itself committed, so the cache was written. + require.Equal(t, "0", prc20BalanceOf(t, chainApp, ctx, ueaAddr).String(), + "the payload's PRC20 transfer must still be committed") + }) +} + +// prc20BalanceOf reads PRC20USDC.balanceOf(holder). +func prc20BalanceOf(t *testing.T, chainApp *app.ChainApp, ctx sdk.Context, holder common.Address) *big.Int { + t.Helper() + + prc20ABI, err := uexecutortypes.ParsePRC20ABI() + require.NoError(t, err) + + ueModuleAccAddress, _ := chainApp.UexecutorKeeper.GetUeModuleAddress(ctx) + res, err := chainApp.EVMKeeper.CallEVM( + ctx, + chainApp.EVMKeeper.NewStateDB(ctx), + prc20ABI, + ueModuleAccAddress, + utils.GetDefaultAddresses().PRC20USDCAddr, + false, + false, + nil, + "balanceOf", + holder, + ) + require.NoError(t, err) + + values, err := prc20ABI.Unpack("balanceOf", res.Ret) + require.NoError(t, err) + require.Len(t, values, 1) + + return values[0].(*big.Int) +} diff --git a/test/integration/uexecutor/inbound_revert_abort_test.go b/test/integration/uexecutor/inbound_revert_abort_test.go new file mode 100644 index 000000000..90a10e463 --- /dev/null +++ b/test/integration/uexecutor/inbound_revert_abort_test.go @@ -0,0 +1,158 @@ +package integrationtest + +import ( + "math/big" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app" + utils "github.com/pushchain/push-chain-node/test/utils" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// Regression coverage for F-2026-18823. +// +// buildRevertOutbound used to fail open: when it could not resolve the revert's +// gas metadata it logged "proceeding without gas fields" and returned the +// outbound anyway, still marked PENDING. attachOutboundsToUtx then indexed it +// into PendingOutbounds unconditionally, where the universal validators refused +// to sign it ("gas price is zero or missing"). The row could never leave the +// queue: no ballot forms for an unsignable outbound and there is no admin abort +// for outbounds. Worse, non-CEA rescue was gated on an INBOUND_REVERT having +// reached REVERTED, so the user had no recovery route either. +// +// The revert is now recorded ABORTED with a reason, kept off the signing queue, +// and accepted by the rescue gate. +// +// NOTE ON THIS ENVIRONMENT: the UniversalCore contract deployed by the test +// harness cannot serve getOutboundTxGasAndFees (its PRC20 stub has no +// SOURCE_CHAIN_NAMESPACE), so every INBOUND_REVERT built here takes the abort +// path. That makes the failure realistic end-to-end but means the resolvable +// path cannot be exercised at this level; it is covered by +// x/uexecutor/keeper/build_revert_outbound_test.go, which drives the same +// function with the gas lookup mocked both ways. + +// requireNotQueuedForSigning asserts that an outbound was never indexed into +// PendingOutbounds, i.e. it will not be picked up for TSS signing. +func requireNotQueuedForSigning(t *testing.T, chainApp *app.ChainApp, ctx sdk.Context, outboundId string) { + t.Helper() + has, err := chainApp.UexecutorKeeper.PendingOutbounds.Has(ctx, outboundId) + require.NoError(t, err) + require.False(t, has, + "outbound %s must not be indexed in PendingOutbounds: it can never be signed and nothing would ever remove it", outboundId) +} + +// findInboundRevert returns the INBOUND_REVERT outbound on a UTX, if any. +func findInboundRevert(utx uexecutortypes.UniversalTx) *uexecutortypes.OutboundTx { + for _, ob := range utx.OutboundTx { + if ob != nil && ob.TxType == uexecutortypes.TxType_INBOUND_REVERT { + return ob + } + } + return nil +} + +// driveNonCEAInboundToAbortedRevert votes a non-CEA FUNDS inbound with an empty +// recipient to quorum. Execution validation rejects it, so an INBOUND_REVERT is +// built — and since the harness cannot serve gas metadata, that revert aborts. +// +// The token/chain config is deliberately left registered so the failure is the +// gas lookup alone; the PRC20-not-found variant is covered in +// vote_inbound_validation_test.go. +func driveNonCEAInboundToAbortedRevert(t *testing.T, txHash string) (*app.ChainApp, sdk.Context, string) { + t.Helper() + + chainApp, ctx, vals, inbound, coreVals := setupInboundBridgeTest(t, 4) + inbound.TxHash = txHash + inbound.IsCEA = false + inbound.Recipient = "" // FUNDS requires a recipient — fails ValidateForExecution post-quorum + + for i := 0; i < 3; i++ { + valAddr, err := sdk.ValAddressFromBech32(coreVals[i].OperatorAddress) + require.NoError(t, err) + require.NoError(t, utils.ExecVoteInbound(t, ctx, chainApp, vals[i], sdk.AccAddress(valAddr).String(), inbound)) + } + + return chainApp, ctx, uexecutortypes.GetInboundUniversalTxKey(*inbound) +} + +// TestInboundRevert_UnresolvableGasMetadata_AbortsInsteadOfQueueing is the +// headline regression test: the revert must be recorded ABORTED with a reason +// and must never reach PendingOutbounds. +func TestInboundRevert_UnresolvableGasMetadata_AbortsInsteadOfQueueing(t *testing.T) { + chainApp, ctx, utxId := driveNonCEAInboundToAbortedRevert(t, "0xabortrevert01") + + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxId) + require.NoError(t, err) + require.True(t, found, "UTX must exist after quorum") + + revert := findInboundRevert(utx) + require.NotNil(t, revert, "a failed non-CEA inbound must still record an INBOUND_REVERT attempt") + + require.Equal(t, uexecutortypes.Status_ABORTED, revert.OutboundStatus, + "a revert whose gas metadata could not be resolved must be ABORTED, never PENDING") + require.NotEmpty(t, revert.AbortReason, "the abort reason must say why the revert could not be built") + require.Contains(t, revert.AbortReason, "gas fee info", + "the reason must name the lookup that failed") + + // Fail-closed: the gas fields stay empty rather than being half-written. + require.Empty(t, revert.GasToken) + require.Empty(t, revert.GasFee) + require.Empty(t, revert.GasPrice) + require.Empty(t, revert.GasLimit) + + requireNotQueuedForSigning(t, chainApp, ctx, revert.Id) + + // The whole queue stays clean, not just this id. + err = chainApp.UexecutorKeeper.PendingOutbounds.Walk(ctx, nil, func(id string, _ uexecutortypes.PendingOutboundEntry) (bool, error) { + t.Fatalf("PendingOutbounds must be empty, found %s", id) + return true, nil + }) + require.NoError(t, err) +} + +// TestInboundRevert_AbortedRevert_UnlocksRescue proves the other half of the +// fix: skipping the queue is not enough on its own, because non-CEA rescue used +// to require a REVERTED inbound-revert. An ABORTED one must now be accepted, or +// the user is left with a clean queue and no way out. +func TestInboundRevert_AbortedRevert_UnlocksRescue(t *testing.T) { + chainApp, ctx, utxId := driveNonCEAInboundToAbortedRevert(t, "0xabortrevert02") + + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxId) + require.NoError(t, err) + require.True(t, found) + revert := findInboundRevert(utx) + require.NotNil(t, revert) + require.Equal(t, uexecutortypes.Status_ABORTED, revert.OutboundStatus, + "precondition: the revert must have aborted for this test to mean anything") + + prc20Addr := utils.GetDefaultAddresses().PRC20USDCAddr + senderAddr := common.HexToAddress(utils.GetDefaultAddresses().DefaultTestAddr) + log := buildRescueFundsLog(t, utxId, prc20Addr, senderAddr, + "eip155", big.NewInt(333), big.NewInt(1_000_000_000), big.NewInt(200_000)) + + err = chainApp.UexecutorKeeper.AttachRescueOutboundFromReceipt( + ctx, + makeRescueReceipt(t, "0xrescueafterabort", log), + uexecutortypes.PCTx{TxHash: "0xrescueafterabort", Status: "SUCCESS"}, + ) + require.NoError(t, err, "rescue must be accepted when the auto-revert aborted; the funds never came back") + + utx, _, err = chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxId) + require.NoError(t, err) + + rescue := findRescueOutbound(utx) + require.NotNil(t, rescue, "a RESCUE_FUNDS outbound must be attached") + require.Equal(t, uexecutortypes.Status_PENDING, rescue.OutboundStatus, + "the rescue itself is signable and must be queued") + require.Equal(t, "333", rescue.GasFee) + + // The rescue is queued; the aborted revert still is not. + has, err := chainApp.UexecutorKeeper.PendingOutbounds.Has(ctx, rescue.Id) + require.NoError(t, err) + require.True(t, has, "the rescue outbound must be indexed for UV pickup") + requireNotQueuedForSigning(t, chainApp, ctx, revert.Id) +} diff --git a/test/integration/uexecutor/inbound_reverted_payload_gas_test.go b/test/integration/uexecutor/inbound_reverted_payload_gas_test.go new file mode 100644 index 000000000..9b1a7ca58 --- /dev/null +++ b/test/integration/uexecutor/inbound_reverted_payload_gas_test.go @@ -0,0 +1,50 @@ +package integrationtest + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + utils "github.com/pushchain/push-chain-node/test/utils" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// TestInboundRevertedPayloadBillsGas covers F-2026-18824 rec 2: a reverted inbound +// payload used to pay no gas at all. The UEA is funded with upc by the setup and the +// deposit is a PRC20, so upc moves for exactly one reason here — gas. +func TestInboundRevertedPayloadBillsGas(t *testing.T) { + prc20 := utils.GetDefaultAddresses().PRC20USDCAddr + + chainApp, ctx, vals, coreVals, ueaAddr := setupMulticallOutboundTest(t, 4) + ueaAcc := sdk.AccAddress(ueaAddr.Bytes()) + upcBefore := chainApp.BankKeeper.GetBalance(ctx, ueaAcc, "upc") + + // 0xdeadbeef matches no selector on the PRC20, which has no fallback -> revert. + inbound := multicallInbound("0xreverted-payload-gas-01", uexecutortypes.TxType_FUNDS_AND_PAYLOAD, "1000000", "0xdeadbeef") + inbound.UniversalPayload.To = prc20.Hex() + + voteToQuorum(t, ctx, chainApp, vals, coreVals, inbound) + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKey) + require.NoError(t, err) + require.True(t, found, "the inbound must be recorded even though its payload reverted") + + // Guard the premise: a payload that stopped reverting, or never ran, would make + // the balance assertion below meaningless. + pcTx := payloadPcTx(t, utx) + require.Equal(t, "FAILED", pcTx.Status, "the payload must have reverted for this test to mean anything") + require.NotContains(t, pcTx.ErrorMsg, "depositAutoSwap failed", + "the payload must be what failed, not the funding step before it") + + upcAfter := chainApp.BankKeeper.GetBalance(ctx, ueaAcc, "upc") + + // The fix. + require.True(t, upcAfter.Amount.LT(upcBefore.Amount), + "a reverted payload must still be billed for the gas it burned (before=%s, after=%s)", + upcBefore.Amount, upcAfter.Amount) + + // Billing is clamped to the available balance. + require.False(t, upcAfter.Amount.IsNegative(), "billing must never drive the UEA balance negative") +} diff --git a/test/integration/uexecutor/module_nonce_test.go b/test/integration/uexecutor/module_nonce_test.go index 87d4c7de6..e2bf960ec 100644 --- a/test/integration/uexecutor/module_nonce_test.go +++ b/test/integration/uexecutor/module_nonce_test.go @@ -1,18 +1,212 @@ package integrationtest import ( + "math/big" "strings" "testing" sdk "github.com/cosmos/cosmos-sdk/types" + evmtypes "github.com/cosmos/evm/x/vm/types" "github.com/ethereum/go-ethereum/common" "github.com/mr-tron/base58" "github.com/stretchr/testify/require" + "github.com/pushchain/push-chain-node/app" utils "github.com/pushchain/push-chain-node/test/utils" uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" ) +// F-2026-18189 — Manual Module EVM Nonce Desync on Reverted Inbound Execution. +// +// Every module-sender DerivedEVMCall in x/uexecutor supplies a *manual* nonce. +// x/vm turns that nonce into the derived transaction's identity: +// +// ethtypes.NewTx(&DynamicFeeTx{Nonce, GasFeeCap, GasTipCap, Gas, To, Value, Data}) +// -> tx.Hash() -> txConfig.TxHash -> res.Hash / the ethereum_tx event attribute +// +// so the nonce is the only field that distinguishes two otherwise byte-identical +// module calls. Two properties therefore have to hold at once, and these two tests +// pin one each: +// +// 1. the nonce the module hands to x/vm must not drift away from the module +// account's own EVM nonce when an attempt fails (Hacken's reported defect), and +// 2. a nonce must be burned by every *attempt*, not just by every committed +// success — otherwise a retry after a failed attempt reproduces a derived tx +// hash that has already been emitted in this block. +// +// (2) is why the naive "read evm.GetNonce(module) immediately before each call and +// drop the counter" fix cannot be shipped: x/vm only advances a sender's nonce for +// a CREATE (state_transition.go bumps it in the contractCreation branch only), so +// for the plain CALLs the module makes, evm.GetNonce(module) is a constant and +// every byte-identical call would collide. The module has to advance that nonce +// itself, unconditionally. + +// moduleDerivedTxHashes returns the ethereum_tx hashes emitted on ctx, in order. +// A derived tx that dies before execution (see the gas-estimation note in +// TestModuleSenderNonceDistinctHashesAcrossFailedAttempt) emits nothing, so this +// is also how the tests tell "attempted and emitted" from "attempted and dropped". +func moduleDerivedTxHashes(ctx sdk.Context) []string { + var out []string + for _, ev := range ctx.EventManager().Events() { + if ev.Type != evmtypes.EventTypeEthereumTx { + continue + } + for _, attr := range ev.Attributes { + if attr.Key == evmtypes.AttributeKeyEthereumTxHash { + out = append(out, attr.Value) + } + } + } + return out +} + +// moduleNonceState reports the two values that must never diverge: the persisted +// uexecutor counter that feeds the manual nonce, and the module account's own EVM +// nonce that x/vm and eth_getTransactionCount read. +func moduleNonceState(t *testing.T, chainApp *app.ChainApp, ctx sdk.Context) (counter, evmNonce uint64) { + t.Helper() + + counter, err := chainApp.UexecutorKeeper.GetModuleAccountNonce(ctx) + require.NoError(t, err) + + moduleAddr, _ := chainApp.UexecutorKeeper.GetUeModuleAddress(ctx) + return counter, chainApp.EVMKeeper.GetNonce(ctx, moduleAddr) +} + +// callDepositPRC20 issues one module-sender depositPRC20Token through the real +// keeper entry point, on an isolated event manager so the caller sees only the +// ethereum_tx events this one call produced. +func callDepositPRC20( + t *testing.T, + chainApp *app.ChainApp, + ctx sdk.Context, + prc20, to common.Address, + amount *big.Int, +) (hashes []string, err error) { + t.Helper() + + callCtx := ctx.WithEventManager(sdk.NewEventManager()) + _, err = chainApp.UexecutorKeeper.CallPRC20Deposit(callCtx, prc20, to, amount) + return moduleDerivedTxHashes(callCtx), err +} + +// TestModuleSenderNonceSurvivesRevertedDeposit is Hacken's stated case: a +// depositPRC20Token that fails must not leave the module's nonce bookkeeping in a +// state that breaks the *next* module-sender call. +// +// The forced failure is a deposit of a PRC20 address that has no code. The +// UniversalCore handler makes a high-level call into it, which reverts. +func TestModuleSenderNonceSurvivesRevertedDeposit(t *testing.T) { + chainApp, ctx, _ := utils.SetAppWithValidators(t) + + prc20 := utils.GetDefaultAddresses().PRC20USDCAddr + // No contract is ever deployed here, so depositPRC20Token reverts on it. + codelessPRC20 := common.HexToAddress("0x000000000000000000000000000000000000dEaD") + recipient := common.HexToAddress("0x0000000000000000000000000000000000001234") + amount := big.NewInt(1_000_000) + + // Sanity: the module account is the EVM sender for all of these calls. + moduleAddr, _ := chainApp.UexecutorKeeper.GetUeModuleAddress(ctx) + require.Equal(t, + sdk.AccAddress(moduleAddr.Bytes()), + chainApp.AccountKeeper.GetModuleAccount(ctx, uexecutortypes.ModuleName).GetAddress(), + ) + + counter, evmNonce := moduleNonceState(t, chainApp, ctx) + require.Equal(t, counter, evmNonce, "module nonce must start in sync") + + // A committed success first, so the reverted attempt below is not the very + // first thing the module ever does. + _, err := callDepositPRC20(t, chainApp, ctx, prc20, recipient, amount) + require.NoError(t, err, "baseline deposit must succeed") + + counter, evmNonce = moduleNonceState(t, chainApp, ctx) + require.Equal(t, counter, evmNonce, "module nonce must stay in sync after a committed deposit") + + beforeRevertCounter, _ := moduleNonceState(t, chainApp, ctx) + + // The reverted deposit. The inbound executors swallow this error and return + // nil, so on-chain nothing else reacts to it — whatever it leaves behind in + // the nonce bookkeeping is what the next call has to live with. + _, err = callDepositPRC20(t, chainApp, ctx, codelessPRC20, recipient, amount) + require.Error(t, err, "depositing a codeless PRC20 must fail") + + afterRevertCounter, afterRevertEvmNonce := moduleNonceState(t, chainApp, ctx) + + // The next module-sender call — Hacken's reported impact is that this one is + // blocked by the nonce the failed attempt left behind. + _, err = callDepositPRC20(t, chainApp, ctx, prc20, recipient, amount) + require.NoError(t, err, "a module-sender call after a reverted one must still succeed") + + // The failed attempt must still have consumed its nonce. A nonce handed back + // on failure is a nonce a byte-identical retry can re-use, and the derived tx + // hash is a pure function of the nonce and the calldata — see + // TestModuleSenderNonceDistinctHashesAcrossFailedAttempt. + require.Equal(t, beforeRevertCounter+1, afterRevertCounter, + "a failed module-sender attempt must still burn its nonce") + + // ...and this is the drift itself: the counter that feeds the manual nonce + // and the module account's own EVM nonce must still agree. + require.Equal(t, afterRevertCounter, afterRevertEvmNonce, + "reverted module call left the manual nonce counter drifted from the module account's EVM nonce") + + counter, evmNonce = moduleNonceState(t, chainApp, ctx) + require.Equal(t, counter, evmNonce, + "module nonce must be back in sync after the follow-up deposit") +} + +// TestModuleSenderNonceDistinctHashesAcrossFailedAttempt is the residual that +// decides the design (recommendation 4 in the write-up). +// +// Making the module account's EVM nonce the source of truth *without* advancing +// it removes the drift, but re-introduces the bug the counter was added for: the +// derived tx hash is a pure function of {Nonce, GasFeeCap, GasTipCap, Gas, To, +// Value, Data}, so byte-identical module calls collide. A failed attempt is the +// sharpest case — it commits nothing at all — but on this EVM fork plain +// successes collide too, because x/vm never advances a CALL sender's nonce. +// +// Note on the failed attempt: a module-sender call passes gasLimit == nil, so +// DerivedEVMCallWithData runs EstimateGasInternal first. For an always-reverting +// call that returns EstimateGasResponse{Gas: 0, VmError: "execution reverted"}, +// and the call then dies in ApplyMessageWithConfig with "intrinsic gas too low" +// before any ethereum_tx event is emitted. So the failed attempt has no hash of +// its own to compare — what it must still do is consume a nonce, so that the +// byte-identical call after it cannot reproduce the hash of the byte-identical +// call before it. +func TestModuleSenderNonceDistinctHashesAcrossFailedAttempt(t *testing.T) { + chainApp, ctx, _ := utils.SetAppWithValidators(t) + + prc20 := utils.GetDefaultAddresses().PRC20USDCAddr + codelessPRC20 := common.HexToAddress("0x000000000000000000000000000000000000dEaD") + recipient := common.HexToAddress("0x0000000000000000000000000000000000001234") + amount := big.NewInt(1_000_000) + + // Three byte-identical calls — same contract, same value, same calldata, same + // gas limit — with a failed attempt wedged between the first and the second. + first, err := callDepositPRC20(t, chainApp, ctx, prc20, recipient, amount) + require.NoError(t, err) + require.Len(t, first, 1, "a committed module deposit must emit exactly one ethereum_tx") + + failed, err := callDepositPRC20(t, chainApp, ctx, codelessPRC20, recipient, amount) + require.Error(t, err, "depositing a codeless PRC20 must fail") + require.Empty(t, failed, "a module call that dies in gas estimation emits no ethereum_tx") + + second, err := callDepositPRC20(t, chainApp, ctx, prc20, recipient, amount) + require.NoError(t, err) + require.Len(t, second, 1) + + third, err := callDepositPRC20(t, chainApp, ctx, prc20, recipient, amount) + require.NoError(t, err) + require.Len(t, third, 1) + + require.NotEqual(t, first[0], second[0], + "byte-identical module calls separated by a failed attempt produced the same derived tx hash") + require.NotEqual(t, second[0], third[0], + "consecutive byte-identical module calls produced the same derived tx hash") + require.NotEqual(t, first[0], third[0], + "byte-identical module calls produced the same derived tx hash") +} + // TestIsUeModuleAddress verifies the module-address check compares raw 20-byte // values (VM-native identity), so hex casing / EIP-55 checksum never matters and // a re-parsed lower/upper-case form of the module address still resolves as the diff --git a/test/integration/uexecutor/outbound_payload_cap_test.go b/test/integration/uexecutor/outbound_payload_cap_test.go new file mode 100644 index 000000000..207df7aa8 --- /dev/null +++ b/test/integration/uexecutor/outbound_payload_cap_test.go @@ -0,0 +1,81 @@ +package integrationtest + +import ( + "math/big" + "strings" + "testing" + + evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + utils "github.com/pushchain/push-chain-node/test/utils" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// gatewayOutboundLog builds a UniversalTxOutbound log carrying payloadBytes, +// matching what DecodeUniversalTxOutboundFromLog expects. +func gatewayOutboundLog(t *testing.T, prc20 common.Address, payloadBytes []byte) *evmtypes.Log { + t.Helper() + + strT, _ := abi.NewType("string", "", nil) + bytesT, _ := abi.NewType("bytes", "", nil) + u256T, _ := abi.NewType("uint256", "", nil) + addrT, _ := abi.NewType("address", "", nil) + u8T, _ := abi.NewType("uint8", "", nil) + + args := abi.Arguments{ + {Type: strT}, {Type: bytesT}, {Type: u256T}, {Type: addrT}, {Type: u256T}, + {Type: u256T}, {Type: bytesT}, {Type: u256T}, {Type: addrT}, {Type: u8T}, {Type: u256T}, + } + target := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + data, err := args.Pack( + "eip155:11155111", target.Bytes(), big.NewInt(1000000), + common.Address{}, big.NewInt(500000), big.NewInt(21000), + payloadBytes, big.NewInt(0), target, uint8(1), big.NewInt(1), + ) + require.NoError(t, err) + + return &evmtypes.Log{ + Address: strings.ToLower(utils.GetDefaultAddresses().UniversalGatewayPCAddr.Hex()), + Topics: []string{ + uexecutortypes.UniversalTxOutboundEventSig, + common.HexToHash("0x01").Hex(), + common.HexToHash("0x02").Hex(), + common.BytesToHash(prc20.Bytes()).Hex(), + }, + Data: data, + Index: 0, + } +} + +// F-2026-18146: the gateway payload is attacker-controlled and lands in state, +// so BuildOutboundsFromReceipt must reject an oversized one at admission. +func TestBuildOutboundsFromReceipt_RejectsOversizedPayload(t *testing.T) { + prc20 := utils.GetDefaultAddresses().PRC20USDCAddr + chainApp, ctx, _, _, _ := setupMulticallOutboundTest(t, 4) + + t.Run("a payload within the cap is accepted", func(t *testing.T) { + receipt := &evmtypes.MsgEthereumTxResponse{ + Hash: "0xabc", + Logs: []*evmtypes.Log{gatewayOutboundLog(t, prc20, []byte{0xde, 0xad, 0xbe, 0xef})}, + } + outbounds, err := chainApp.UexecutorKeeper.BuildOutboundsFromReceipt(ctx, "utx-ok", receipt) + require.NoError(t, err) + require.Len(t, outbounds, 1) + }) + + t.Run("an oversized payload is rejected", func(t *testing.T) { + // Hex-encoded, so half the cap in bytes is exactly at it; one more byte is over. + oversized := make([]byte, uexecutortypes.MaxOutboundPayloadBytes/2) + receipt := &evmtypes.MsgEthereumTxResponse{ + Hash: "0xabc", + Logs: []*evmtypes.Log{gatewayOutboundLog(t, prc20, oversized)}, + } + outbounds, err := chainApp.UexecutorKeeper.BuildOutboundsFromReceipt(ctx, "utx-big", receipt) + require.Error(t, err) + require.Contains(t, err.Error(), "payload too large") + require.Empty(t, outbounds, "no outbound may be built from an oversized payload") + }) +} diff --git a/test/integration/uexecutor/payload_size_cap_test.go b/test/integration/uexecutor/payload_size_cap_test.go new file mode 100644 index 000000000..ba6e99e00 --- /dev/null +++ b/test/integration/uexecutor/payload_size_cap_test.go @@ -0,0 +1,225 @@ +package integrationtest + +import ( + "strings" + "testing" + + "cosmossdk.io/collections" + sdk "github.com/cosmos/cosmos-sdk/types" + authz "github.com/cosmos/cosmos-sdk/x/authz" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app" + utils "github.com/pushchain/push-chain-node/test/utils" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// hexBlobOfLen returns a lowercase 0x-prefixed blob exactly n characters long. +// Canonicalize leaves an even-bodied lowercase hex blob untouched, so the +// length the keeper sees is the length built here. +func hexBlobOfLen(n int) string { + body := strings.Repeat("ab", (n-2)/2) + if len(body)+2 < n { + body += "c" + } + return "0x" + body +} + +// universalPayloadOfSize builds a valid payload whose serialized size is +// exactly want bytes. +func universalPayloadOfSize(t *testing.T, want int) *uexecutortypes.UniversalPayload { + t.Helper() + + // tag byte + 3-byte varint length for every size used here. + const dataOverhead = 4 + + for _, nonce := range []string{"1", "11"} { + p := &uexecutortypes.UniversalPayload{ + To: utils.GetDefaultAddresses().HandlerAddr.Hex(), + Nonce: nonce, + } + dataLen := want - p.Size() - dataOverhead + if dataLen < 2 || dataLen%2 != 0 { + continue + } + p.Data = "0x" + strings.Repeat("ab", (dataLen-2)/2) + if p.Size() == want { + return p + } + } + + t.Fatalf("could not build a universal payload of exactly %d bytes", want) + return nil +} + +// TestVoteInboundPayloadSizeCap covers the vote half of the 128 KiB universal +// payload cap. +// +// The vote arrives wrapped in an authz.MsgExec — that is what the universal +// validator broadcasts (universalClient/pushsigner/pushsigner.go wrapWithAuthZ) +// and what utils.ExecVoteInbound reproduces. authz.MsgExec carries no +// ValidateBasic of its own, so baseapp does not reach the inner msg at CheckTx; +// the inner ValidateBasic runs later, inside authz's Exec msg server, and the +// keeper check runs after that. Both are exercised here. +func TestVoteInboundPayloadSizeCap(t *testing.T) { + usdcAddress := utils.GetDefaultAddresses().ExternalUSDCAddr + testAddress := utils.GetDefaultAddresses().DefaultTestAddr + + newInbound := func(txHash, rawPayload string) *uexecutortypes.Inbound { + return &uexecutortypes.Inbound{ + SourceChain: "eip155:11155111", + TxHash: txHash, + Sender: testAddress, + Amount: "1000000", + AssetAddr: usdcAddress.String(), + LogIndex: "1", + TxType: uexecutortypes.TxType_FUNDS_AND_PAYLOAD, + RawPayload: rawPayload, + } + } + + utxKeyOf := func(in *uexecutortypes.Inbound) string { + canon := *in + canon.Canonicalize() + return uexecutortypes.GetInboundUniversalTxKey(canon) + } + + t.Run("raw payload at the cap is voted on", func(t *testing.T) { + chainApp, ctx, vals, coreVals, _ := setupInboundValidationTest(t, 4) + + raw := hexBlobOfLen(uexecutortypes.MaxUniversalPayloadBytes) + require.Len(t, raw, uexecutortypes.MaxUniversalPayloadBytes) + + inbound := newInbound("0xatcap01", raw) + + valAddr, err := sdk.ValAddressFromBech32(coreVals[0].OperatorAddress) + require.NoError(t, err) + voteErr := utils.ExecVoteInbound(t, ctx, chainApp, vals[0], sdk.AccAddress(valAddr).String(), inbound) + + // State first: the vote was recorded, so the cap did not reject it. + entry, err := chainApp.UexecutorKeeper.PendingInbounds.Get(ctx, utxKeyOf(inbound)) + require.NoError(t, err, "a vote at the cap must be recorded") + require.Len(t, entry.Variants, 1) + require.Len(t, entry.Variants[0].Inbound.RawPayload, uexecutortypes.MaxUniversalPayloadBytes) + + require.NoError(t, voteErr) + }) + + t.Run("raw payload one byte over the cap is rejected on the authz path", func(t *testing.T) { + chainApp, ctx, vals, coreVals, _ := setupInboundValidationTest(t, 4) + + raw := hexBlobOfLen(uexecutortypes.MaxUniversalPayloadBytes + 1) + require.Len(t, raw, uexecutortypes.MaxUniversalPayloadBytes+1) + + inbound := newInbound("0xovercap01", raw) + + valAddr, err := sdk.ValAddressFromBech32(coreVals[0].OperatorAddress) + require.NoError(t, err) + voteErr := utils.ExecVoteInbound(t, ctx, chainApp, vals[0], sdk.AccAddress(valAddr).String(), inbound) + + // State first: nothing about this inbound reached consensus state. + _, err = chainApp.UexecutorKeeper.PendingInbounds.Get(ctx, utxKeyOf(inbound)) + require.ErrorIs(t, err, collections.ErrNotFound, "an oversized vote must not write PendingInbounds") + + _, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxKeyOf(inbound)) + require.NoError(t, err) + require.False(t, found, "an oversized vote must not create a UniversalTx") + + require.Error(t, voteErr) + require.Contains(t, voteErr.Error(), "raw_payload too large") + require.Contains(t, voteErr.Error(), "131073 bytes exceeds the 131072 byte limit") + }) + + t.Run("keeper rejects an oversized vote without any msg validation", func(t *testing.T) { + chainApp, ctx, _, coreVals, _ := setupInboundValidationTest(t, 4) + + inbound := newInbound("0xovercap02", hexBlobOfLen(uexecutortypes.MaxUniversalPayloadBytes+1)) + + valAddr, err := sdk.ValAddressFromBech32(coreVals[0].OperatorAddress) + require.NoError(t, err) + + // Straight into the keeper, so nothing but the keeper's own check can + // reject it. + voteErr := chainApp.UexecutorKeeper.VoteInbound(ctx, valAddr, *inbound) + + _, err = chainApp.UexecutorKeeper.PendingInbounds.Get(ctx, utxKeyOf(inbound)) + require.ErrorIs(t, err, collections.ErrNotFound, "the keeper must not write PendingInbounds for an oversized vote") + + require.Error(t, voteErr) + require.Contains(t, voteErr.Error(), "raw_payload too large") + }) +} + +// TestExecutePayloadSizeCap covers the direct half of the cap: MsgExecutePayload +// is fee exempt (app/txpolicy/gasless.go), so the payload it carries is not +// priced anywhere and only the size cap bounds it. +func TestExecutePayloadSizeCap(t *testing.T) { + testAddress := utils.GetDefaultAddresses().DefaultTestAddr + // A real 20-byte account, since MsgExecutePayload rejects any other length. + signerAcc := sdk.AccAddress(common.HexToAddress(testAddress).Bytes()) + signer := signerAcc.String() + + newMsg := func(payload *uexecutortypes.UniversalPayload) *uexecutortypes.MsgExecutePayload { + return &uexecutortypes.MsgExecutePayload{ + Signer: signer, + UniversalAccountId: &uexecutortypes.UniversalAccountId{ + ChainNamespace: "eip155", + ChainId: "11155111", + Owner: testAddress, + }, + UniversalPayload: payload, + VerificationData: "0x", + } + } + + execViaAuthz := func(chainApp *app.ChainApp, ctx sdk.Context, payload *uexecutortypes.UniversalPayload) error { + execMsg := authz.NewMsgExec(signerAcc, []sdk.Msg{newMsg(payload)}) + _, err := chainApp.AuthzKeeper.Exec(ctx, &execMsg) + return err + } + + t.Run("payload over the cap is rejected on the authz path", func(t *testing.T) { + chainApp, ctx, _, _, _ := setupInboundValidationTest(t, 1) + + oversized := universalPayloadOfSize(t, uexecutortypes.MaxUniversalPayloadBytes+1) + require.Equal(t, uexecutortypes.MaxUniversalPayloadBytes+1, oversized.Size()) + + err := execViaAuthz(chainApp, ctx, oversized) + require.Error(t, err) + require.Contains(t, err.Error(), "universal payload too large") + require.Contains(t, err.Error(), "131073 bytes exceeds the 131072 byte limit") + }) + + t.Run("keeper rejects an oversized payload without any msg validation", func(t *testing.T) { + chainApp, ctx, _, _, _ := setupInboundValidationTest(t, 1) + + oversized := universalPayloadOfSize(t, uexecutortypes.MaxUniversalPayloadBytes+1) + + // Straight into the keeper, so nothing but the keeper's own check can + // reject it — and before any EVM work. + err := chainApp.UexecutorKeeper.ExecutePayload( + ctx, + common.HexToAddress(testAddress), + newMsg(oversized).UniversalAccountId, + oversized, + "0x", + ) + require.Error(t, err) + require.Contains(t, err.Error(), "universal payload too large") + }) + + t.Run("payload at the cap passes the size gate", func(t *testing.T) { + chainApp, ctx, _, _, _ := setupInboundValidationTest(t, 1) + + atCap := universalPayloadOfSize(t, uexecutortypes.MaxUniversalPayloadBytes) + require.Equal(t, uexecutortypes.MaxUniversalPayloadBytes, atCap.Size()) + require.NoError(t, newMsg(atCap).ValidateBasic()) + + // Execution may still fail further down (this signer has no deployed + // UEA); what matters is that it is never the size gate. + if err := execViaAuthz(chainApp, ctx, atCap); err != nil { + require.NotContains(t, err.Error(), "too large") + } + }) +} diff --git a/test/integration/uexecutor/rescue_funds_test.go b/test/integration/uexecutor/rescue_funds_test.go index ec9501398..86d015785 100644 --- a/test/integration/uexecutor/rescue_funds_test.go +++ b/test/integration/uexecutor/rescue_funds_test.go @@ -4,6 +4,8 @@ import ( "encoding/hex" "fmt" "math/big" + "sort" + "strings" "testing" "time" @@ -18,7 +20,9 @@ import ( "github.com/pushchain/push-chain-node/app" utils "github.com/pushchain/push-chain-node/test/utils" chainutils "github.com/pushchain/push-chain-node/utils" + uexecutorkeeper "github.com/pushchain/push-chain-node/x/uexecutor/keeper" uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" + uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types" ) // buildRescueFundsLog constructs a synthetic evmtypes.Log that looks exactly like a @@ -71,8 +75,22 @@ func buildRescueFundsLog( } } -// setupRescueFundsTest creates a CEA inbound whose deposit will fail (asset address has -// no registered token config), drives it to quorum, and returns the UTX key of the failed UTX. +// Asset of the stuck deposit built by setupRescueFundsTest: a pETH-style 18-decimal +// token, registered in uregistry but whose PRC20 has no deployed contract — so the +// deposit fails while the asset still resolves through the registry. +// +// The 18-decimal choice is deliberate: the default token registered by the CEA setup is +// 6-decimal USDC, so the two form the 18 → 6 pair where a cross-asset rescue amplifies. +// 1e18 raw of an 18-decimal token is one whole token; reinterpreted as 6-decimal USDC the +// same raw integer is a claim on 10^12 whole USDC. +var ( + rescueOriginalAsset = common.HexToAddress("0x000000000000000000000000000000000000DEAD") + rescueOriginalPRC20 = common.HexToAddress("0x0000000000000000000000000000000000000eE1") + rescueOriginalAmount = "1000000000000000000" // 1e18 raw = 1 whole 18-decimal token +) + +// setupRescueFundsTest creates a CEA inbound whose deposit will fail (its PRC20 has no +// deployed contract), drives it to quorum, and returns the UTX key of the failed UTX. // The returned UTX has at least one FAILED PCTx and is ready for a rescue outbound. func setupRescueFundsTest( t *testing.T, @@ -91,21 +109,37 @@ func setupRescueFundsTest( testAddress := utils.GetDefaultAddresses().DefaultTestAddr recipient := utils.GetDefaultAddresses().TargetAddr2 - // Use an asset address that has no registered token config — depositPRC20 will fail. - unregisteredAsset := common.HexToAddress("0x000000000000000000000000000000000000DEAD") + + // Register the stuck asset. A rescue derives its asset from the original inbound, so + // the original asset must resolve; the deposit still fails because rescueOriginalPRC20 + // has no contract deployed at it, leaving the funds stuck on the source chain — which + // is exactly the situation rescue exists for. + require.NoError(t, chainApp.UregistryKeeper.AddTokenConfig(ctx, &uregistrytypes.TokenConfig{ + Chain: "eip155:11155111", + Address: rescueOriginalAsset.String(), + Name: "Push Ether", + Symbol: "pETH", + Decimals: 18, + Enabled: true, + LiquidityCap: "1000000000000000000000000", + TokenType: 1, + NativeRepresentation: &uregistrytypes.NativeRepresentation{ + ContractAddress: rescueOriginalPRC20.String(), + }, + })) inbound := &uexecutortypes.Inbound{ SourceChain: "eip155:11155111", TxHash: "0xrescue01", Sender: testAddress, Recipient: recipient, - Amount: "1000000", - AssetAddr: unregisteredAsset.String(), + Amount: rescueOriginalAmount, + AssetAddr: rescueOriginalAsset.String(), LogIndex: "1", TxType: uexecutortypes.TxType_FUNDS_AND_PAYLOAD, UniversalPayload: &uexecutortypes.UniversalPayload{ To: recipient, - Value: "1000000", + Value: rescueOriginalAmount, Data: "0x", GasLimit: "21000000", MaxFeePerGas: "1000000000", @@ -134,7 +168,7 @@ func setupRescueFundsTest( require.True(t, found, "UTX must exist after quorum") require.NotEmpty(t, utx.PcTx, "setup: at least one PCTx must exist") - require.Equal(t, "FAILED", utx.PcTx[0].Status, "setup: deposit must fail for unregistered asset") + require.Equal(t, "FAILED", utx.PcTx[0].Status, "setup: deposit must fail so the funds stay stuck") return chainApp, ctx, vals, utxId, coreVals } @@ -149,7 +183,12 @@ func makeRescueReceipt(t *testing.T, txHash string, log *evmtypes.Log) *evmtypes } func TestRescueFunds(t *testing.T) { - prc20Addr := utils.GetDefaultAddresses().PRC20USDCAddr + // A rescue event must name the PRC20 registered for the stuck inbound's OWN asset, so + // each subtest uses the PRC20 belonging to whichever setup it built its inbound from: + // setupRescueFundsTest stakes the 18-decimal pETH, the bridge/CEA-payload setups the + // 6-decimal USDC. + prc20Addr := rescueOriginalPRC20 + usdcPRC20Addr := utils.GetDefaultAddresses().PRC20USDCAddr senderAddr := common.HexToAddress(utils.GetDefaultAddresses().DefaultTestAddr) t.Run("rescue outbound is attached to original UTX on valid CEA inbound with failed deposit", func(t *testing.T) { @@ -172,9 +211,16 @@ func TestRescueFunds(t *testing.T) { require.Equal(t, uexecutortypes.Status_PENDING, rescueObs.OutboundStatus) require.Equal(t, uexecutortypes.TxType_RESCUE_FUNDS, rescueObs.TxType) require.Equal(t, "eip155:11155111", rescueObs.DestinationChain) - require.Equal(t, "1000000", rescueObs.Amount) + require.Equal(t, rescueOriginalAmount, rescueObs.Amount) require.Equal(t, "111", rescueObs.GasFee) + // The asset is the original inbound's, derived from the registry — never the + // caller's. Amount and asset must describe the same deposit. + require.Equal(t, rescueOriginalAsset.String(), rescueObs.ExternalAssetAddr, + "rescue must carry the original inbound's external asset") + require.Equal(t, rescueOriginalPRC20.String(), rescueObs.Prc20AssetAddr, + "rescue must carry the PRC20 registered for the original asset") + // The rescue call must be recorded as a PCTx in the UTX history. // UTX already had the failed deposit PCTx; the rescue pcTx is appended after it. require.Greater(t, len(utx.PcTx), 1, "rescue PCTx must be appended to UTX history") @@ -183,6 +229,106 @@ func TestRescueFunds(t *testing.T) { require.Equal(t, "SUCCESS", lastPcTx.Status) }) + // --- F-2026-18177: the rescue asset is derived, never supplied ---------------- + + t.Run("rescue naming a different registered PRC20 than the original asset is rejected", func(t *testing.T) { + // The headline case. The stuck deposit is 1e18 raw of an 18-decimal token; the + // rescue event names 6-decimal USDC, which is registered on the same chain and so + // passes every "is this a real PRC20" check. The amount always comes from the + // original inbound, so honouring the caller's PRC20 would emit an outbound paying + // 1e18 base units of USDC — 10^12 whole USDC — for a stuck deposit of one pETH. + chainApp, ctx, _, utxId, _ := setupRescueFundsTest(t, 4) + + pendingBefore := pendingOutboundIds(t, ctx, chainApp) + + log := buildRescueFundsLog(t, utxId, usdcPRC20Addr, senderAddr, + "eip155", big.NewInt(111), big.NewInt(1_000_000_000), big.NewInt(200_000)) + err := chainApp.UexecutorKeeper.AttachRescueOutboundFromReceipt(ctx, + makeRescueReceipt(t, "0xrescuetx13", log), + uexecutortypes.PCTx{TxHash: "0xrescuetx13", Status: "SUCCESS"}) + + // State is asserted before the error: if the derivation regresses, the call + // succeeds and the substituted outbound shows up here, rather than the subtest + // aborting on the require.Error below and never reaching these checks. + utx, found, getErr := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxId) + require.NoError(t, getErr) + require.True(t, found) + require.Nil(t, findRescueOutbound(utx), + "no rescue outbound may be created when the event names another asset") + require.Equal(t, pendingBefore, pendingOutboundIds(t, ctx, chainApp), + "a rejected cross-asset rescue must not add a PendingOutbounds row") + + require.Error(t, err) + require.Contains(t, err.Error(), "does not match") + }) + + t.Run("rescue for an original asset with no registered token config is rejected", func(t *testing.T) { + chainApp, ctx, _, utxId, _ := setupRescueFundsTest(t, 4) + + // Point the stored inbound at an asset uregistry knows nothing about. An + // unregistered original asset must be an error, not an opening to substitute + // whichever asset the caller happens to name. + unregistered := common.HexToAddress("0x000000000000000000000000000000000000BEEF") + require.NoError(t, chainApp.UexecutorKeeper.UpdateUniversalTx(ctx, utxId, + func(utx *uexecutortypes.UniversalTx) error { + utx.InboundTx.AssetAddr = unregistered.String() + return nil + })) + + pendingBefore := pendingOutboundIds(t, ctx, chainApp) + + log := buildRescueFundsLog(t, utxId, prc20Addr, senderAddr, + "eip155", big.NewInt(111), big.NewInt(1_000_000_000), big.NewInt(200_000)) + err := chainApp.UexecutorKeeper.AttachRescueOutboundFromReceipt(ctx, + makeRescueReceipt(t, "0xrescuetx14", log), + uexecutortypes.PCTx{TxHash: "0xrescuetx14", Status: "SUCCESS"}) + + utx, _, getErr := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxId) + require.NoError(t, getErr) + require.Nil(t, findRescueOutbound(utx), + "no rescue outbound may be created for an unregistered original asset") + require.Equal(t, pendingBefore, pendingOutboundIds(t, ctx, chainApp), + "a rejected rescue must not add a PendingOutbounds row") + + require.Error(t, err) + require.Contains(t, err.Error(), "no token config registered for original asset") + }) + + t.Run("rescue PRC20 comparison ignores address casing", func(t *testing.T) { + // The event's PRC20 is always EIP-55 checksummed by the log decoder, while the + // registry stores whatever an admin registered. Comparing canonically means a + // lowercase registry entry is still the same PRC20. + chainApp, ctx, _, utxId, _ := setupRescueFundsTest(t, 4) + + require.NoError(t, chainApp.UregistryKeeper.UpdateTokenConfig(ctx, &uregistrytypes.TokenConfig{ + Chain: "eip155:11155111", + Address: rescueOriginalAsset.String(), + Name: "Push Ether", + Symbol: "pETH", + Decimals: 18, + Enabled: true, + LiquidityCap: "1000000000000000000000000", + TokenType: 1, + NativeRepresentation: &uregistrytypes.NativeRepresentation{ + ContractAddress: strings.ToLower(rescueOriginalPRC20.String()), + }, + })) + + log := buildRescueFundsLog(t, utxId, rescueOriginalPRC20, senderAddr, + "eip155", big.NewInt(111), big.NewInt(1_000_000_000), big.NewInt(200_000)) + err := chainApp.UexecutorKeeper.AttachRescueOutboundFromReceipt(ctx, + makeRescueReceipt(t, "0xrescuetx15", log), + uexecutortypes.PCTx{TxHash: "0xrescuetx15", Status: "SUCCESS"}) + require.NoError(t, err) + + utx, _, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxId) + require.NoError(t, err) + rescueOb := findRescueOutbound(utx) + require.NotNil(t, rescueOb) + require.Equal(t, strings.ToLower(rescueOriginalPRC20.String()), rescueOb.Prc20AssetAddr, + "the registry's spelling of the PRC20 is what lands on the outbound") + }) + t.Run("rescue outbound recipient defaults to inbound sender when no revert instructions", func(t *testing.T) { chainApp, ctx, _, utxId, _ := setupRescueFundsTest(t, 4) @@ -222,11 +368,11 @@ func TestRescueFunds(t *testing.T) { } utxId := uexecutortypes.GetInboundUniversalTxKey(*inbound) - log := buildRescueFundsLog(t, utxId, prc20Addr, senderAddr, + log := buildRescueFundsLog(t, utxId, usdcPRC20Addr, senderAddr, "eip155", big.NewInt(111), big.NewInt(1_000_000_000), big.NewInt(200_000)) err := chainApp.UexecutorKeeper.AttachRescueOutboundFromReceipt(ctx, makeRescueReceipt(t, "0xrescuetx03", log), uexecutortypes.PCTx{TxHash: "0xrescuetx03", Status: "SUCCESS"}) require.Error(t, err) - require.Contains(t, err.Error(), "no reverted inbound-revert outbound") + require.Contains(t, err.Error(), "no reverted or aborted inbound-revert outbound") }) t.Run("rescue is rejected for non-CEA inbound when auto-revert is PENDING", func(t *testing.T) { @@ -251,11 +397,11 @@ func TestRescueFunds(t *testing.T) { }) require.NoError(t, err) - log := buildRescueFundsLog(t, utxId, prc20Addr, senderAddr, + log := buildRescueFundsLog(t, utxId, usdcPRC20Addr, senderAddr, "eip155", big.NewInt(111), big.NewInt(1_000_000_000), big.NewInt(200_000)) err = chainApp.UexecutorKeeper.AttachRescueOutboundFromReceipt(ctx, makeRescueReceipt(t, "0xrescuetx03b", log), uexecutortypes.PCTx{TxHash: "0xrescuetx03b", Status: "SUCCESS"}) require.Error(t, err) - require.Contains(t, err.Error(), "no reverted inbound-revert outbound") + require.Contains(t, err.Error(), "no reverted or aborted inbound-revert outbound") }) t.Run("rescue succeeds for non-CEA inbound with reverted auto-revert", func(t *testing.T) { @@ -280,7 +426,7 @@ func TestRescueFunds(t *testing.T) { }) require.NoError(t, err) - log := buildRescueFundsLog(t, utxId, prc20Addr, senderAddr, + log := buildRescueFundsLog(t, utxId, usdcPRC20Addr, senderAddr, "eip155", big.NewInt(222), big.NewInt(1_000_000_000), big.NewInt(200_000)) err = chainApp.UexecutorKeeper.AttachRescueOutboundFromReceipt(ctx, makeRescueReceipt(t, "0xrescuetx03c", log), uexecutortypes.PCTx{TxHash: "0xrescuetx03c", Status: "SUCCESS"}) require.NoError(t, err) @@ -317,7 +463,7 @@ func TestRescueFunds(t *testing.T) { // Confirm first PCTx (deposit) succeeded — that's the invariant we rely on. require.Equal(t, "SUCCESS", utx.PcTx[0].Status, "deposit must have succeeded for this test to be meaningful") - log := buildRescueFundsLog(t, utxId, prc20Addr, senderAddr, + log := buildRescueFundsLog(t, utxId, usdcPRC20Addr, senderAddr, "eip155", big.NewInt(111), big.NewInt(1_000_000_000), big.NewInt(200_000)) err = chainApp.UexecutorKeeper.AttachRescueOutboundFromReceipt(ctx, makeRescueReceipt(t, "0xrescuetx04", log), uexecutortypes.PCTx{TxHash: "0xrescuetx04", Status: "SUCCESS"}) require.Error(t, err) @@ -587,6 +733,21 @@ func TestRescueFunds(t *testing.T) { }) } +// pendingOutboundIds returns the sorted outbound IDs currently in the PendingOutbounds +// index, so a test can assert that a rejected rescue left the index untouched. +func pendingOutboundIds(t *testing.T, ctx sdk.Context, chainApp *app.ChainApp) []string { + t.Helper() + querier := uexecutorkeeper.NewQuerier(chainApp.UexecutorKeeper) + resp, err := querier.AllPendingOutbounds(ctx, &uexecutortypes.QueryAllPendingOutboundsRequest{}) + require.NoError(t, err) + ids := make([]string, 0, len(resp.Entries)) + for _, e := range resp.Entries { + ids = append(ids, e.OutboundId) + } + sort.Strings(ids) + return ids +} + // findRescueOutbound returns the first RESCUE_FUNDS outbound from a UTX, or nil. func findRescueOutbound(utx uexecutortypes.UniversalTx) *uexecutortypes.OutboundTx { for _, ob := range utx.OutboundTx { diff --git a/test/integration/uexecutor/revert_stuck_inbound_test.go b/test/integration/uexecutor/revert_stuck_inbound_test.go index d5dc8e8cc..c574fcf1a 100644 --- a/test/integration/uexecutor/revert_stuck_inbound_test.go +++ b/test/integration/uexecutor/revert_stuck_inbound_test.go @@ -89,6 +89,266 @@ func seedBallot(t *testing.T, chainApp *app.ChainApp, ctx sdk.Context, inbound * })) } +// seedPendingBallotWithVotes stores a PENDING ballot carrying a real +// eligible-voter list and per-voter vote slots, which seedBallot deliberately +// leaves empty. The F-2026-18147 scenarios all turn on whether any eligible +// voter still holds a NOT_YET_VOTED slot, so they need the populated shape. +// +// The voter strings are never resolved against the staking set on this path — +// RevertStuckInbound only reads Status/EligibleVoters/Votes off the ballot. +func seedPendingBallotWithVotes( + t *testing.T, + chainApp *app.ChainApp, + ctx sdk.Context, + inbound *uexecutortypes.Inbound, + status uvalidatortypes.BallotStatus, + voters []string, + votes []uvalidatortypes.VoteResult, + threshold int64, +) { + t.Helper() + require.Len(t, votes, len(voters), "each eligible voter needs exactly one vote slot") + ballotKey, err := uexecutortypes.GetInboundBallotKey(*inbound) + require.NoError(t, err) + require.NoError(t, chainApp.UvalidatorKeeper.Ballots.Set(ctx, ballotKey, uvalidatortypes.Ballot{ + Id: ballotKey, + BallotType: uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_INBOUND_TX, + EligibleVoters: voters, + Votes: votes, + VotingThreshold: threshold, + Status: status, + BlockHeightCreated: 1, + BlockHeightExpiry: 100_000_000, + })) + require.NoError(t, chainApp.UvalidatorKeeper.ActiveBallotIDs.Set(ctx, ballotKey)) +} + +// threeVoters is the eligible-voter list shared by the F-2026-18147 scenarios. +func threeVoters() []string { + return []string{"cosmosvaloper1aaa", "cosmosvaloper1bbb", "cosmosvaloper1ccc"} +} + +// TestRevertStuckInbound_PendingUnreachable_ThresholdMet_CreatesRevertOutbound +// is the headline F-2026-18147 case. +// +// RecomputeBallotQuorum preserves the votes of still-eligible voters, lowers the +// threshold, and returns PENDING without ever calling CheckIfFinalizingVote. The +// shape reproduced here is what that leaves behind in the worst case: every +// eligible voter has voted YES and the preserved YES count already clears the +// recomputed threshold, so the ballot *should* have passed — but Ballot.AddVote +// rejects repeat votes, so no further vote can ever be cast and nothing will +// move it off PENDING. Natural expiry is 100M blocks away. +// +// Before this fix the admin hatch required EXPIRED, and recompute only expires a +// ballot at zero eligible voters, so the deposit was stranded permanently. +func TestRevertStuckInbound_PendingUnreachable_ThresholdMet_CreatesRevertOutbound(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + }, + 2, // YES (3) already clears the recomputed threshold + ) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + resp, err := ms.RevertStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgRevertStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.NoError(t, err, "an unreachable PENDING ballot must be revertible") + require.NotEmpty(t, resp.UtxId) + require.NotEmpty(t, resp.OutboundId) + + // --- UTX assertions --- + utx, _, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, resp.UtxId) + require.NoError(t, err) + require.Equal(t, uexecutortypes.GetInboundUniversalTxKey(*inbound), utx.Id) + require.NotNil(t, utx.InboundTx) + require.Equal(t, inbound.TxHash, utx.InboundTx.TxHash) + + require.Len(t, utx.PcTx, 1) + require.Equal(t, "FAILED", utx.PcTx[0].Status) + require.Contains(t, utx.PcTx[0].ErrorMsg, "unreachable", + "the audit trail must record WHY the hatch opened, not the expired wording") + + // --- Revert outbound assertions --- + require.Len(t, utx.OutboundTx, 1) + ob := utx.OutboundTx[0] + require.Equal(t, resp.OutboundId, ob.Id) + require.Equal(t, uexecutortypes.TxType_INBOUND_REVERT, ob.TxType) + // The harness's UniversalCore stub cannot serve getOutboundTxGasAndFees, so the + // revert's gas metadata is unresolvable and F-2026-18823 records it ABORTED + // rather than queueing it for a signature it could never receive. What this test + // pins is that the unreachable-PENDING hatch BUILDS the revert at all; the + // resolvable (PENDING) path is covered by + // x/uexecutor/keeper/build_revert_outbound_test.go. + require.Equal(t, uexecutortypes.Status_ABORTED, ob.OutboundStatus, + "a revert with unresolvable gas metadata must be ABORTED, not PENDING") + require.NotEmpty(t, ob.AbortReason, "ABORTED revert must record why it could not be built") + require.Equal(t, inbound.SourceChain, ob.DestinationChain) + require.Equal(t, inbound.RevertInstructions.FundRecipient, ob.Recipient) + require.Equal(t, inbound.Amount, ob.Amount) + require.Equal(t, inbound.AssetAddr, ob.ExternalAssetAddr) + + // --- PendingOutbounds index --- + // An ABORTED revert must stay out of the signing queue: no ballot can form for + // it and there is no admin abort for outbounds, so an indexed row would be + // permanently stuck. + has, err := chainApp.UexecutorKeeper.PendingOutbounds.Has(ctx, ob.Id) + require.NoError(t, err) + require.False(t, has, "an ABORTED revert must not be indexed in PendingOutbounds") +} + +// TestRevertStuckInbound_PendingUnreachable_BelowThreshold_Accepted covers the +// second stuck shape: every eligible voter has voted, but the YES count never +// reached the threshold and the NO count never reached it either, so +// IsFinalizingVote fires for neither branch. Reachable without any recompute at +// all — 3 voters, threshold 3, one dissenting FAILURE vote. +// +// Unreachability, not vote arithmetic, is the predicate; both shapes qualify. +func TestRevertStuckInbound_PendingUnreachable_BelowThreshold_Accepted(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_FAILURE, + }, + 3, // YES (2) < 3, NO (1) < 3 → neither branch of IsFinalizingVote fires + ) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + resp, err := ms.RevertStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgRevertStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.NoError(t, err, "a fully-voted PENDING ballot below threshold is equally unreachable") + + utx, _, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, resp.UtxId) + require.NoError(t, err) + require.Len(t, utx.OutboundTx, 1) + require.Equal(t, uexecutortypes.TxType_INBOUND_REVERT, utx.OutboundTx[0].TxType) + + // Unresolvable gas metadata in this harness means the revert is ABORTED and so + // deliberately not queued (F-2026-18823); the hatch opening is what matters here. + require.Equal(t, uexecutortypes.Status_ABORTED, utx.OutboundTx[0].OutboundStatus) + has, err := chainApp.UexecutorKeeper.PendingOutbounds.Has(ctx, utx.OutboundTx[0].Id) + require.NoError(t, err) + require.False(t, has, "an ABORTED revert must not be indexed in PendingOutbounds") +} + +// TestRevertStuckInbound_PendingWithUnvotedVoter_Refused is the guard against +// widening the hatch too far. +// +// This ballot is deliberately the most tempting possible refusal: the YES votes +// already clear the threshold, so it *looks* exactly like the headline case. It +// is not — one eligible voter still holds a NOT_YET_VOTED slot, so a single +// normal VoteOnBallot finalizes it through the proper VoteInbound pipeline, +// which mints and executes rather than refunding. Admin revert must not race +// that. This is also the shape Hacken's no-code workaround produces: add an +// eligible UV, recompute, and the new voter arrives NOT_YET_VOTED. +func TestRevertStuckInbound_PendingWithUnvotedVoter_Refused(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_NOT_YET_VOTED, + }, + 2, // YES (2) already meets threshold — still refused, it can finalize normally + ) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.RevertStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgRevertStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.Error(t, err, "a PENDING ballot with an unvoted eligible voter can still finalize; admin revert must refuse it") + require.Contains(t, err.Error(), "admin revert requires EXPIRED") + + // The refusal must be total: no UTX, so no revert outbound can be signed. + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + has, hErr := chainApp.UexecutorKeeper.HasUniversalTx(ctx, utxKey) + require.NoError(t, hErr) + require.False(t, has, "a refused revert must not leave a UniversalTx behind") +} + +// TestRevertStuckInbound_RejectedBallot_FullyVoted_StillRefused re-pins the +// F-2026-18801 refusal against the new predicate. +// +// A REJECTED ballot is fully voted by construction, so the "every eligible voter +// has voted" test on its own would let it through. It must not: REJECTED means a +// supermajority affirmatively voted the observation invalid, and refunding would +// pay out of the TSS vault against a deposit the validator set concluded never +// happened. PENDING-unreachable is the opposite case — nobody can act at all. +// The status guard in IsUnreachablePending is what keeps them apart. +func TestRevertStuckInbound_RejectedBallot_FullyVoted_StillRefused(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_REJECTED, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_FAILURE, + uvalidatortypes.VoteResult_VOTE_RESULT_FAILURE, + uvalidatortypes.VoteResult_VOTE_RESULT_FAILURE, + }, + 2, + ) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.RevertStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgRevertStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.Error(t, err, "REJECTED stays refused however its vote slots are filled (F-2026-18801)") + require.Contains(t, err.Error(), "admin revert requires EXPIRED") + + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + has, hErr := chainApp.UexecutorKeeper.HasUniversalTx(ctx, utxKey) + require.NoError(t, hErr) + require.False(t, has, "a refused revert must not leave a UniversalTx behind") +} + +// TestRevertStuckInbound_ExpiredBallot_FullyVoted_StillAccepted keeps the +// original precondition intact under the new switch: EXPIRED is accepted on its +// status alone, and still records the expired wording rather than the +// unreachable-pending wording. +func TestRevertStuckInbound_ExpiredBallot_FullyVoted_StillAccepted(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedPendingBallotWithVotes(t, chainApp, ctx, inbound, + uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, + threeVoters(), + []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_NOT_YET_VOTED, + uvalidatortypes.VoteResult_VOTE_RESULT_NOT_YET_VOTED, + }, + 3, + ) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + resp, err := ms.RevertStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgRevertStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.NoError(t, err) + + utx, _, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, resp.UtxId) + require.NoError(t, err) + require.Len(t, utx.PcTx, 1) + require.Contains(t, utx.PcTx[0].ErrorMsg, "expired") + require.Len(t, utx.OutboundTx, 1) + require.Equal(t, uexecutortypes.TxType_INBOUND_REVERT, utx.OutboundTx[0].TxType) +} + func TestRevertStuckInbound_HappyPath_ExpiredBallot_CreatesRevertOutbound(t *testing.T) { chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) seedBallot(t, chainApp, ctx, inbound, uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED) @@ -125,7 +385,15 @@ func TestRevertStuckInbound_HappyPath_ExpiredBallot_CreatesRevertOutbound(t *tes require.Equal(t, uexecutortypes.GetOutboundRevertId(inbound.SourceChain, inbound.TxHash, inbound.LogIndex), ob.Id, "outbound id must follow the canonical revert-id format") require.Equal(t, uexecutortypes.TxType_INBOUND_REVERT, ob.TxType, "outbound type must be INBOUND_REVERT") - require.Equal(t, uexecutortypes.Status_PENDING, ob.OutboundStatus, "outbound must start PENDING so UVs sign it") + // The harness's UniversalCore stub cannot serve getOutboundTxGasAndFees, so the + // revert's gas metadata is unresolvable and it is recorded ABORTED rather than + // queued for a signature it could never receive. The admin message still reports + // the outbound it created, and the UTX becomes eligible for RESCUE_FUNDS. The + // resolvable (PENDING) path is covered by + // x/uexecutor/keeper/build_revert_outbound_test.go. + require.Equal(t, uexecutortypes.Status_ABORTED, ob.OutboundStatus, + "a revert with unresolvable gas metadata must be ABORTED, not PENDING") + require.NotEmpty(t, ob.AbortReason, "ABORTED revert must record why it could not be built") require.Equal(t, inbound.SourceChain, ob.DestinationChain, "revert goes back to the source chain") require.Equal(t, inbound.RevertInstructions.FundRecipient, ob.Recipient, "recipient must use RevertInstructions.FundRecipient when set") @@ -134,10 +402,12 @@ func TestRevertStuckInbound_HappyPath_ExpiredBallot_CreatesRevertOutbound(t *tes require.Equal(t, chainutils.LenientCanonicalizeEVMAddress(inbound.Sender), ob.Sender, "sender field carries original depositor") // --- PendingOutbounds index assertions --- - pending, err := chainApp.UexecutorKeeper.PendingOutbounds.Get(ctx, ob.Id) - require.NoError(t, err, "revert outbound must be indexed in PendingOutbounds for UV pickup") - require.Equal(t, ob.Id, pending.OutboundId) - require.Equal(t, utx.Id, pending.UniversalTxId) + // An ABORTED revert must stay out of the signing queue: no ballot can ever form + // for it and there is no admin abort for outbounds, so an indexed row would be + // permanently stuck. + has, err := chainApp.UexecutorKeeper.PendingOutbounds.Has(ctx, ob.Id) + require.NoError(t, err) + require.False(t, has, "an ABORTED revert must not be indexed in PendingOutbounds") } // TestRevertStuckInbound_RecipientFallback_UsesSender covers the case where @@ -297,3 +567,42 @@ func TestRevertStuckInbound_RecomputeThenRevert_E2E(t *testing.T) { require.Len(t, utx.OutboundTx, 1) require.Equal(t, uexecutortypes.TxType_INBOUND_REVERT, utx.OutboundTx[0].TxType) } + +// TestRevertStuckInbound_RejectedBallot_RefusedDeliberately pins the refusal +// documented for F-2026-18801. +// +// The terminal-routing hook files BOTH terminal-failure statuses into +// ExpiredInbounds, but the admin hatch accepts only EXPIRED. That asymmetry is +// intentional, and this test exists so a future change cannot quietly relax it: +// +// - EXPIRED is uncertainty. Quorum never formed, the deposit may be real, the +// funds may be stuck in the source gateway. Refunding is correct. +// - REJECTED is a supermajority asserting the observation is invalid. A revert +// outbound there would pay out of the TSS vault against a deposit the +// validator set concluded never happened. +// +// Note this state is unreachable for inbounds today (VoteOnInboundBallot +// hardcodes VOTE_RESULT_SUCCESS, so threshold-FAILURE never fires); the ballot is +// seeded directly here precisely because no vote path can produce it. If inbound +// negative voting is ever added, this test is the place the design decision has +// to be re-made rather than inherited. +func TestRevertStuckInbound_RejectedBallot_RefusedDeliberately(t *testing.T) { + chainApp, ctx, inbound, admin := setupRevertStuckInbound(t) + seedBallot(t, chainApp, ctx, inbound, uvalidatortypes.BallotStatus_BALLOT_STATUS_REJECTED) + + ms := uexecutorkeeper.NewMsgServerImpl(chainApp.UexecutorKeeper) + _, err := ms.RevertStuckInbound(sdk.WrapSDKContext(ctx), &uexecutortypes.MsgRevertStuckInbound{ + Signer: admin, + Inbound: inbound, + }) + require.Error(t, err, "admin revert must refuse a REJECTED ballot") + require.Contains(t, err.Error(), "admin revert requires EXPIRED", + "the refusal must name the required status so an operator knows why") + + // The refusal must be total: no UTX, and therefore no revert outbound that + // could later be signed and broadcast. + utxKey := uexecutortypes.GetInboundUniversalTxKey(*inbound) + has, hErr := chainApp.UexecutorKeeper.HasUniversalTx(ctx, utxKey) + require.NoError(t, hErr) + require.False(t, has, "a refused revert must not leave a UniversalTx behind") +} diff --git a/test/integration/uexecutor/vote_chain_meta_eligibility_test.go b/test/integration/uexecutor/vote_chain_meta_eligibility_test.go new file mode 100644 index 000000000..637ff068a --- /dev/null +++ b/test/integration/uexecutor/vote_chain_meta_eligibility_test.go @@ -0,0 +1,252 @@ +package integrationtest + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app" + utils "github.com/pushchain/push-chain-node/test/utils" + uvalidatortypes "github.com/pushchain/push-chain-node/x/uvalidator/types" +) + +// coreAccOf returns the account bech32 that signs MsgVoteChainMeta on behalf of +// the given staking validator (the hotkey's grantee target). +func coreAccOf(t *testing.T, val stakingtypes.Validator) string { + t.Helper() + valAddr, err := sdk.ValAddressFromBech32(val.OperatorAddress) + require.NoError(t, err) + return sdk.AccAddress(valAddr).String() +} + +// forceUVLifecycleStatus overwrites only the lifecycle status of an already +// registered universal validator, leaving identity/network info intact and +// leaving the underlying staking validator bonded. It bypasses transition +// validation so INACTIVE can be reached directly. +func forceUVLifecycleStatus( + t *testing.T, + testApp *app.ChainApp, + ctx sdk.Context, + val stakingtypes.Validator, + status uvalidatortypes.UVStatus, +) { + t.Helper() + valAddr, err := sdk.ValAddressFromBech32(val.OperatorAddress) + require.NoError(t, err) + + uv, err := testApp.UvalidatorKeeper.UniversalValidatorSet.Get(ctx, valAddr) + require.NoError(t, err) + uv.LifecycleInfo.CurrentStatus = status + require.NoError(t, testApp.UvalidatorKeeper.UniversalValidatorSet.Set(ctx, valAddr, uv)) +} + +// requireStillBonded asserts the staking validator behind a universal validator +// is still bonded. This is the precondition the finding rests on: lifecycle +// removal does not unbond stake, so a bonded-only admission gate keeps letting +// the removed hotkey in. +func requireStillBonded(t *testing.T, testApp *app.ChainApp, ctx sdk.Context, val stakingtypes.Validator) { + t.Helper() + valAddr, err := sdk.ValAddressFromBech32(val.OperatorAddress) + require.NoError(t, err) + sv, err := testApp.StakingKeeper.GetValidator(ctx, valAddr) + require.NoError(t, err) + require.True(t, sv.IsBonded(), + "removal must leave the validator bonded -- otherwise the finding's vector would not exist") +} + +// TestVoteChainMeta_EligibilityGate is the F-2026-18148 regression suite. +// +// MsgVoteChainMeta used to admit any bonded, registered universal validator. +// Admin removal moves a universal validator to PENDING_LEAVE while its stake +// stays bonded, and AfterValidatorRemoved prunes its ChainMeta rows but revokes +// neither its AuthZ grant nor its membership in the set -- so the removed +// hotkey could reinsert votes straight after the prune. Admission is now gated +// on the same eligibility predicate (ACTIVE / PENDING_JOIN + bonded + not +// tombstoned) that uvalidator uses to snapshot ballot voters. +func TestVoteChainMeta_EligibilityGate(t *testing.T) { + chainId := "eip155:11155111" + + t.Run("removed PENDING_LEAVE validator cannot reinsert a vote after the prune", func(t *testing.T) { + testApp, ctx, uvals, vals := setupVoteChainMetaTest(t, 5) + + // Removal of an ACTIVE universal validator requires no ongoing TSS. + _ = testApp.UtssKeeper.CurrentTssProcess.Remove(ctx) + for _, v := range vals { + valAddr, err := sdk.ValAddressFromBech32(v.OperatorAddress) + require.NoError(t, err) + require.NoError(t, testApp.UvalidatorKeeper.UpdateValidatorStatus( + ctx, valAddr, + uvalidatortypes.UVStatus_UV_STATUS_ACTIVE, + uvalidatortypes.TransitionReason_TRANSITION_REASON_UNSPECIFIED, + )) + } + + coreAccs := make([]string, len(vals)) + for i := range vals { + coreAccs[i] = coreAccOf(t, vals[i]) + } + + // Five ACTIVE validators vote. Prices 100..500, heights 10..50. + // After the 3rd vote the oracle bootstraps; by the 5th the recorded + // upper median price is 300 and LastAppliedChainHeight is 30. + prices := []uint64{100, 200, 300, 400, 500} + heights := []uint64{10, 20, 30, 40, 50} + for i := range vals { + require.NoError(t, utils.ExecVoteChainMeta(t, ctx, testApp, uvals[i], coreAccs[i], chainId, prices[i], heights[i])) + } + + stored, found, err := testApp.UexecutorKeeper.GetChainMeta(ctx, chainId) + require.NoError(t, err) + require.True(t, found) + require.Len(t, stored.Signers, 5) + require.Equal(t, uint64(300), stored.Prices[stored.MedianIndex], "baseline recorded median price") + require.Equal(t, uint64(30), stored.LastAppliedChainHeight, "baseline applied chain height") + + // Admin removes validator 4: ACTIVE -> PENDING_LEAVE, ChainMeta pruned. + require.NoError(t, testApp.UvalidatorKeeper.RemoveUniversalValidator(ctx, vals[4].OperatorAddress)) + + removedValAddr, err := sdk.ValAddressFromBech32(vals[4].OperatorAddress) + require.NoError(t, err) + uv, uvFound, err := testApp.UvalidatorKeeper.GetUniversalValidator(ctx, removedValAddr) + require.NoError(t, err) + require.True(t, uvFound, "removal keeps the row in the set -- only the lifecycle status changes") + require.Equal(t, uvalidatortypes.UVStatus_UV_STATUS_PENDING_LEAVE, uv.LifecycleInfo.CurrentStatus) + + // The two halves of the vector: stake is still bonded, and the AuthZ + // grant was never revoked, so the hotkey can still build the tx. + requireStillBonded(t, testApp, ctx, vals[4]) + + pruned, _, err := testApp.UexecutorKeeper.GetChainMeta(ctx, chainId) + require.NoError(t, err) + require.Len(t, pruned.Signers, 4, "the removed validator's ChainMeta row must have been pruned") + + // The removed hotkey now tries to reinsert a vote. A price of 250 sits + // between the surviving 200 and 300, so if it landed it would drag the + // upper median down from 300 to 250. Height 35 clears the stale-height + // gate (LastAppliedChainHeight = 30). + reinsertErr := utils.ExecVoteChainMeta(t, ctx, testApp, uvals[4], coreAccs[4], chainId, 250, 35) + + after, _, err := testApp.UexecutorKeeper.GetChainMeta(ctx, chainId) + require.NoError(t, err) + + // State assertions first: an aborting error assertion must not be able + // to hide a vote that actually landed. + require.Equal(t, uint64(300), after.Prices[after.MedianIndex], + "the median must still be computed over the four surviving votes only") + require.NotContains(t, after.Signers, removedValAddr.String(), + "the removed validator must not reappear among the ChainMeta signers") + require.NotContains(t, after.Prices, uint64(250), "the rejected price must not be recorded") + require.Len(t, after.Signers, 4, "no new signer row may be inserted") + require.Equal(t, uint64(30), after.LastAppliedChainHeight, + "a rejected vote must not advance the applied chain height") + + require.Error(t, reinsertErr, "a PENDING_LEAVE universal validator must not be able to vote on chain meta") + require.Contains(t, reinsertErr.Error(), "is not an eligible voter") + }) + + t.Run("INACTIVE but still-bonded validator is rejected", func(t *testing.T) { + testApp, ctx, uvals, vals := setupVoteChainMetaTest(t, 3) + + for _, v := range vals { + valAddr, err := sdk.ValAddressFromBech32(v.OperatorAddress) + require.NoError(t, err) + require.NoError(t, testApp.UvalidatorKeeper.UpdateValidatorStatus( + ctx, valAddr, + uvalidatortypes.UVStatus_UV_STATUS_ACTIVE, + uvalidatortypes.TransitionReason_TRANSITION_REASON_UNSPECIFIED, + )) + } + forceUVLifecycleStatus(t, testApp, ctx, vals[2], uvalidatortypes.UVStatus_UV_STATUS_INACTIVE) + requireStillBonded(t, testApp, ctx, vals[2]) + + voteErr := utils.ExecVoteChainMeta(t, ctx, testApp, uvals[2], coreAccOf(t, vals[2]), chainId, 777, 7) + + _, found, err := testApp.UexecutorKeeper.GetChainMeta(ctx, chainId) + require.NoError(t, err) + require.False(t, found, "an INACTIVE validator's vote must not create a ChainMeta entry") + + require.Error(t, voteErr, "an INACTIVE universal validator must not be able to vote on chain meta") + require.Contains(t, voteErr.Error(), "is not an eligible voter") + }) + + t.Run("ACTIVE validator is still accepted", func(t *testing.T) { + testApp, ctx, uvals, vals := setupVoteChainMetaTest(t, 3) + + for _, v := range vals { + valAddr, err := sdk.ValAddressFromBech32(v.OperatorAddress) + require.NoError(t, err) + require.NoError(t, testApp.UvalidatorKeeper.UpdateValidatorStatus( + ctx, valAddr, + uvalidatortypes.UVStatus_UV_STATUS_ACTIVE, + uvalidatortypes.TransitionReason_TRANSITION_REASON_UNSPECIFIED, + )) + } + + require.NoError(t, utils.ExecVoteChainMeta(t, ctx, testApp, uvals[0], coreAccOf(t, vals[0]), chainId, 100, 1)) + + stored, found, err := testApp.UexecutorKeeper.GetChainMeta(ctx, chainId) + require.NoError(t, err) + require.True(t, found) + require.Len(t, stored.Signers, 1, "the ACTIVE validator's vote must be recorded") + }) + + t.Run("PENDING_JOIN validator is still accepted", func(t *testing.T) { + // setupVoteChainMetaTest registers every universal validator through + // AddUniversalValidator, which leaves them in PENDING_JOIN. + testApp, ctx, uvals, vals := setupVoteChainMetaTest(t, 3) + + valAddr, err := sdk.ValAddressFromBech32(vals[1].OperatorAddress) + require.NoError(t, err) + uv, found, err := testApp.UvalidatorKeeper.GetUniversalValidator(ctx, valAddr) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, uvalidatortypes.UVStatus_UV_STATUS_PENDING_JOIN, uv.LifecycleInfo.CurrentStatus) + + require.NoError(t, utils.ExecVoteChainMeta(t, ctx, testApp, uvals[1], coreAccOf(t, vals[1]), chainId, 100, 1)) + + stored, found, err := testApp.UexecutorKeeper.GetChainMeta(ctx, chainId) + require.NoError(t, err) + require.True(t, found) + require.Len(t, stored.Signers, 1, "the PENDING_JOIN validator's vote must be recorded") + }) + + t.Run("fewer than three eligible validators cannot reach the bootstrap minimum", func(t *testing.T) { + // Documents the bootstrap interaction, it does not assert a defect: + // chainMetaMinVotesForFirstWrite = 3 counts fresh vote ROWS, and there + // is at most one row per validator. Tightening admission can only + // shrink the pool of validators able to produce a row, so a set with + // fewer than three ELIGIBLE universal validators can never bootstrap + // the oracle. That was already true of any topology with fewer than + // three bonded universal validators; this gate makes lifecycle state + // count towards it too. + testApp, ctx, uvals, vals := setupVoteChainMetaTest(t, 3) + + for _, v := range vals { + valAddr, err := sdk.ValAddressFromBech32(v.OperatorAddress) + require.NoError(t, err) + require.NoError(t, testApp.UvalidatorKeeper.UpdateValidatorStatus( + ctx, valAddr, + uvalidatortypes.UVStatus_UV_STATUS_ACTIVE, + uvalidatortypes.TransitionReason_TRANSITION_REASON_UNSPECIFIED, + )) + } + forceUVLifecycleStatus(t, testApp, ctx, vals[2], uvalidatortypes.UVStatus_UV_STATUS_PENDING_LEAVE) + requireStillBonded(t, testApp, ctx, vals[2]) + + require.NoError(t, utils.ExecVoteChainMeta(t, ctx, testApp, uvals[0], coreAccOf(t, vals[0]), chainId, 100, 1)) + require.NoError(t, utils.ExecVoteChainMeta(t, ctx, testApp, uvals[1], coreAccOf(t, vals[1]), chainId, 200, 2)) + thirdErr := utils.ExecVoteChainMeta(t, ctx, testApp, uvals[2], coreAccOf(t, vals[2]), chainId, 300, 3) + + stored, found, err := testApp.UexecutorKeeper.GetChainMeta(ctx, chainId) + require.NoError(t, err) + require.True(t, found) + require.Len(t, stored.Signers, 2, "only the two eligible validators may hold a vote row") + require.Equal(t, uint64(0), stored.LastAppliedChainHeight, + "two fresh votes are below chainMetaMinVotesForFirstWrite=3, so the oracle stays un-bootstrapped") + + require.Error(t, thirdErr) + require.Contains(t, thirdErr.Error(), "is not an eligible voter") + }) +} diff --git a/test/integration/uexecutor/vote_chain_meta_test.go b/test/integration/uexecutor/vote_chain_meta_test.go index b75fe4daf..bb6474256 100644 --- a/test/integration/uexecutor/vote_chain_meta_test.go +++ b/test/integration/uexecutor/vote_chain_meta_test.go @@ -53,6 +53,18 @@ func setupVoteChainMetaTest(t *testing.T, numVals int) (*app.ChainApp, sdk.Conte return testApp, ctx, universalVals, validators } +// chainMetaKeys returns every key currently present in the ChainMetas map. +func chainMetaKeys(t *testing.T, ctx sdk.Context, testApp *app.ChainApp) []string { + t.Helper() + var keys []string + require.NoError(t, testApp.UexecutorKeeper.ChainMetas.Walk(ctx, nil, + func(chainID string, _ uexecutortypes.ChainMeta) (bool, error) { + keys = append(keys, chainID) + return false, nil + })) + return keys +} + func TestVoteChainMetaIntegration(t *testing.T) { t.Parallel() chainId := "eip155:11155111" @@ -84,6 +96,37 @@ func TestVoteChainMetaIntegration(t *testing.T) { require.Equal(t, uint64(0), stored.LastAppliedChainHeight, "two votes should still not bootstrap the oracle") }) + t.Run("vote for an unregistered chain is rejected and writes no ChainMetas row", func(t *testing.T) { + // F-2026-18803: only eip155:11155111 is registered by the fixture. A + // bonded universal validator voting on any other chain id used to mint a + // ChainMetas row keyed by that raw id (collections.StringKey). + const unregistered = "eip155:999999999" + + testApp, ctx, uvals, vals := setupVoteChainMetaTest(t, 1) + + coreVal, err := sdk.ValAddressFromBech32(vals[0].OperatorAddress) + require.NoError(t, err) + coreAcc := sdk.AccAddress(coreVal).String() + + before := chainMetaKeys(t, ctx, testApp) + require.Empty(t, before) + + voteErr := utils.ExecVoteChainMeta(t, ctx, testApp, uvals[0], coreAcc, unregistered, 100_000_000_000, 12345) + + // Store first, deliberately: the finding is the row being written. + _, found, err := testApp.UexecutorKeeper.GetChainMeta(ctx, unregistered) + require.NoError(t, err) + require.False(t, found, "unregistered chain must not create a ChainMetas row") + require.Equal(t, before, chainMetaKeys(t, ctx, testApp), "ChainMetas must be unchanged") + + require.Error(t, voteErr) + require.Contains(t, voteErr.Error(), "is not registered") + + // The registered chain still works from the same validator. + require.NoError(t, utils.ExecVoteChainMeta(t, ctx, testApp, uvals[0], coreAcc, chainId, 100_000_000_000, 12345)) + require.Equal(t, []string{chainId}, chainMetaKeys(t, ctx, testApp)) + }) + t.Run("third fresh vote bootstraps the oracle and sets LastAppliedChainHeight to median", func(t *testing.T) { testApp, ctx, uvals, vals := setupVoteChainMetaTest(t, 3) diff --git a/test/integration/uexecutor/vote_inbound_validation_test.go b/test/integration/uexecutor/vote_inbound_validation_test.go index 4e833dec6..ac1978fd1 100644 --- a/test/integration/uexecutor/vote_inbound_validation_test.go +++ b/test/integration/uexecutor/vote_inbound_validation_test.go @@ -233,7 +233,11 @@ func TestVoteInboundValidation(t *testing.T) { foundRevert = true require.Equal(t, inbound.SourceChain, ob.DestinationChain) require.Equal(t, inbound.Amount, ob.Amount) - require.Equal(t, uexecutortypes.Status_PENDING, ob.OutboundStatus) + // The harness's UniversalCore stub cannot serve gas metadata, so the + // revert is unsignable and is recorded ABORTED instead of queued. + require.Equal(t, uexecutortypes.Status_ABORTED, ob.OutboundStatus) + require.NotEmpty(t, ob.AbortReason) + requireNotQueuedForSigning(t, chainApp, ctx, ob.Id) break } } @@ -356,7 +360,12 @@ func TestVoteInboundValidation(t *testing.T) { require.Equal(t, inbound.SourceChain, ob.DestinationChain) require.Equal(t, inbound.Amount, ob.Amount) require.Equal(t, inbound.AssetAddr, ob.ExternalAssetAddr) - require.Equal(t, uexecutortypes.Status_PENDING, ob.OutboundStatus) + // The token config was removed above, so the revert cannot resolve the + // PRC20 it needs for gas metadata. It is recorded ABORTED with the + // reason instead of being queued as an unsignable PENDING row. + require.Equal(t, uexecutortypes.Status_ABORTED, ob.OutboundStatus) + require.Contains(t, ob.AbortReason, "failed to resolve PRC20") + requireNotQueuedForSigning(t, chainApp, ctx, ob.Id) break } } diff --git a/test/integration/utss/fund_migration_test.go b/test/integration/utss/fund_migration_test.go index 13595e64c..12f7dc001 100644 --- a/test/integration/utss/fund_migration_test.go +++ b/test/integration/utss/fund_migration_test.go @@ -1,12 +1,14 @@ package integrationtest import ( + "bytes" "fmt" "math/big" "strconv" "strings" "testing" + "cosmossdk.io/collections" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -17,6 +19,7 @@ import ( utils "github.com/pushchain/push-chain-node/test/utils" uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types" + utsskeeper "github.com/pushchain/push-chain-node/x/utss/keeper" utsstypes "github.com/pushchain/push-chain-node/x/utss/types" uvalidatortypes "github.com/pushchain/push-chain-node/x/uvalidator/types" ) @@ -60,6 +63,11 @@ const universalCoreSetupABI = `[ } ]` +// testBalance is the native balance the admin reports observing on the old TSS +// address. Comfortably above gas_price*21000 + 150 so the derived +// transfer_amount is positive for any oracle gas price the harness produces. +const testBalance = "1000000000000000000" // 1e18 wei + // seedFundMigrationChainValues grants MANAGER_ROLE to the admin and seeds the // per-chain tss-fund-migration gas limit and L1 gas fee on UniversalCore. // InitiateFundMigration rejects a zero gas limit, so without this seeding the @@ -196,9 +204,11 @@ func TestInitiateFundMigration(t *testing.T) { t.Run("Successfully initiates fund migration", func(t *testing.T) { app, ctx, _, oldKeyId := setupFundMigrationTest(t, 3, false) - migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain) + migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) require.NoError(t, err) - require.Equal(t, uint64(0), migrationId) + // Ids start at 1, not 0 — 0 is reserved for "unset" and is rejected by + // MsgVoteFundMigration.ValidateBasic (F-2026-18789). + require.Equal(t, uint64(1), migrationId) // Verify migration is stored migration, err := app.UtssKeeper.FundMigrations.Get(ctx, migrationId) @@ -228,10 +238,72 @@ func TestInitiateFundMigration(t *testing.T) { require.True(t, found, "FundMigrationInitiatedEvent should be emitted") }) + t.Run("Derives transfer_amount from the observed balance and pinned fees", func(t *testing.T) { + app, ctx, _, oldKeyId := setupFundMigrationTest(t, 3, false) + + migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) + require.NoError(t, err) + + migration, err := app.UtssKeeper.FundMigrations.Get(ctx, migrationId) + require.NoError(t, err) + + // transfer_amount must equal balance - (gas_price * gas_limit) - l1_gas_fee, + // computed from the very fields recorded alongside it. Deriving it here + // rather than accepting it from the admin is what makes the two consistent + // by construction — the admin cannot know these fees, they are read from + // UniversalCore inside the handler (F-2026-18142). + gasPrice, ok := new(big.Int).SetString(migration.GasPrice, 10) + require.True(t, ok) + l1GasFee, ok := new(big.Int).SetString(migration.L1GasFee, 10) + require.True(t, ok) + balance, ok := new(big.Int).SetString(testBalance, 10) + require.True(t, ok) + + want := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(migration.GasLimit)) + want.Add(want, l1GasFee) + want.Sub(balance, want) + + require.Equal(t, want.String(), migration.TransferAmount) + require.Positive(t, want.Sign(), "the fixture balance must exceed the fees or this proves nothing") + + // The pinned amount must also reach the universal validators, which read + // it off the event rather than re-deriving it from a live balance. + var attr string + for _, ev := range ctx.EventManager().Events() { + if ev.Type != utsstypes.EventTypeFundMigrationInitiated { + continue + } + for _, a := range ev.Attributes { + if a.Key == "transfer_amount" { + attr = a.Value + } + } + } + require.Equal(t, migration.TransferAmount, attr, + "transfer_amount must be emitted on the event") + }) + + t.Run("Fails when the balance cannot cover the migration fee", func(t *testing.T) { + app, ctx, _, oldKeyId := setupFundMigrationTest(t, 3, false) + + // 1 wei cannot cover gas_price*21000 + 150. Rejecting at initiate time + // beats creating a PENDING migration that can never be signed. + _, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, "1") + require.ErrorContains(t, err, "does not cover the migration fee") + + // Nothing may be left behind under any id. + var stored int + require.NoError(t, app.UtssKeeper.FundMigrations.Walk(ctx, nil, func(uint64, utsstypes.FundMigration) (bool, error) { + stored++ + return false, nil + })) + require.Zero(t, stored, "a rejected migration must not be stored") + }) + t.Run("Fails if old key not found", func(t *testing.T) { app, ctx, _, _ := setupFundMigrationTest(t, 3, false) - _, err := app.UtssKeeper.InitiateFundMigration(ctx, "nonexistent-key", testChain) + _, err := app.UtssKeeper.InitiateFundMigration(ctx, "nonexistent-key", testChain, testBalance) require.ErrorContains(t, err, "not found in TssKeyHistory") }) @@ -241,25 +313,25 @@ func TestInitiateFundMigration(t *testing.T) { currentKey, err := app.UtssKeeper.CurrentTssKey.Get(ctx) require.NoError(t, err) - _, err = app.UtssKeeper.InitiateFundMigration(ctx, currentKey.KeyId, testChain) + _, err = app.UtssKeeper.InitiateFundMigration(ctx, currentKey.KeyId, testChain, testBalance) require.ErrorContains(t, err, "current active key") }) t.Run("Fails if outbound is still enabled", func(t *testing.T) { app, ctx, _, oldKeyId := setupFundMigrationTest(t, 3, true) // outbound enabled - _, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain) + _, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) require.ErrorContains(t, err, "outbound is still enabled") }) t.Run("Fails if duplicate pending migration exists", func(t *testing.T) { app, ctx, _, oldKeyId := setupFundMigrationTest(t, 3, false) - _, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain) + _, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) require.NoError(t, err) // Try again — should fail (same chain already has pending migration) - _, err = app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain) + _, err = app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) require.ErrorContains(t, err, "pending migration already exists for chain") }) } @@ -269,7 +341,7 @@ func TestVoteFundMigration(t *testing.T) { app, ctx, universalVals, oldKeyId := setupFundMigrationTest(t, 3, false) // Initiate migration - migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain) + migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) require.NoError(t, err) txHash := "0xdeadbeef12345678deadbeef12345678deadbeef12345678deadbeef12345678" @@ -317,7 +389,7 @@ func TestVoteFundMigration(t *testing.T) { t.Run("Migration failure flow", func(t *testing.T) { app, ctx, universalVals, oldKeyId := setupFundMigrationTest(t, 3, false) - migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain) + migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) require.NoError(t, err) txHash := "" @@ -345,7 +417,7 @@ func TestVoteFundMigration(t *testing.T) { t.Run("Fails to vote on already finalized migration", func(t *testing.T) { app, ctx, universalVals, oldKeyId := setupFundMigrationTest(t, 3, false) - migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain) + migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) require.NoError(t, err) // Finalize it first @@ -365,7 +437,7 @@ func TestFundMigrationQueries(t *testing.T) { t.Run("GetFundMigration returns correct migration", func(t *testing.T) { app, ctx, _, oldKeyId := setupFundMigrationTest(t, 3, false) - migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain) + migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) require.NoError(t, err) migration, err := app.UtssKeeper.FundMigrations.Get(ctx, migrationId) @@ -377,7 +449,7 @@ func TestFundMigrationQueries(t *testing.T) { t.Run("PendingMigrations tracks correctly", func(t *testing.T) { app, ctx, universalVals, oldKeyId := setupFundMigrationTest(t, 3, false) - migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain) + migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) require.NoError(t, err) // Should be in pending @@ -413,7 +485,7 @@ func TestFundMigrationQueries(t *testing.T) { func TestVoteFundMigration_EquivalentHashEncodingsConverge(t *testing.T) { app, ctx, universalVals, oldKeyId := setupFundMigrationTest(t, 3, false) - migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain) + migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) require.NoError(t, err) canonical := "0xb28f49668e7e76dc96d7aabe5b7f63fecfbd1c3574774c05e8204e749fd96fbd" @@ -446,10 +518,148 @@ func TestVoteFundMigration_EquivalentHashEncodingsConverge(t *testing.T) { func TestVoteFundMigration_MalformedHashRejected(t *testing.T) { app, ctx, universalVals, oldKeyId := setupFundMigrationTest(t, 3, false) - migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain) + migrationId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) require.NoError(t, err) valAddr, _ := sdk.ValAddressFromBech32(universalVals[0]) err = app.UtssKeeper.VoteFundMigration(ctx, valAddr, migrationId, "0xnot-a-real-hash", true) require.ErrorContains(t, err, "invalid tx hash") } + +// TestInitiateFundMigration_FirstMigrationIsVotable is the F-2026-18789 +// regression. +// +// Migration ids used to come straight off collections.Sequence, whose first +// value is 0, while MsgVoteFundMigration.ValidateBasic rejects +// migration_id == 0 as "unset". The very first migration on a fresh chain was +// therefore unvotable: every vote died in ValidateBasic before it ever reached +// the keeper, the migration never left PendingMigrations, and +// InitiateFundMigration then refused every later migration for that chain — +// the lane was bricked with no way out short of an upgrade. audit-fixes is a +// fresh-genesis branch, so this fires on first use. +// +// Ids are now allocated as sequence + 1: the first id is 1 and 0 stays +// reserved for "unset". The three cases are separate subtests on purpose, so +// that each one reports independently instead of the first failure masking +// the rest. +func TestInitiateFundMigration_FirstMigrationIsVotable(t *testing.T) { + // firstMigration runs the very first migration a fresh chain ever + // allocates — the case that used to be unreachable — and returns its id. + firstMigration := func(t *testing.T) (*app.ChainApp, sdk.Context, []string, string, uint64) { + t.Helper() + chainApp, ctx, universalVals, oldKeyId := setupFundMigrationTest(t, 3, false) + + seq, err := chainApp.UtssKeeper.NextMigrationId.Peek(ctx) + require.NoError(t, err) + require.Zero(t, seq, "fixture must start from a virgin sequence or this proves nothing") + + migrationId, err := chainApp.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) + require.NoError(t, err) + return chainApp, ctx, universalVals, oldKeyId, migrationId + } + + const txHash = "0xdeadbeef12345678deadbeef12345678deadbeef12345678deadbeef12345678" + + voteMsg := func(t *testing.T, val string, migrationId uint64) *utsstypes.MsgVoteFundMigration { + t.Helper() + valAddr, err := sdk.ValAddressFromBech32(val) + require.NoError(t, err) + return &utsstypes.MsgVoteFundMigration{ + Signer: sdk.AccAddress(valAddr).String(), + MigrationId: migrationId, + TxHash: txHash, + Success: true, + } + } + + t.Run("the first id is never the reserved 0", func(t *testing.T) { + chainApp, ctx, _, _, migrationId := firstMigration(t) + + require.NotZero(t, migrationId, + "the first migration id must never be 0: MsgVoteFundMigration.ValidateBasic rejects 0 as unset") + require.Equal(t, uint64(1), migrationId) + + // The record really is stored under that votable id. + migration, err := chainApp.UtssKeeper.FundMigrations.Get(ctx, migrationId) + require.NoError(t, err) + require.Equal(t, utsstypes.FundMigrationStatus_FUND_MIGRATION_STATUS_PENDING, migration.Status) + require.Equal(t, testChain, migration.Chain) + }) + + t.Run("a vote on the first migration passes ValidateBasic", func(t *testing.T) { + _, _, universalVals, _, migrationId := firstMigration(t) + + // This is the exact gate that bricked the lane: the message a universal + // validator broadcasts is rejected here, before the keeper is reached. + msg := voteMsg(t, universalVals[0], migrationId) + require.NoError(t, msg.ValidateBasic(), + "a vote on the first migration must survive ValidateBasic") + }) + + t.Run("the first migration finalizes and unblocks the chain", func(t *testing.T) { + chainApp, ctx, universalVals, oldKeyId, migrationId := firstMigration(t) + msgServer := utsskeeper.NewMsgServerImpl(chainApp.UtssKeeper) + + for _, val := range universalVals { + msg := voteMsg(t, val, migrationId) + require.NoError(t, msg.ValidateBasic()) + _, err := msgServer.VoteFundMigration(ctx, msg) + require.NoError(t, err, "a vote on the first migration must reach the ballot") + } + + migration, err := chainApp.UtssKeeper.FundMigrations.Get(ctx, migrationId) + require.NoError(t, err) + require.Equal(t, utsstypes.FundMigrationStatus_FUND_MIGRATION_STATUS_COMPLETED, migration.Status, + "votes on the first migration must be able to finalize it") + require.Equal(t, txHash, migration.TxHash) + + _, err = chainApp.UtssKeeper.PendingMigrations.Get(ctx, migrationId) + require.ErrorIs(t, err, collections.ErrNotFound, + "a finalized migration must leave PendingMigrations, or the chain stays blocked") + + // The outcome the bug denied: the chain is migratable again, under the + // next votable id. + secondId, err := chainApp.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) + require.NoError(t, err, "a second migration must be possible once the first finalized") + require.Equal(t, uint64(2), secondId) + }) +} + +// TestVoteFundMigration_ZeroMigrationIdStaysRejected pins the other half of +// the F-2026-18789 contract: 0 keeps meaning "unset". The fix moves ids off 0 +// rather than allowing 0, so this guard must stay in place. +func TestVoteFundMigration_ZeroMigrationIdStaysRejected(t *testing.T) { + msg := &utsstypes.MsgVoteFundMigration{ + Signer: sdk.AccAddress(bytes.Repeat([]byte{1}, 20)).String(), + MigrationId: 0, + TxHash: "0xdeadbeef12345678deadbeef12345678deadbeef12345678deadbeef12345678", + Success: true, + } + require.ErrorContains(t, msg.ValidateBasic(), "migration_id is required") +} + +// TestFundMigrationIdsSurviveGenesisRoundTrip guards the interaction between +// the sequence + 1 allocation and genesis: the exported counter must not hand +// an already-used id back after an export/import cycle. +func TestFundMigrationIdsSurviveGenesisRoundTrip(t *testing.T) { + app, ctx, _, oldKeyId := setupFundMigrationTest(t, 3, false) + + firstId, err := app.UtssKeeper.InitiateFundMigration(ctx, oldKeyId, testChain, testBalance) + require.NoError(t, err) + require.Equal(t, uint64(1), firstId) + + // ExportGenesis reads Params, which this fixture never seeds. + require.NoError(t, app.UtssKeeper.Params.Set(ctx, utsstypes.Params{ + Admin: "push1negskcfqu09j5zvpk7nhvacnwyy2mafffy7r6a", + })) + + exported := app.UtssKeeper.ExportGenesis(ctx) + require.Equal(t, uint64(1), exported.NextMigrationId) + require.NoError(t, app.UtssKeeper.InitGenesis(ctx, exported)) + + // The next allocation must not collide with the id already in state. + seq, err := app.UtssKeeper.NextMigrationId.Next(ctx) + require.NoError(t, err) + require.Greater(t, seq+1, firstId, + "an export/import cycle must not re-issue an id that is already taken") +} diff --git a/test/integration/uvalidator/ballot_voting_test.go b/test/integration/uvalidator/ballot_voting_test.go index 64b0be3a0..ff1b55b2e 100644 --- a/test/integration/uvalidator/ballot_voting_test.go +++ b/test/integration/uvalidator/ballot_voting_test.go @@ -4,12 +4,15 @@ import ( "fmt" "testing" + "cosmossdk.io/core/appmodule" + storetypes "cosmossdk.io/store/types" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/stretchr/testify/require" "github.com/pushchain/push-chain-node/app" utils "github.com/pushchain/push-chain-node/test/utils" + uvalidatorkeeper "github.com/pushchain/push-chain-node/x/uvalidator/keeper" uvalidatortypes "github.com/pushchain/push-chain-node/x/uvalidator/types" ) @@ -102,7 +105,7 @@ func TestIntegration_CreateBallot(t *testing.T) { require.Equal(t, int64(60), ballot.BlockHeightExpiry) }) - t.Run("creating a new ballot expires stale active ballots", func(t *testing.T) { + t.Run("creating a new ballot does NOT expire stale active ballots", func(t *testing.T) { chainApp, ctx, validators := setupBallotTest(t, 2) k := chainApp.UvalidatorKeeper voters := voterAddrs(t, validators) @@ -114,16 +117,24 @@ func TestIntegration_CreateBallot(t *testing.T) { voters, 1, 1) require.NoError(t, err) - // Advance height past the expiry so the next CreateBallot triggers cleanup + // Advance past the old ballot's expiry and create another one. Creation + // no longer scans the active set — that walked every active ballot and + // paid an IAVL read each time. Expiry is the EndBlocker's job now. ctx = ctx.WithBlockHeight(10) _, err = k.CreateBallot(ctx, "new-ballot", uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_INBOUND_TX, voters, 1, 100) require.NoError(t, err) - // Old ballot should now be expired old, err := k.GetBallot(ctx, "old-ballot") require.NoError(t, err) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, old.Status, + "CreateBallot must not sweep; the EndBlocker owns expiry") + + // The sweep is what expires it. + require.NoError(t, k.ExpireBallotsBeforeHeight(ctx, ctx.BlockHeight())) + old, err = k.GetBallot(ctx, "old-ballot") + require.NoError(t, err) require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, old.Status) }) } @@ -963,3 +974,105 @@ func TestIntegration_IsTombstonedUniversalValidator(t *testing.T) { require.Contains(t, err.Error(), "invalid signer address") }) } + +// ─── EndBlocker expiry sweep ───────────────────────────────────────────────── + +// endBlockCtx moves ctx to the given height and attaches a block gas meter. +// The test fixture's context has none, and x/feemarket's EndBlocker — which the +// module manager runs before x/uvalidator's — errors out without one. +func endBlockCtx(ctx sdk.Context, height int64) sdk.Context { + return ctx.WithBlockHeight(height).WithBlockGasMeter(storetypes.NewInfiniteGasMeter()) +} + +// TestIntegration_UvalidatorEndBlockerRuns is the guard against a +// silently-never-called EndBlock. The sweep only fires if x/uvalidator is BOTH +// listed in SetOrderEndBlockers AND satisfies appmodule.HasEndBlocker — listing +// alone is not enough, the module manager skips modules that do not implement +// the interface. This drives the app's real EndBlocker and observes the effect. +func TestIntegration_UvalidatorEndBlockerRuns(t *testing.T) { + t.Run("expires due ballots and leaves the rest alone", func(t *testing.T) { + chainApp, ctx, validators := setupBallotTest(t, 3) + k := chainApp.UvalidatorKeeper + voters := voterAddrs(t, validators) + + // ctx starts at height 1 → expiry heights 2 and 1001. + _, err := k.CreateBallot(ctx, "eb-due", + uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_INBOUND_TX, + voters, 2, 1) + require.NoError(t, err) + + _, err = k.CreateBallot(ctx, "eb-future", + uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_INBOUND_TX, + voters, 2, 1000) + require.NoError(t, err) + + // Creation must not have swept anything. + due, err := k.GetBallot(ctx, "eb-due") + require.NoError(t, err) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, due.Status) + + // Advance the block and run the app's real EndBlocker chain. + ctx = endBlockCtx(ctx, 5) + _, err = chainApp.EndBlocker(ctx) + require.NoError(t, err) + + // THE assertion: if x/uvalidator's EndBlock never fires, this fails. + due, err = k.GetBallot(ctx, "eb-due") + require.NoError(t, err) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, due.Status, + "the x/uvalidator EndBlocker did not run the ballot expiry sweep") + + future, err := k.GetBallot(ctx, "eb-future") + require.NoError(t, err) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, future.Status, + "a not-yet-due ballot must survive the sweep") + }) + + t.Run("caps a large backlog at MaxExpiriesPerBlock per block", func(t *testing.T) { + chainApp, ctx, validators := setupBallotTest(t, 3) + k := chainApp.UvalidatorKeeper + voters := voterAddrs(t, validators) + + const extra = 3 + total := uvalidatorkeeper.MaxExpiriesPerBlock + extra + for i := 0; i < total; i++ { + _, err := k.CreateBallot(ctx, fmt.Sprintf("eb-cap-%03d", i), + uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_INBOUND_TX, + voters, 2, 1) + require.NoError(t, err) + } + + countExpired := func(ctx sdk.Context) int { + n := 0 + require.NoError(t, k.ExpiredBallotIDs.Walk(ctx, nil, func(string) (bool, error) { + n++ + return false, nil + })) + return n + } + + ctx = endBlockCtx(ctx, 5) + _, err := chainApp.EndBlocker(ctx) + require.NoError(t, err) + require.Equal(t, uvalidatorkeeper.MaxExpiriesPerBlock, countExpired(ctx), + "one block must expire at most MaxExpiriesPerBlock ballots") + + // The leftovers are carried to the next block, not lost. + ctx = endBlockCtx(ctx, 6) + _, err = chainApp.EndBlocker(ctx) + require.NoError(t, err) + require.Equal(t, total, countExpired(ctx), + "the backlog remainder must be swept by the following block") + }) + + t.Run("module is wired into the EndBlocker ordering", func(t *testing.T) { + chainApp, _, _ := setupBallotTest(t, 1) + + require.Contains(t, chainApp.ModuleManager.OrderEndBlockers, uvalidatortypes.ModuleName, + "x/uvalidator must be listed in SetOrderEndBlockers or its EndBlock never runs") + + _, ok := chainApp.ModuleManager.Modules[uvalidatortypes.ModuleName].(appmodule.HasEndBlocker) + require.True(t, ok, + "x/uvalidator must implement appmodule.HasEndBlocker; the module manager skips modules that do not") + }) +} diff --git a/test/integration/uvalidator/jailed_voter_quorum_test.go b/test/integration/uvalidator/jailed_voter_quorum_test.go new file mode 100644 index 000000000..dcf9beb1a --- /dev/null +++ b/test/integration/uvalidator/jailed_voter_quorum_test.go @@ -0,0 +1,202 @@ +package integrationtest + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" + uvalidatortypes "github.com/pushchain/push-chain-node/x/uvalidator/types" +) + +// jailLikeSlashingBeginBlock reproduces exactly what x/slashing does to a +// validator during BeginBlock: it calls staking's Keeper.Jail, which runs +// jailValidator -> sets Validator.Jailed and deletes the power index, and +// never touches Validator.Status. +// +// Crucially it does NOT run staking's EndBlocker, so the bonded -> unbonding +// transition has not happened yet. That is the exact window every transaction +// in the block is processed in. +func jailLikeSlashingBeginBlock(t *testing.T, chainApp *app.ChainApp, ctx sdk.Context, val stakingtypes.Validator) stakingtypes.Validator { + t.Helper() + + consAddr, err := val.GetConsAddr() + require.NoError(t, err) + require.NoError(t, chainApp.StakingKeeper.Jail(ctx, consAddr)) + + valAddr, err := sdk.ValAddressFromBech32(val.OperatorAddress) + require.NoError(t, err) + jailed, err := chainApp.StakingKeeper.GetValidator(ctx, valAddr) + require.NoError(t, err) + return jailed +} + +// TestGetEligibleVoters_ExcludesSameBlockJailedValidator is the F-2026-18133 +// regression suite. +// +// Slashing jails in BeginBlock; staking moves the validator bonded -> +// unbonding only in EndBlocker. For the entire tx-processing phase in between, +// a jailed validator is both Jailed and IsBonded(). Before the fix that +// validator was snapshotted into a new ballot's EligibleVoters, so the frozen +// VotingThreshold ((2*N)/3 + 1) was computed on an inflated N while only N-1 +// signers could actually vote -- stranding the ballot at N <= 3. +func TestGetEligibleVoters_ExcludesSameBlockJailedValidator(t *testing.T) { + t.Run("precondition: a same-block jailed validator still reports IsBonded", func(t *testing.T) { + // This subtest asserts the SDK behaviour the finding depends on. If it + // ever stops holding, the fix below is redundant and this will say so. + chainApp, ctx, validators := setupQueryTest(t, 3) + for _, v := range validators { + setUVStatus(t, chainApp, ctx, v, uvalidatortypes.UVStatus_UV_STATUS_ACTIVE) + } + + jailed := jailLikeSlashingBeginBlock(t, chainApp, ctx, validators[0]) + + require.True(t, jailed.IsJailed(), "staking.Jail must set Validator.Jailed") + require.Equal(t, stakingtypes.Bonded, jailed.Status, + "staking.Jail must NOT touch Validator.Status before EndBlocker") + require.True(t, jailed.IsBonded(), + "IsBonded() is GetStatus()==Bonded, so a jailed validator still passes it -- "+ + "this is precisely why an explicit Jailed gate is required") + }) + + t.Run("jailed validator is excluded from the eligible-voter set", func(t *testing.T) { + chainApp, ctx, validators := setupQueryTest(t, 3) + for _, v := range validators { + setUVStatus(t, chainApp, ctx, v, uvalidatortypes.UVStatus_UV_STATUS_ACTIVE) + } + + before, err := chainApp.UvalidatorKeeper.GetEligibleVoters(ctx) + require.NoError(t, err) + require.Len(t, before, 3, "all three are eligible before the jail") + + jailLikeSlashingBeginBlock(t, chainApp, ctx, validators[0]) + + after, err := chainApp.UvalidatorKeeper.GetEligibleVoters(ctx) + require.NoError(t, err) + require.Len(t, after, 2, "the jailed validator must drop out of the eligible set") + for _, v := range after { + require.NotEqual(t, validators[0].OperatorAddress, v.IdentifyInfo.CoreValidatorAddress, + "jailed validator must not appear among eligible voters") + } + }) + + t.Run("PENDING_JOIN validator jailed in the same block is also excluded", func(t *testing.T) { + // setupQueryTest leaves every UV in PENDING_JOIN, which is an eligible + // lifecycle state. The Jailed gate must apply there too. + chainApp, ctx, validators := setupQueryTest(t, 3) + + jailLikeSlashingBeginBlock(t, chainApp, ctx, validators[2]) + + voters, err := chainApp.UvalidatorKeeper.GetEligibleVoters(ctx) + require.NoError(t, err) + require.Len(t, voters, 2) + for _, v := range voters { + require.NotEqual(t, validators[2].OperatorAddress, v.IdentifyInfo.CoreValidatorAddress) + } + }) + + t.Run("ballot created in the same block freezes a threshold computed on N-1", func(t *testing.T) { + // N = 3 is the worst reachable row from the finding: with the jailed + // validator counted the threshold is (2*3)/3+1 = 3 against only 2 + // possible signers -> permanently stranded. With it excluded the + // threshold is (2*2)/3+1 = 2 -> reachable. + chainApp, ctx, validators := setupQueryTest(t, 3) + for _, v := range validators { + setUVStatus(t, chainApp, ctx, v, uvalidatortypes.UVStatus_UV_STATUS_ACTIVE) + } + + // BeginBlock: slashing jails validators[0]. + jailLikeSlashingBeginBlock(t, chainApp, ctx, validators[0]) + + // Same block, tx-processing phase: a surviving UV observes an inbound, + // which creates the ballot and freezes EligibleVoters + VotingThreshold. + ballot := voteInboundAndLoadBallot(t, chainApp, ctx, validators[1], sameBlockJailInbound) + + // Headline assertion first: the frozen threshold must be computed on + // N-1. Everything else in this subtest is corroboration. + require.Equal(t, int64(2), ballot.VotingThreshold, + "threshold must be (2*2)/3+1 = 2 on the N-1 survivors, not (2*3)/3+1 = 3 on the inflated N") + + require.Len(t, ballot.EligibleVoters, 2, + "the jailed validator must not be snapshotted into the ballot") + require.NotContains(t, ballot.EligibleVoters, validators[0].OperatorAddress, + "jailed validator address must be absent from the frozen voter snapshot") + require.Contains(t, ballot.EligibleVoters, validators[1].OperatorAddress) + require.Contains(t, ballot.EligibleVoters, validators[2].OperatorAddress) + }) + + t.Run("the surviving validators can still finalize that ballot", func(t *testing.T) { + // The liveness half of the finding: with the jailed validator counted, + // the frozen threshold of 3 is unreachable by the 2 survivors and the + // ballot is stranded (only an admin MsgRecomputeBallotQuorum recovers + // it, and DefaultExpiryAfterBlocks = 100_000_000 means it never ages + // out on its own). + chainApp, ctx, validators := setupQueryTest(t, 3) + for _, v := range validators { + setUVStatus(t, chainApp, ctx, v, uvalidatortypes.UVStatus_UV_STATUS_ACTIVE) + } + + jailLikeSlashingBeginBlock(t, chainApp, ctx, validators[0]) + + // First survivor votes: creates the ballot, does not finalize it. + firstVoter, err := sdk.ValAddressFromBech32(validators[1].OperatorAddress) + require.NoError(t, err) + isFinalized, isNew, err := chainApp.UexecutorKeeper.VoteOnInboundBallot(ctx, firstVoter, sameBlockJailInbound) + require.NoError(t, err) + require.True(t, isNew, "the first vote must have created the ballot") + require.False(t, isFinalized, "one vote out of a threshold of two must not finalize") + + // Second (and last) survivor votes: this must be the finalizing vote. + secondVoter, err := sdk.ValAddressFromBech32(validators[2].OperatorAddress) + require.NoError(t, err) + isFinalized, isNew, err = chainApp.UexecutorKeeper.VoteOnInboundBallot(ctx, secondVoter, sameBlockJailInbound) + require.NoError(t, err) + require.False(t, isNew, "second vote must land on the existing ballot") + require.True(t, isFinalized, + "every non-jailed validator has now voted; if this is false the ballot is stranded "+ + "behind a threshold no reachable signer set can meet") + + ballotKey, err := uexecutortypes.GetInboundBallotKey(sameBlockJailInbound) + require.NoError(t, err) + ballot, err := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballotKey) + require.NoError(t, err) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PASSED, ballot.Status, + "the ballot must have reached a terminal PASSED status") + }) +} + +// sameBlockJailInbound is the observation used by the ballot subtests above. +var sameBlockJailInbound = uexecutortypes.Inbound{ + SourceChain: "eip155:11155111", + TxHash: "0xf18133jailedquorum", + LogIndex: "0", +} + +// voteInboundAndLoadBallot casts voter's inbound vote through the real +// uexecutor path (x/uexecutor/keeper/voting.go, the first of the seven +// GetEligibleVoters call sites) and returns the ballot it created. +func voteInboundAndLoadBallot( + t *testing.T, + chainApp *app.ChainApp, + ctx sdk.Context, + voter stakingtypes.Validator, + inbound uexecutortypes.Inbound, +) uvalidatortypes.Ballot { + t.Helper() + + voterAddr, err := sdk.ValAddressFromBech32(voter.OperatorAddress) + require.NoError(t, err) + + _, isNew, err := chainApp.UexecutorKeeper.VoteOnInboundBallot(ctx, voterAddr, inbound) + require.NoError(t, err) + require.True(t, isNew, "the vote must have created the ballot") + + ballotKey, err := uexecutortypes.GetInboundBallotKey(inbound) + require.NoError(t, err) + ballot, err := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballotKey) + require.NoError(t, err) + return ballot +} diff --git a/test/integration/uvalidator/recompute_ballot_quorum_test.go b/test/integration/uvalidator/recompute_ballot_quorum_test.go index 7966d9d2e..df66e477d 100644 --- a/test/integration/uvalidator/recompute_ballot_quorum_test.go +++ b/test/integration/uvalidator/recompute_ballot_quorum_test.go @@ -290,3 +290,257 @@ func TestRecomputeBallotQuorum_AdminAuth_AcceptsAdmin(t *testing.T) { require.NotNil(t, resp) require.Equal(t, int64(3), resp.NewEligibleCount) } + +// --------------------------------------------------------------------------- +// F-2026-18793 — RecomputeBallotQuorum is type-aware (default-deny allow-list) +// --------------------------------------------------------------------------- + +// makeTypedBallot builds a PENDING ballot of an arbitrary observation type with +// an explicit threshold, so TSS-style ballots (100% of the DKLS participant set, +// not 2/3+1) can be constructed exactly as x/utss creates them. +func makeTypedBallot( + t *testing.T, + ballotID string, + ballotType uvalidatortypes.BallotObservationType, + eligibleVoters []string, + votes []uvalidatortypes.VoteResult, + threshold int64, +) uvalidatortypes.Ballot { + t.Helper() + if len(votes) == 0 { + votes = make([]uvalidatortypes.VoteResult, len(eligibleVoters)) + } + return uvalidatortypes.Ballot{ + Id: ballotID, + BallotType: ballotType, + EligibleVoters: eligibleVoters, + Votes: votes, + VotingThreshold: threshold, + Status: uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, + BlockHeightCreated: 1, + BlockHeightExpiry: 100_000_000, + } +} + +// Hacken rec 3 — the headline case. A TSS key ballot is created with a 100% +// quorum over the DKLS participants (votesNeeded = len(Participants)). An admin +// recompute must be refused outright: it would drop the threshold from 5 to +// (2*3)/3+1 = 3 AND swap the participant list for the live UV set, manufacturing +// an attestation the DKLS run never produced. +// +// The state assertions run BEFORE the error assertion on purpose: require.Error +// aborts the test on failure, so an error-first ordering would never reach the +// checks that catch a refusal which had already mutated state. +func TestRecomputeBallotQuorum_TSSKeyBallot_Refused_StateUnchanged(t *testing.T) { + chainApp, ctx, validators := setupQueryTest(t, 5) + for _, v := range validators { + setUVStatus(t, chainApp, ctx, v, uvalidatortypes.UVStatus_UV_STATUS_ACTIVE) + } + + // 5 DKLS participants, 100% quorum (threshold 5), 4 of 5 votes cast. + participants := make([]string, len(validators)) + for i, v := range validators { + participants[i] = v.OperatorAddress + } + votes := []uvalidatortypes.VoteResult{ + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_SUCCESS, + uvalidatortypes.VoteResult_VOTE_RESULT_NOT_YET_VOTED, + } + ballot := makeTypedBallot(t, "tss-key-ballot", uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_TSS_KEY, participants, votes, 5) + require.NoError(t, chainApp.UvalidatorKeeper.Ballots.Set(ctx, ballot.Id, ballot)) + require.NoError(t, chainApp.UvalidatorKeeper.ActiveBallotIDs.Set(ctx, ballot.Id)) + + // Make the live UV set genuinely differ from the participant set, so an + // unguarded recompute would visibly rewrite both threshold and eligibles. + for i := 0; i < 2; i++ { + v := validators[i] + v.Status = stakingtypes.Unbonded + require.NoError(t, chainApp.StakingKeeper.SetValidator(ctx, v)) + } + eligibleNow, err := chainApp.UvalidatorKeeper.GetEligibleVoters(ctx) + require.NoError(t, err) + require.Len(t, eligibleNow, 3, "live UV set must differ from the DKLS participant set for this test to bite") + + _, _, _, _, _, recomputeErr := chainApp.UvalidatorKeeper.RecomputeBallotQuorum(ctx, ballot.Id) + + // --- state first --- + after, getErr := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballot.Id) + require.NoError(t, getErr) + require.Equal(t, int64(5), after.VotingThreshold, + "TSS threshold must stay at 100% of participants; a recompute would have set it to 3") + require.Equal(t, participants, after.EligibleVoters, + "the DKLS participant set must be byte-for-byte unchanged; a recompute would have swapped in the live UV set") + require.Equal(t, votes, after.Votes, "votes must be untouched") + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, after.Status, + "refused recompute must not finalize the ballot") + + // --- then the refusal itself --- + require.Error(t, recomputeErr, "recompute on a TSS_KEY ballot must be refused") + require.Contains(t, recomputeErr.Error(), "cannot be recomputed") + require.Contains(t, recomputeErr.Error(), "TSS_KEY") +} + +// The three allow-listed types are created from GetEligibleVoters() with a +// (2*N)/3+1 threshold, so recompute reproduces creation exactly for them and +// must keep working unchanged. +func TestRecomputeBallotQuorum_AllowedTypes_StillRecompute(t *testing.T) { + allowed := []uvalidatortypes.BallotObservationType{ + uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_INBOUND_TX, + uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_OUTBOUND_TX, + uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_FUND_MIGRATION, + } + + for _, bt := range allowed { + t.Run(bt.String(), func(t *testing.T) { + chainApp, ctx, validators := setupQueryTest(t, 5) + for _, v := range validators { + setUVStatus(t, chainApp, ctx, v, uvalidatortypes.UVStatus_UV_STATUS_ACTIVE) + } + + voterStrs := make([]string, len(validators)) + for i, v := range validators { + voterStrs[i] = v.OperatorAddress + } + ballot := makeTypedBallot(t, "allowed-"+bt.String(), bt, voterStrs, nil, 4) + require.NoError(t, chainApp.UvalidatorKeeper.Ballots.Set(ctx, ballot.Id, ballot)) + require.NoError(t, chainApp.UvalidatorKeeper.ActiveBallotIDs.Set(ctx, ballot.Id)) + + // Strand 3 → 2 eligible → threshold (2*2)/3+1 = 2. + for i := 0; i < 3; i++ { + v := validators[i] + v.Status = stakingtypes.Unbonded + require.NoError(t, chainApp.StakingKeeper.SetValidator(ctx, v)) + } + + oldEligible, newEligible, oldThreshold, newThreshold, newStatus, err := + chainApp.UvalidatorKeeper.RecomputeBallotQuorum(ctx, ballot.Id) + require.NoError(t, err, "%s must remain recomputable", bt.String()) + require.Equal(t, int64(5), oldEligible) + require.Equal(t, int64(2), newEligible) + require.Equal(t, int64(4), oldThreshold) + require.Equal(t, int64(2), newThreshold) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, newStatus) + + updated, err := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballot.Id) + require.NoError(t, err) + require.Equal(t, int64(2), updated.VotingThreshold) + require.Len(t, updated.EligibleVoters, 2) + require.Equal(t, []string{validators[3].OperatorAddress, validators[4].OperatorAddress}, updated.EligibleVoters) + }) + } +} + +// Pins the default-deny: UNSPECIFIED and a type value the switch has never seen +// are both refused. A future ballot type (e.g. READ_RESULT, which lands with the +// read-state branch) therefore inherits a refusal instead of silently inheriting +// the 2/3+1 formula. +func TestRecomputeBallotQuorum_UnrecognisedType_Refused(t *testing.T) { + cases := []struct { + name string + ballotType uvalidatortypes.BallotObservationType + }{ + {"unspecified", uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_UNSPECIFIED}, + {"future type not on the allow-list", uvalidatortypes.BallotObservationType(99)}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + chainApp, ctx, validators := setupQueryTest(t, 3) + for _, v := range validators { + setUVStatus(t, chainApp, ctx, v, uvalidatortypes.UVStatus_UV_STATUS_ACTIVE) + } + + voterStrs := []string{validators[0].OperatorAddress, validators[1].OperatorAddress, validators[2].OperatorAddress} + ballot := makeTypedBallot(t, "unknown-type-"+tc.name, tc.ballotType, voterStrs, nil, 7) + require.NoError(t, chainApp.UvalidatorKeeper.Ballots.Set(ctx, ballot.Id, ballot)) + require.NoError(t, chainApp.UvalidatorKeeper.ActiveBallotIDs.Set(ctx, ballot.Id)) + + _, _, _, _, _, recomputeErr := chainApp.UvalidatorKeeper.RecomputeBallotQuorum(ctx, ballot.Id) + + // State first — see the TSS test for why the ordering matters. + after, getErr := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballot.Id) + require.NoError(t, getErr) + require.Equal(t, int64(7), after.VotingThreshold, "threshold must be untouched by a refused recompute") + require.Equal(t, voterStrs, after.EligibleVoters, "eligible voters must be untouched by a refused recompute") + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, after.Status) + + require.Error(t, recomputeErr) + require.Contains(t, recomputeErr.Error(), "cannot be recomputed") + }) + } +} + +// The PENDING-only guard still runs before the type check, so a non-pending +// ballot reports the status problem rather than the type problem. +func TestRecomputeBallotQuorum_StatusGuardRunsBeforeTypeGuard(t *testing.T) { + chainApp, ctx, validators := setupQueryTest(t, 3) + + voterStrs := []string{validators[0].OperatorAddress, validators[1].OperatorAddress, validators[2].OperatorAddress} + ballot := makeTypedBallot(t, "finalized-tss-ballot", uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_TSS_KEY, voterStrs, nil, 3) + ballot.Status = uvalidatortypes.BallotStatus_BALLOT_STATUS_PASSED + require.NoError(t, chainApp.UvalidatorKeeper.Ballots.Set(ctx, ballot.Id, ballot)) + + _, _, _, _, _, err := chainApp.UvalidatorKeeper.RecomputeBallotQuorum(ctx, ballot.Id) + require.Error(t, err) + require.Contains(t, err.Error(), "not pending") +} + +// The zero-eligible → EXPIRED path is reached only by allow-listed types; a +// refused type is refused outright and is NOT auto-expired as a side effect. +func TestRecomputeBallotQuorum_ZeroEligible_AllowedExpires_RefusedDoesNot(t *testing.T) { + t.Run("allowed type still auto-expires", func(t *testing.T) { + chainApp, ctx, validators := setupQueryTest(t, 3) + for _, v := range validators { + setUVStatus(t, chainApp, ctx, v, uvalidatortypes.UVStatus_UV_STATUS_ACTIVE) + } + + voterStrs := []string{validators[0].OperatorAddress, validators[1].OperatorAddress, validators[2].OperatorAddress} + ballot := makeTypedBallot(t, "zero-eligible-fund-migration", uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_FUND_MIGRATION, voterStrs, nil, 3) + require.NoError(t, chainApp.UvalidatorKeeper.Ballots.Set(ctx, ballot.Id, ballot)) + require.NoError(t, chainApp.UvalidatorKeeper.ActiveBallotIDs.Set(ctx, ballot.Id)) + + for _, v := range validators { + v.Status = stakingtypes.Unbonded + require.NoError(t, chainApp.StakingKeeper.SetValidator(ctx, v)) + } + + _, newEligible, _, _, newStatus, err := chainApp.UvalidatorKeeper.RecomputeBallotQuorum(ctx, ballot.Id) + require.NoError(t, err) + require.Equal(t, int64(0), newEligible) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, newStatus) + + updated, _ := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballot.Id) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED, updated.Status) + }) + + t.Run("refused type stays pending", func(t *testing.T) { + chainApp, ctx, validators := setupQueryTest(t, 3) + for _, v := range validators { + setUVStatus(t, chainApp, ctx, v, uvalidatortypes.UVStatus_UV_STATUS_ACTIVE) + } + + voterStrs := []string{validators[0].OperatorAddress, validators[1].OperatorAddress, validators[2].OperatorAddress} + ballot := makeTypedBallot(t, "zero-eligible-tss", uvalidatortypes.BallotObservationType_BALLOT_OBSERVATION_TYPE_TSS_KEY, voterStrs, nil, 3) + require.NoError(t, chainApp.UvalidatorKeeper.Ballots.Set(ctx, ballot.Id, ballot)) + require.NoError(t, chainApp.UvalidatorKeeper.ActiveBallotIDs.Set(ctx, ballot.Id)) + + for _, v := range validators { + v.Status = stakingtypes.Unbonded + require.NoError(t, chainApp.StakingKeeper.SetValidator(ctx, v)) + } + + _, _, _, _, _, recomputeErr := chainApp.UvalidatorKeeper.RecomputeBallotQuorum(ctx, ballot.Id) + + after, getErr := chainApp.UvalidatorKeeper.Ballots.Get(ctx, ballot.Id) + require.NoError(t, getErr) + require.Equal(t, uvalidatortypes.BallotStatus_BALLOT_STATUS_PENDING, after.Status, + "a refused recompute must not expire the ballot as a side effect") + require.Equal(t, voterStrs, after.EligibleVoters) + require.Equal(t, int64(3), after.VotingThreshold) + + require.Error(t, recomputeErr) + }) +} diff --git a/test/utils/bytecode.go b/test/utils/bytecode.go index 3a892874c..d44f0e631 100644 --- a/test/utils/bytecode.go +++ b/test/utils/bytecode.go @@ -2,8 +2,6 @@ package utils const UEA_EVM_BYTECODE = "6080604052600436101561001a575b3615610018575f80fd5b005b5f3560e01c80631db61b54146100c95780635378c7aa146100c45780636eaf7ba3146100bf5780637d644a61146100ba5780638bcb651e146100b5578063a84813a4146100b0578063affed0e0146100ab578063c6f1b7e7146100a6578063f698da25146100a1578063f85135cc1461009c5763ffa1ad740361000e576108ac565b6107b3565b61068d565b610641565b610606565b610549565b610447565b6103f6565b610390565b61032f565b34610121575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101215760206040517f2aef22f9d7df5f9d21c56d14029233f3fdaa91917727e1eb68e504d27072d6cd8152f35b5f80fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6060810190811067ffffffffffffffff82111761016e57604052565b610125565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761016e57604052565b604051906101c461012083610173565b565b73ffffffffffffffffffffffffffffffffffffffff81160361012157565b602435906101c4826101c6565b35906101c4826101c6565b67ffffffffffffffff811161016e57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b929192610242826101fc565b916102506040519384610173565b829481845281830111610121578281602093845f960137010152565b9080601f830112156101215781602061028793359101610236565b90565b3590600282101561012157565b91909161012081840312610121576102ad6101b4565b926102b7826101f1565b84526020820135602085015260408201359167ffffffffffffffff8311610121576102ea6101009261032794830161026c565b6040860152606081013560608601526080810135608086015260a081013560a086015260c081013560c086015260e081013560e08601520161028a565b610100830152565b346101215760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101215760043567ffffffffffffffff8111610121576103886103836020923690600401610297565b610935565b604051908152f35b346101215760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101215760243567ffffffffffffffff8111610121576103ec6103e4602092369060040161026c565b600435610af9565b6040519015158152f35b34610121575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261012157602073ffffffffffffffffffffffffffffffffffffffff60065416604051908152f35b346101215760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101215760043567ffffffffffffffff81116101215760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc8236030112610121576040516104c081610152565b816004013567ffffffffffffffff8111610121576104e4906004369185010161026c565b8152602482013567ffffffffffffffff81116101215761050a906004369185010161026c565b6020820152604482013567ffffffffffffffff811161012157610018926004610536923692010161026c565b60408201526105436101e4565b90610ed6565b346101215760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101215760043567ffffffffffffffff8111610121576101207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc8236030112610121576024359067ffffffffffffffff821161012157366023830112156101215781600401359067ffffffffffffffff8211610121573660248385010111610121576024610018930190600401611101565b34610121575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610121576020600554604051908152f35b34610121575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101215760206040517314191ea54b4c176fcf86f51b0fac7cb1e71df7d78152f35b34610121575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610121576020610388611496565b5f5b8381106106d65750505f910152565b81810151838201526020016106c7565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602093610722815180928187528780880191016106c5565b0116010190565b906102879160208152604061078061074d84516060602086015260808501906106e6565b60208501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe085830301848601526106e6565b9201519060607fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0828503019101526106e6565b34610121575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610121576060604080516107f081610152565b828152826020820152015261085960405161080a81610152565b6040516108218161081a816112d3565b0382610173565b81526040516108338161081a81611390565b60208201526040516108488161081a81611413565b604082015260405191829182610729565b0390f35b6040519061086c602083610173565b5f8252565b60405190610880604083610173565b600582527f312e302e300000000000000000000000000000000000000000000000000000006020830152565b34610121575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610121576108596108e5610871565b6040519182916020835260208301906106e6565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b6002111561093057565b6108f9565b805173ffffffffffffffffffffffffffffffffffffffff1690610a4e6020820151610a2260408401516020815191012093606081015190608081015160a08201516005549161010060e08501519401519461098f86610926565b61099886610926565b604051998a9860208a019c8d9793610120979360ff97939b9a96929b73ffffffffffffffffffffffffffffffffffffffff6101408c019d7f1d8b43e5066bd20bfdacf7b8f4790c0309403b18434e3699ce3c5e57502ed8c48d521660208c015260408b015260608a0152608089015260a088015260c087015260e086015261010085015216910152565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282610173565b519020610a22610aa2610a5f611496565b9260405192839160208301958690916042927f19010000000000000000000000000000000000000000000000000000000000008352600283015260228201520190565b51902090565b90600182811c92168015610aef575b6020831014610ac257565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b91607f1691610ab7565b610b0f91610b0691611858565b9092919261189c565b73ffffffffffffffffffffffffffffffffffffffff610b2f600354610aa8565b6003601f8211610ba8575b547fffffffffffffffffffffffffffffffffffffffff00000000000000000000000081169160148110610b73575b505060601c91161490565b7fffffffffffffffffffffffffffffffffffffffff0000000000000000000000009250829060140360031b1b16165f80610b68565b5060035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b610b3a565b818110610bde575050565b5f8155600101610bd3565b90601f8211610bf6575050565b6101c49160015f5260205f20906020601f840160051c83019310610c22575b601f0160051c0190610bd3565b9091508190610c15565b9190601f8111610c3b57505050565b6101c4925f5260205f20906020601f840160051c83019310610c2257601f0160051c0190610bd3565b90815167ffffffffffffffff811161016e57610c8c81610c85600254610aa8565b6002610c2c565b602092601f8211600114610cea57610cda929382915f92610cdf575b50507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b600255565b015190505f80610ca8565b60025f527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08216937f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace915f5b868110610d8b5750836001959610610d54575b505050811b01600255565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690555f8080610d49565b91926020600181928685015181550194019201610d36565b90815167ffffffffffffffff811161016e57610dcb81610dc4600354610aa8565b6003610c2c565b602092601f8211600114610e1d57610e18929382915f92610cdf5750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b600355565b60035f527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08216937fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b915f5b868110610ebe5750836001959610610e87575b505050811b01600355565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690555f8080610e7c565b91926020600181928685015181550194019201610e69565b919060045460ff81166110d9577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff006001911617600455825192835167ffffffffffffffff811161016e57610f3481610f2f600154610aa8565b610be9565b6020601f8211600114610ffa5773ffffffffffffffffffffffffffffffffffffffff9392610fa083610fb8946040946101c4999a5f92610cdf5750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b6001555b610fb16020820151610c64565b0151610da3565b1673ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000006006541617600655565b60015f527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08216957fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6965f5b8181106110c15750836101c4979860409473ffffffffffffffffffffffffffffffffffffffff989794610fb8976001951061108a575b505050811b01600155610fa4565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690555f808061107c565b83830151895560019098019760209384019301611046565b7f69783db7000000000000000000000000000000000000000000000000000000005f5260045ffd5b919060025f54146112ab5760025f557314191ea54b4c176fcf86f51b0fac7cb1e71df7d73303611254575b5050611139903690610297565b60e0810151801515908161124a575b506112225761116161115c60055460010190565b600555565b604081015161116f81611963565b156111fc575061117e90611c11565b905b156111c557507f3c72595b43537fb9a702f683573f82d3b3ad19487fd74cbb94728a41ec76d1cb6111b96005546040519182918261164d565b0390a16101c460015f55565b8051156111d457805190602001fd5b7facfdb444000000000000000000000000000000000000000000000000000000005f5260045ffd5b611205906119bc565b156112195761121390611a9a565b90611180565b61121390611a3f565b7ff87d9271000000000000000000000000000000000000000000000000000000005f5260045ffd5b905042115f611148565b90611271611277926112696103833687610297565b923691610236565b90610af9565b15611283575f8061112c565b7fc7dbd31d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f3ee5aeb5000000000000000000000000000000000000000000000000000000005f5260045ffd5b6001545f92916112e282610aa8565b808252916001811690811561135657506001146112fd575050565b60015f9081529293509091907fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf65b83831061133c575060209250010190565b60018160209294939454838587010152019101919061132b565b60209495507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0091509291921683830152151560051b010190565b6002545f929161139f82610aa8565b808252916001811690811561135657506001146113ba575050565b60025f9081529293509091907f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace5b8383106113f9575060209250010190565b6001816020929493945483858701015201910191906113e8565b6003545f929161142282610aa8565b8082529160018116908115611356575060011461143d575050565b60035f9081529293509091907fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b83831061147c575060209250010190565b60018160209294939454838587010152019101919061146b565b6040516114a68161081a81611390565b80515f9181156115c957905f915b81831061153557505050610a22610aa26114cc610871565b60208151910120604051928391602083019530918791606091949373ffffffffffffffffffffffffffffffffffffffff9160808501967f2aef22f9d7df5f9d21c56d14029233f3fdaa91917727e1eb68e504d27072d6cd86526020860152604085015216910152565b9091926115b46001916115ae6115a86115a261158361157d6115578b8a611749565b517fff000000000000000000000000000000000000000000000000000000000000001690565b60f81c90565b9361159d60ff8616603081101590816115bd575b5061175f565b6117f1565b9261180c565b60ff1690565b9061184b565b930191906114b4565b6039915011155f611597565b60846040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602160248201527f456d70747920737472696e672063616e6e6f7420626520636f6e76657274656460448201527f2e000000000000000000000000000000000000000000000000000000000000006064820152fd5b9190604083525f6003549061166182610aa8565b918260408701526001811690815f146116dc5750600114611686575b60209150930152565b5060035f9081527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b8282106116c5575060209150840160600161167d565b8054828701606001526020909101906001016116af565b60209390849350604092507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166060880152151560051b8601010161167d565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b90815181101561175a570160200190565b61171c565b1561176657565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4e6f6e2d64696769742063686172616374657220666f756e642e0000000000006044820152fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b90600a820291808304600a149015171561180757565b6117c4565b60ff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd09116019060ff821161180757565b906004820180921161180757565b9190820180921161180757565b8151919060418303611888576118819250602082015190606060408401519301515f1a90611d65565b9192909190565b50505f9160029190565b6004111561093057565b6118a581611892565b806118ae575050565b6118b781611892565b600181036118e7577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b6118f081611892565b6002810361192457507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b80611930600392611892565b146119385750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b60048151106119b757602001517fffffffff00000000000000000000000000000000000000000000000000000000167f2cc2842d000000000000000000000000000000000000000000000000000000001490565b505f90565b60048151106119b757602001517fffffffff00000000000000000000000000000000000000000000000000000000167fcac656d6000000000000000000000000000000000000000000000000000000001490565b3d15611a3a573d90611a21826101fc565b91611a2f6040519384610173565b82523d5f602084013e565b606090565b5f809173ffffffffffffffffffffffffffffffffffffffff8151166040602083015192015191602083519301915af1611a76611a10565b9091565b908160209103126101215751610287816101c6565b6040513d5f823e3d90fd5b80513073ffffffffffffffffffffffffffffffffffffffff90911603611ba15760200151611ba15760046020611b01611ae860065473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1690565b604051928380927ff8ba7e030000000000000000000000000000000000000000000000000000000082525afa908115611bf8575f91611bc9575b5073ffffffffffffffffffffffffffffffffffffffff811615611ba1575f809160405160208101907f52fa5c2200000000000000000000000000000000000000000000000000000000825260048152611b95602482610173565b51915af4611a76611a10565b7fae962d4e000000000000000000000000000000000000000000000000000000005f5260045ffd5b611beb915060203d602011611bf1575b611be38183610173565b810190611a7a565b5f611b3b565b503d611bd9565b611a8f565b805182101561175a5760209160051b010190565b604001519081517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc810190811161180757611c64611c4e826101fc565b91611c5c6040519384610173565b8083526101fc565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06020830193013684375f5b8251811015611cc55780611cb2611557611cac60019461183d565b88611749565b5f1a611cbe8286611749565b5301611c91565b509250906020611cd9928051010190611df4565b5f5b8151811015611d58575f80611d0e611cf38486611bfd565b515173ffffffffffffffffffffffffffffffffffffffff1690565b6020611d1a8587611bfd565b510151906040611d2a8688611bfd565b51015191602083519301915af1611d3f611a10565b908015611d50575050600101611cdb565b939092509050565b505060019061028761085d565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411611de9579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15611bf8575f5173ffffffffffffffffffffffffffffffffffffffff811615611ddf57905f905f90565b505f906001905f90565b5050505f9160039190565b6020818303126101215780519067ffffffffffffffff821161012157019080601f830112156101215781519167ffffffffffffffff831161016e578260051b9160405193611e456020850186610173565b8452602080850193830101918183116101215760208101935b838510611e6d57505050505090565b845167ffffffffffffffff811161012157820160607fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe082860301126101215760405190611eb982610152565b6020810151611ec7816101c6565b825260408101516020830152606081015167ffffffffffffffff81116101215760209101019184601f8401121561012157825191611f04836101fc565b611f116040519182610173565b838152866020858701011161012157611f346020959486958680850191016106c5565b6040820152815201940193611e5e56fea26469706673582212202c80640fc4c48d71a0d3494f62b95ab7ca574ca348f3f949610dec582ff771d464736f6c634300081a0033" -const UEA_SVM_BYTECODE = "6080604052600436101561001a575b3615610018575f80fd5b005b5f3560e01c8063196359b3146100d95780635378c7aa146100d45780636eaf7ba3146100cf5780637d644a61146100ca5780638bcb651e146100c557806397b3a757146100c0578063a84813a4146100bb578063affed0e0146100b6578063c6f1b7e7146100b1578063f698da25146100ac578063f85135cc146100a75763ffa1ad740361000e576108f5565b6107fc565b6106d6565b61068a565b61064f565b610592565b610559565b610457565b610406565b6103a0565b61033f565b34610131575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101315760206040517f3aefc31558906b9b2c54de94f82a9b2455c24b4ba2b642ebb545ea2cc64a1e4b8152f35b5f80fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6060810190811067ffffffffffffffff82111761017e57604052565b610135565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761017e57604052565b604051906101d461012083610183565b565b73ffffffffffffffffffffffffffffffffffffffff81160361013157565b602435906101d4826101d6565b35906101d4826101d6565b67ffffffffffffffff811161017e57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b9291926102528261020c565b916102606040519384610183565b829481845281830111610131578281602093845f960137010152565b9080601f830112156101315781602061029793359101610246565b90565b3590600282101561013157565b91909161012081840312610131576102bd6101c4565b926102c782610201565b84526020820135602085015260408201359167ffffffffffffffff8311610131576102fa6101009261033794830161027c565b6040860152606081013560608601526080810135608086015260a081013560a086015260c081013560c086015260e081013560e08601520161029a565b610100830152565b346101315760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101315760043567ffffffffffffffff81116101315761039861039360209236906004016102a7565b610979565b604051908152f35b346101315760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101315760243567ffffffffffffffff8111610131576103fc6103f4602092369060040161027c565b6004356114cf565b6040519015158152f35b34610131575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013157602073ffffffffffffffffffffffffffffffffffffffff60065416604051908152f35b346101315760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101315760043567ffffffffffffffff81116101315760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc8236030112610131576040516104d081610162565b816004013567ffffffffffffffff8111610131576104f4906004369185010161027c565b8152602482013567ffffffffffffffff81116101315761051a906004369185010161027c565b6020820152604482013567ffffffffffffffff811161013157610018926004610546923692010161027c565b60408201526105536101f4565b90610e40565b34610131575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013157602060405160ca8152f35b346101315760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101315760043567ffffffffffffffff8111610131576101207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc8236030112610131576024359067ffffffffffffffff821161013157366023830112156101315781600401359067ffffffffffffffff821161013157366024838501011161013157602461001893019060040161106b565b34610131575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610131576020600554604051908152f35b34610131575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101315760206040517314191ea54b4c176fcf86f51b0fac7cb1e71df7d78152f35b34610131575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610131576020610398611400565b5f5b83811061071f5750505f910152565b8181015183820152602001610710565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f60209361076b8151809281875287808801910161070e565b0116010190565b90610297916020815260406107c9610796845160606020860152608085019061072f565b60208501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0858303018486015261072f565b9201519060607fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08285030191015261072f565b34610131575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101315760606040805161083981610162565b82815282602082015201526108a260405161085381610162565b60405161086a816108638161123d565b0382610183565b815260405161087c81610863816112fa565b6020820152604051610891816108638161137d565b604082015260405191829182610772565b0390f35b604051906108b5602083610183565b5f8252565b604051906108c9604083610183565b600582527f312e302e300000000000000000000000000000000000000000000000000000006020830152565b34610131575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610131576108a261092e6108ba565b60405191829160208352602083019061072f565b6002111561094c57565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b805173ffffffffffffffffffffffffffffffffffffffff1690610a926020820151610a6660408401516020815191012093606081015190608081015160a08201516005549161010060e0850151940151946109d386610942565b6109dc86610942565b604051998a9860208a019c8d9793610120979360ff97939b9a96929b73ffffffffffffffffffffffffffffffffffffffff6101408c019d7f1d8b43e5066bd20bfdacf7b8f4790c0309403b18434e3699ce3c5e57502ed8c48d521660208c015260408b015260608a0152608089015260a088015260c087015260e086015261010085015216910152565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282610183565b519020610a66610ae6610aa3611400565b9260405192839160208301958690916042927f19010000000000000000000000000000000000000000000000000000000000008352600283015260228201520190565b51902090565b90600182811c92168015610b33575b6020831014610b0657565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b91607f1691610afb565b818110610b48575050565b5f8155600101610b3d565b90601f8211610b60575050565b6101d49160015f5260205f20906020601f840160051c83019310610b8c575b601f0160051c0190610b3d565b9091508190610b7f565b9190601f8111610ba557505050565b6101d4925f5260205f20906020601f840160051c83019310610b8c57601f0160051c0190610b3d565b90815167ffffffffffffffff811161017e57610bf681610bef600254610aec565b6002610b96565b602092601f8211600114610c5457610c44929382915f92610c49575b50507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b600255565b015190505f80610c12565b60025f527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08216937f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace915f5b868110610cf55750836001959610610cbe575b505050811b01600255565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690555f8080610cb3565b91926020600181928685015181550194019201610ca0565b90815167ffffffffffffffff811161017e57610d3581610d2e600354610aec565b6003610b96565b602092601f8211600114610d8757610d82929382915f92610c495750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b600355565b60035f527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08216937fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b915f5b868110610e285750836001959610610df1575b505050811b01600355565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690555f8080610de6565b91926020600181928685015181550194019201610dd3565b919060045460ff8116611043577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff006001911617600455825192835167ffffffffffffffff811161017e57610e9e81610e99600154610aec565b610b53565b6020601f8211600114610f645773ffffffffffffffffffffffffffffffffffffffff9392610f0a83610f22946040946101d4999a5f92610c495750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b6001555b610f1b6020820151610bce565b0151610d0d565b1673ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000006006541617600655565b60015f527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08216957fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6965f5b81811061102b5750836101d4979860409473ffffffffffffffffffffffffffffffffffffffff989794610f229760019510610ff4575b505050811b01600155610f0e565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690555f8080610fe6565b83830151895560019098019760209384019301610fb0565b7f69783db7000000000000000000000000000000000000000000000000000000005f5260045ffd5b919060025f54146112155760025f557314191ea54b4c176fcf86f51b0fac7cb1e71df7d733036111be575b50506110a39036906102a7565b60e081015180151590816111b4575b5061118c576110cb6110c660055460010190565b600555565b60408101516110d98161166d565b1561116657506110e89061191e565b905b1561112f57507f3c72595b43537fb9a702f683573f82d3b3ad19487fd74cbb94728a41ec76d1cb6111236005546040519182918261159e565b0390a16101d460015f55565b80511561113e57805190602001fd5b7facfdb444000000000000000000000000000000000000000000000000000000005f5260045ffd5b61116f906116c6565b156111835761117d90611775565b906110ea565b61117d9061171a565b7ff87d9271000000000000000000000000000000000000000000000000000000005f5260045ffd5b905042115f6110b2565b906111db6111e1926111d361039336876102a7565b923691610246565b906114cf565b156111ed575f80611096565b7fa764e90c000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f3ee5aeb5000000000000000000000000000000000000000000000000000000005f5260045ffd5b6001545f929161124c82610aec565b80825291600181169081156112c05750600114611267575050565b60015f9081529293509091907fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf65b8383106112a6575060209250010190565b600181602092949394548385870101520191019190611295565b60209495507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0091509291921683830152151560051b010190565b6002545f929161130982610aec565b80825291600181169081156112c05750600114611324575050565b60025f9081529293509091907f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace5b838310611363575060209250010190565b600181602092949394548385870101520191019190611352565b6003545f929161138c82610aec565b80825291600181169081156112c057506001146113a7575050565b60035f9081529293509091907fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b8383106113e6575060209250010190565b6001816020929493945483858701015201910191906113d5565b6114086108ba565b6020815191012060405160208101917f3aefc31558906b9b2c54de94f82a9b2455c24b4ba2b642ebb545ea2cc64a1e4b8352604082015260806060820152610ae68161145660a082016112fa565b306080830152037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282610183565b3d156114b2573d906114998261020c565b916114a76040519384610183565b82523d5f602084013e565b606090565b90816020910312610131575180151581036101315790565b5f91610a6661154f849360405192839160208301957fbbdd82070000000000000000000000000000000000000000000000000000000087526060602485015261151a6084850161137d565b9160448501527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc84830301606485015261072f565b519060ca5afa61155d611488565b90156115765780602080610297935183010191016114b7565b7ffd23ff64000000000000000000000000000000000000000000000000000000005f5260045ffd5b9190604083525f600354906115b282610aec565b918260408701526001811690815f1461162d57506001146115d7575b60209150930152565b5060035f9081527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b82821061161657506020915084016060016115ce565b805482870160600152602090910190600101611600565b60209390849350604092507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166060880152151560051b860101016115ce565b60048151106116c157602001517fffffffff00000000000000000000000000000000000000000000000000000000167f2cc2842d000000000000000000000000000000000000000000000000000000001490565b505f90565b60048151106116c157602001517fffffffff00000000000000000000000000000000000000000000000000000000167fcac656d6000000000000000000000000000000000000000000000000000000001490565b5f809173ffffffffffffffffffffffffffffffffffffffff8151166040602083015192015191602083519301915af1611751611488565b9091565b908160209103126101315751610297816101d6565b6040513d5f823e3d90fd5b80513073ffffffffffffffffffffffffffffffffffffffff9091160361187c576020015161187c57600460206117dc6117c360065473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1690565b604051928380927ff8ba7e030000000000000000000000000000000000000000000000000000000082525afa9081156118d3575f916118a4575b5073ffffffffffffffffffffffffffffffffffffffff81161561187c575f809160405160208101907ff6829c3200000000000000000000000000000000000000000000000000000000825260048152611870602482610183565b51915af4611751611488565b7fae962d4e000000000000000000000000000000000000000000000000000000005f5260045ffd5b6118c6915060203d6020116118cc575b6118be8183610183565b810190611755565b5f611816565b503d6118b4565b61176a565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b80518210156119195760209160051b010190565b6118d8565b604001519081517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc8101908111611a985761197161195b8261020c565b916119696040519384610183565b80835261020c565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06020830193013684375f5b82518110156119f857806119e56119bf6119b9600194611ac5565b88611ad8565b517fff000000000000000000000000000000000000000000000000000000000000001690565b5f1a6119f18286611ad8565b530161199e565b509250906020611a0c928051010190611ae9565b5f5b8151811015611a8b575f80611a41611a268486611905565b515173ffffffffffffffffffffffffffffffffffffffff1690565b6020611a4d8587611905565b510151906040611a5d8688611905565b51015191602083519301915af1611a72611488565b908015611a83575050600101611a0e565b939092509050565b50506001906102976108a6565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b9060048201809211611ad357565b611a98565b908151811015611919570160200190565b6020818303126101315780519067ffffffffffffffff821161013157019080601f830112156101315781519167ffffffffffffffff831161017e578260051b9160405193611b3a6020850186610183565b8452602080850193830101918183116101315760208101935b838510611b6257505050505090565b845167ffffffffffffffff811161013157820160607fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe082860301126101315760405190611bae82610162565b6020810151611bbc816101d6565b825260408101516020830152606081015167ffffffffffffffff81116101315760209101019184601f8401121561013157825191611bf98361020c565b611c066040519182610183565b838152866020858701011161013157611c2960209594869586808501910161070e565b6040820152815201940193611b5356fea26469706673582212201ccb86d8429e38b976f7d8a6f0b6fbb7c3cbc64fa3443fe78c9f0ff9b68a483464736f6c634300081a0033" - const UEA_PROXY_BYTECODE = "608060405260043610610028575f3560e01c806323efa7ec14610032578063aaf10f4214610051575b6100306100a8565b005b34801561003d575f80fd5b5061003061004c366004610368565b6100ba565b34801561005c575f80fd5b507f868a771a75a4aa6c2be13e9a9617cb8ea240ed84a3a90c8469537393ec3e115d5460405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b6100b86100b36102cc565b61034a565b565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000810460ff16159067ffffffffffffffff165f811580156101045750825b90505f8267ffffffffffffffff1660011480156101205750303b155b90508115801561012e575080155b15610165576040517ff92ee8a900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b84547fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000016600117855583156101c65784547fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff16680100000000000000001785555b5f6101ef7f868a771a75a4aa6c2be13e9a9617cb8ea240ed84a3a90c8469537393ec3e115d5490565b905073ffffffffffffffffffffffffffffffffffffffff81161561023f576040517fae962d4e00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b867f868a771a75a4aa6c2be13e9a9617cb8ea240ed84a3a90c8469537393ec3e115d555083156102c45784547fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050565b5f806102f67f868a771a75a4aa6c2be13e9a9617cb8ea240ed84a3a90c8469537393ec3e115d5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610345576040517fae962d4e00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b919050565b365f80375f80365f845af43d5f803e808015610364573d5ff35b3d5ffd5b5f60208284031215610378575f80fd5b813573ffffffffffffffffffffffffffffffffffffffff8116811461039b575f80fd5b939250505056fea2646970667358221220c4b8f9457567bdcd08b95faef7df86de4e9daead65e2db22018126d9eb77d85864736f6c634300081a0033" const HANDLER_CONTRACT_BYTECODE = "608080604052600436101561001c575b50361561001a575f80fd5b005b5f905f3560e01c908162bc574b14614b895750806301ffc9a714614ab5578063022d63fb14614a98578063049bf04c146149955780630615f0a2146149355780630840ba721461445e57806308af7f86146143fe5780630aa6220b146142bc5780631a4e49d4146142935780631a873ce4146142605780631c90064a14614200578063248a9ca3146141ae57806325c36c751461416a5780632f2ff15d146140df57806336568abe14613ef65780633f4ba83a14613e1b5780634243fbaa14613dd1578063447146a214613d875780634b1d2eeb14613d475780634d20d0f814613d145780634d49fbf314613b105780634eb7d1a114613ad05780634f882f3314613a6b57806355b487f1146139f9578063580fc6a9146138cc5780635b549182146138995780635c975abb14613858578063634e93da146136c35780636435967b14612eec578063649a5ec714612bc257806364f10e5014612b7857806368c70c9e14612b2d5780636ca752e314612ae25780636f419e31146129fd5780637574d9a0146129e0578063780ad8271461235d57806378a881271461233a578063801bee15146120d157806381fbadad146120b35780638377e2301461207f5780638456cb5914611f0c5780638468c05b14611ea557806384ef8ffc14611d6e57806387525a3714611d735780638da5cb5b14611d6e5780638f247b8c14611cc85780638f40e8f514611cab57806391d1485414611c34578063a1eda53c14611bae578063a217fddf14611b92578063a5172ddb14611b47578063a861469f14611afd578063ad14d38514611ab3578063b5d8349f14611a52578063b6322a9f14611a07578063b6aa5ce3146119eb578063bb88f3d9146119a0578063bfc7a3e4146118d2578063c6b54b3c14611897578063c6f1b7e714611846578063cc8463c81461181b578063cd20c6e8146117d6578063cefc14291461161f578063cf6eefb714611593578063d29c5c1c14611558578063d547741f146114c1578063d602b9fd14611426578063dbc1b464146113c5578063dd19e755146111af578063e229cd7614611192578063e63ab1e914611157578063ec87621c1461111c578063eefbaa3514610ffd578063f5b541a614610fc2578063f6b9ec7c14610fa5578063f881446714610820578063f8c8765e146104b8578063fb46e99d1461049a578063fc6b5de81461043c5763fcb6c2300361000f5734610439576040600319360112610439576103ac614cf6565b6024359081151580920361043557602073ffffffffffffffffffffffffffffffffffffffff7f42cc79f957a9535dc49c660eec62747fb540bef0dd29cd7f6c0a810816af4cff92169283855260038252604085207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0081541660ff8316179055604051908152a280f35b8280fd5b80fd5b5034610439576020600319360112610439576004359067ffffffffffffffff8211610439576020610487816104743660048701614c7e565b8160405193828580945193849201614d5f565b8101601281520301902054604051908152f35b50346104395780600319360112610439576020600654604051908152f35b5034610439576080600319360112610439576104d2614cf6565b6104da614d19565b6104e2614d3c565b6064359173ffffffffffffffffffffffffffffffffffffffff831680930361081c577ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00549367ffffffffffffffff60ff8660401c1615951680159081610814575b600114908161080a575b159081610801575b506107d95773ffffffffffffffffffffffffffffffffffffffff92918380928760017fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000007ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005416177ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0055610763575b6105eb615874565b6105f3615874565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00556106203361505c565b50167fffffffffffffffffffffffff0000000000000000000000000000000000000000600a541617600a55167fffffffffffffffffffffffff00000000000000000000000000000000000000006007541617600755167fffffffffffffffffffffffff000000000000000000000000000000000000000060085416176008557fffffffffffffffffffffffff000000000000000000000000000000000000000060095416176009556106cf5780f35b7fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054167ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00557fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602060405160018152a180f35b680100000000000000007fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005416177ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00556105e3565b6004867ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b9050155f610555565b303b15915061054d565b869150610543565b8480fd5b5060a060031936011261043957610835614cf6565b9061083e614dc3565b9160443591606435936084359273ffffffffffffffffffffffffffffffffffffffff8416928385036104395773ffffffffffffffffffffffffffffffffffffffff601054163303610f7d576108916154d7565b61089961552a565b8691839773ffffffffffffffffffffffffffffffffffffffff8216948515610f55578615610f55573415610f2d578815610f2d5762ffffff1615610eeb575b15610ec3575b824211610e9b5761099960208973ffffffffffffffffffffffffffffffffffffffff6007541673ffffffffffffffffffffffffffffffffffffffff600a54169488861090815f14610e945786915b15610e8c57905b604051958694859384937f1698ee820000000000000000000000000000000000000000000000000000000085526004850191604091949373ffffffffffffffffffffffffffffffffffffffff62ffffff9281606087019816865216602085015216910152565b03915afa908115610d41579073ffffffffffffffffffffffffffffffffffffffff918491610e5d575b501615610e3557803b15610d3d5781600491604051928380927fd0e30db000000000000000000000000000000000000000000000000000000000825234905af18015610dd057908291610e20575b5050610a4f73ffffffffffffffffffffffffffffffffffffffff600a5416349073ffffffffffffffffffffffffffffffffffffffff6008541690615760565b62ffffff73ffffffffffffffffffffffffffffffffffffffff600a54169760405198610a7a8a614bb9565b89528460208a0152169182604089015230606089015260808801528560a08801523460c08801528060e08801526020610b6661010473ffffffffffffffffffffffffffffffffffffffff6008541699846040519b8c9485937fdb3e2198000000000000000000000000000000000000000000000000000000008552600485019073ffffffffffffffffffffffffffffffffffffffff60e0809282815116855282602082015116602086015262ffffff60408201511660408601528260608201511660608601526080810151608086015260a081015160a086015260c081015160c0860152015116910152565b5af1968715610e13578197610ddb575b50610bb273ffffffffffffffffffffffffffffffffffffffff600a541673ffffffffffffffffffffffffffffffffffffffff600854169061565a565b6040517f42966c6800000000000000000000000000000000000000000000000000000000815286600482015260208160248185885af1908115610dd0578291610da1575b5015610d795786340394348611610d4c57873403610c78575b505060606040967f01fd625a5ce1109c10761818e2ef64ea92cd4966d78086d37e5a4b50e322687892885191825287602083015288820152a360017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f005582519182526020820152f35b73ffffffffffffffffffffffffffffffffffffffff600a5416803b15610435578280916024604051809481937f2e1a7d4d0000000000000000000000000000000000000000000000000000000083528c60048401525af18015610d41579183918893610d23575b5081809381925af1610cef614e7b565b5015610cfb5780610c0f565b807f90b8ec180000000000000000000000000000000000000000000000000000000060049252fd5b610d309193508290614c03565b610d3d578186915f610cdf565b5080fd5b6040513d85823e3d90fd5b6024827f4e487b710000000000000000000000000000000000000000000000000000000081526011600452fd5b807f66b2a6fe0000000000000000000000000000000000000000000000000000000060049252fd5b610dc3915060203d602011610dc9575b610dbb8183614c03565b810190614ed9565b5f610bf6565b503d610db1565b6040513d84823e3d90fd5b9096506020813d602011610e0b575b81610df760209383614c03565b81010312610e075751955f610b76565b5f80fd5b3d9150610dea565b50604051903d90823e3d90fd5b81610e2a91614c03565b61043957805f610a10565b6004827f76ecffc0000000000000000000000000000000000000000000000000000000008152fd5b610e7f915060203d602011610e85575b610e778183614c03565b810190614fac565b5f6109c2565b503d610e6d565b508590610933565b809161092c565b6004827f1ab7da6b000000000000000000000000000000000000000000000000000000008152fd5b9150600654603c810290808204603c1490151715610d4c57610ee5904261504f565b916108de565b8483526004602052604083205462ffffff169850886108d8575b6004837f3733548a000000000000000000000000000000000000000000000000000000008152fd5b6004847f1f2a2005000000000000000000000000000000000000000000000000000000008152fd5b6004847fd92e233d000000000000000000000000000000000000000000000000000000008152fd5b807fbce361b00000000000000000000000000000000000000000000000000000000060049252fd5b503461043957806003193601126104395760206040516101f48152f35b503461043957806003193601126104395760206040517f97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b9298152f35b50346104395760606003193601126104395760043567ffffffffffffffff8111610d3d5761102f903690600401614c7e565b60243560443581156110f457916110dd917f5e41bf0052b493123a63e4e0d9095ed4324108e489d58c9a0948b2be366ac8c6936110b8604051838551916020818189019461107e818388614d5f565b8101600b8152030190205582604051602081885161109d818388614d5f565b81016011815203019020556040519182918651928391614d5f565b8101906012825260208142930301902055604051938493608085526080850190614d80565b91602084015260408301524260608301520390a180f35b6004847fe661aed0000000000000000000000000000000000000000000000000000000008152fd5b503461043957806003193601126104395760206040517f241ecf16d79d0f8dbfb92cbc07fe17840425976cf0667f022fe9877caa831b088152f35b503461043957806003193601126104395760206040517f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a8152f35b503461043957806003193601126104395760206040516113888152f35b5034610439576040600319360112610439576111c9614cf6565b602435604080516111da8282614c03565b600f815260208101927f6569703135353a3131313535313131000000000000000000000000000000000084528251865b600f81106113b257506014600f820152602f90205490811561138a578061134b5750915b73ffffffffffffffffffffffffffffffffffffffff8151602081855161125581838b614d5f565b8101600c815203019020541693841561132357602061127e918351809381928751928391614d5f565b8101600b815203019020549081156112fb57806112f1949596976112a185615421565b73ffffffffffffffffffffffffffffffffffffffff6112c08886614ec6565b991681526013602052205490805197889788526020880152860152606085015260c0608085015260c0840190614d80565b9060a08301520390f35b6004877fe661aed0000000000000000000000000000000000000000000000000000000008152fd5b6004877fd92e233d000000000000000000000000000000000000000000000000000000008152fd5b929080841061135a575061122e565b86604491857fff632bea000000000000000000000000000000000000000000000000000000008352600452602452fd5b6004877fba496b84000000000000000000000000000000000000000000000000000000008152fd5b806020809286010151818401520161120a565b5034610439576020600319360112610439576004359067ffffffffffffffff821161043957602073ffffffffffffffffffffffffffffffffffffffff611412826104743660048801614c7e565b8101600c8152030190205416604051908152f35b503461043957806003193601126104395761143f6152ab565b7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840080547fffffffffffff0000000000000000000000000000000000000000000000000000811690915560a01c65ffffffffffff1661149a5780f35b7f8886ebfc4259abdbc16601dd8fb5678e54878f47b3c34836cfc51154a96051098180a180f35b5034610439576040600319360112610439576004356114de614d19565b90801561153057908161152761152261152c945f527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800602052600160405f20015490565b61539b565b615aa7565b5080f35b6004837f3fc3c27a000000000000000000000000000000000000000000000000000000008152fd5b503461043957806003193601126104395760206040517fe53b6cbb4204145187ea4c9b95f311e9ee4f2690cdb9b3a219f863f3a71e06c98152f35b5034610439578060031936011261043957604065ffffffffffff6115f97feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698400549065ffffffffffff73ffffffffffffffffffffffffffffffffffffffff83169260a01c1690565b73ffffffffffffffffffffffffffffffffffffffff849392935193168352166020820152f35b50346104395780600319360112610439577feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d86984005473ffffffffffffffffffffffffffffffffffffffff1633036117aa577feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d86984005473ffffffffffffffffffffffffffffffffffffffff81169060a01c65ffffffffffff16801580156117a0575b611775575061170b9061170573ffffffffffffffffffffffffffffffffffffffff7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840154166159dd565b5061505c565b507fffffffffffff00000000000000000000000000000000000000000000000000007feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840054167feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d86984005580f35b7f19ca5ebb000000000000000000000000000000000000000000000000000000008352600452602482fd5b50428110156116bc565b807fc22c8022000000000000000000000000000000000000000000000000000000006024925233600452fd5b503461043957602060031936011261043957604060209173ffffffffffffffffffffffffffffffffffffffff61180a614cf6565b168152601383522054604051908152f35b50346104395780600319360112610439576020611836614fd8565b65ffffffffffff60405191168152f35b5034610439578060031936011261043957602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461043957806003193601126104395760206040517f0f6ee822d2ee125e4ce6edbae6c10a76fa9fd4617e0399ab687226fa334421008152f35b50346104395760206003193601126104395773ffffffffffffffffffffffffffffffffffffffff611901614cf6565b611909615313565b1680156119785773ffffffffffffffffffffffffffffffffffffffff601054827fffffffffffffffffffffffff0000000000000000000000000000000000000000821617601055167fda4281cd6f2da5f8862b210df953c55b2e8c441b67821264ef4a35ca833fc2a78380a380f35b6004827fd92e233d000000000000000000000000000000000000000000000000000000008152fd5b5034610439576020600319360112610439576004359067ffffffffffffffff82116104395760206119d8816104743660048701614c7e565b8101601681520301902054604051908152f35b5034610439578060031936011261043957602060405160648152f35b5034610439576020600319360112610439576004359067ffffffffffffffff8211610439576020611a3f816104743660048701614c7e565b8101601581520301902054604051908152f35b5034610439576020600319360112610439576004359067ffffffffffffffff821161043957602073ffffffffffffffffffffffffffffffffffffffff611a9f826104743660048801614c7e565b8101600d8152030190205416604051908152f35b50346104395760206003193601126104395760ff604060209273ffffffffffffffffffffffffffffffffffffffff611ae9614cf6565b168152600384522054166040519015158152f35b50346104395760206003193601126104395762ffffff604060209273ffffffffffffffffffffffffffffffffffffffff611b35614cf6565b16815260048452205416604051908152f35b5034610439576020600319360112610439576004359067ffffffffffffffff8211610439576020611b7f816104743660048701614c7e565b8101600b81520301902054604051908152f35b5034610439578060031936011261043957602090604051908152f35b50346104395780600319360112610439577feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698401548060d01c9182151580611c2a575b15611c21575060a01c65ffffffffffff165b6040805165ffffffffffff928316815292909116602083015290f35b0390f35b91505080611c01565b5042831015611bef565b50346104395760406003193601126104395773ffffffffffffffffffffffffffffffffffffffff6040611c65614d19565b9260043581527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b6268006020522091165f52602052602060ff60405f2054166040519015158152f35b50346104395780600319360112610439576020604051610bb88152f35b50346104395760206003193601126104395773ffffffffffffffffffffffffffffffffffffffff611cf7614cf6565b611cff615313565b1680156119785773ffffffffffffffffffffffffffffffffffffffff600a54827fffffffffffffffffffffffff0000000000000000000000000000000000000000821617600a55167f3aa820195fb2daaa2fb7669944e7c9eccf99303478e74f09887bd8fe649b9c588380a380f35b614e29565b503461043957604060031936011261043957611d8d614cf6565b73ffffffffffffffffffffffffffffffffffffffff611daa614d19565b91611db3615313565b169081158015611e87575b611e5f578173ffffffffffffffffffffffffffffffffffffffff6040927f37af3ddd829f67f886ff225a9b652d2104f68d4cba4cbc989264fa5ef7621ac1947fffffffffffffffffffffffff0000000000000000000000000000000000000000600754161760075516807fffffffffffffffffffffffff0000000000000000000000000000000000000000600854161760085582519182526020820152a180f35b6004837fd92e233d000000000000000000000000000000000000000000000000000000008152fd5b5073ffffffffffffffffffffffffffffffffffffffff811615611dbe565b5034610439577f8529702b0bf1b5d9d01b0c119b695d9365ef62dc9f5e6a87c24f77bb38fd6518611ed536614cc4565b90816040516020818451611eec8183858901614d5f565b8101601681520301902055611f0660405192839283614eaa565b0390a180f35b50346104395780600319360112610439577f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a81527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b6268006020526040812073ffffffffffffffffffffffffffffffffffffffff33165f5260205260ff60405f2054161561202f57611f996154d7565b60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff007fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f033005416177fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f03300557f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a2586020604051338152a180f35b807fe2517d3f0000000000000000000000000000000000000000000000000000000060449252336004527f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a602452fd5b5034610439578060031936011261043957602073ffffffffffffffffffffffffffffffffffffffff60105416604051908152f35b50346104395780600319360112610439576020600e54604051908152f35b50346104395760606003193601126104395760043567ffffffffffffffff8111610d3d57612103903690600401614c7e565b61210b614d19565b60443562ffffff81169182820361081c5773ffffffffffffffffffffffffffffffffffffffff8116801561231257916020916121ee9373ffffffffffffffffffffffffffffffffffffffff600754169173ffffffffffffffffffffffffffffffffffffffff600a541691821091825f1461230b5780925b1561230357506040517f1698ee8200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff92831660048201529116602482015262ffffff90921660448301529092839190829081906064820190565b03915afa80156122f85773ffffffffffffffffffffffffffffffffffffffff9185916122d9575b50169081156122b157916122a0917f21e3c1439de176cb39006e603b26a8d890fe2267c804597e40d2954871141d7d93604051602081855161225a8183858a01614d5f565b8101600d815203019020827fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055604051938493606085526060850190614d80565b91602084015260408301520390a180f35b6004847f76ecffc0000000000000000000000000000000000000000000000000000000008152fd5b6122f2915060203d602011610e8557610e778183614c03565b5f612215565b6040513d86823e3d90fd5b905090610933565b8192612182565b6004867fd92e233d000000000000000000000000000000000000000000000000000000008152fd5b50346104395761235a61234c36614dd5565b916123556154d7565b614ef1565b80f35b50346104395760c060031936011261043957612377614cf6565b60243590612383614d3c565b906064359262ffffff8416908185036127c75760843560a435936123a56154d7565b6123ad61552a565b6123b88684836155a1565b8473ffffffffffffffffffffffffffffffffffffffff821697888a52600360205260ff60408b205416156129b8579415612977575b15612922575b8442116128fa576020846124b0928a73ffffffffffffffffffffffffffffffffffffffff600754169173ffffffffffffffffffffffffffffffffffffffff600a541690818d10805f146128f35781935b501561230357506040517f1698ee8200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff92831660048201529116602482015262ffffff90921660448301529092839190829081906064820190565b03915afa80156128795773ffffffffffffffffffffffffffffffffffffffff9189916128d4575b5016156128ac578015612884576040517f47e7ef24000000000000000000000000000000000000000000000000000000008152306004820152602481018390526020816044818b8b5af190811561287957889161285a575b5015612832576125588273ffffffffffffffffffffffffffffffffffffffff6008541688615760565b62ffffff73ffffffffffffffffffffffffffffffffffffffff600a5416936040519461258386614bb9565b8886526020808701918252929091166040808701828152306060890190815260808901998a5260a0890188815260c08a0188815260e08b018f815260085495517f414bf3890000000000000000000000000000000000000000000000000000000081529b5173ffffffffffffffffffffffffffffffffffffffff90811660048e01529751881660248d0152935162ffffff1660448c01529151861660648b0152995160848a0152985160a4890152975160c48801529651821660e48701529585916101049183918c91165af19283156128275787936127f3575b5082106127cb5761268673ffffffffffffffffffffffffffffffffffffffff600854168661565a565b8573ffffffffffffffffffffffffffffffffffffffff600a5416803b15610d3d578180916024604051809481937f2e1a7d4d0000000000000000000000000000000000000000000000000000000083528960048401525af18015610dd0576127b2575b5080808085885af16126f9614e7b565b501561278a57927ff5d6ca9b390b5271e0cbb3d43b4d708d5b17804cb81a4c65e027226d87ccf0e2949273ffffffffffffffffffffffffffffffffffffffff9260c09584600a54169060405196875260208701526040860152606085015260808401521660a0820152a160017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f005580f35b6004867f90b8ec18000000000000000000000000000000000000000000000000000000008152fd5b816127bc91614c03565b6127c757855f6126e9565b8580fd5b6004867f8199f5f3000000000000000000000000000000000000000000000000000000008152fd5b9092506020813d60201161281f575b8161280f60209383614c03565b81010312610e075751915f61265d565b3d9150612802565b6040513d89823e3d90fd5b6004877f66b2a6fe000000000000000000000000000000000000000000000000000000008152fd5b612873915060203d602011610dc957610dbb8183614c03565b5f61252f565b6040513d8a823e3d90fd5b6004877f1f2a2005000000000000000000000000000000000000000000000000000000008152fd5b6004877f76ecffc0000000000000000000000000000000000000000000000000000000008152fd5b6128ed915060203d602011610e8557610e778183614c03565b5f6124d7565b8293612443565b6004887f1ab7da6b000000000000000000000000000000000000000000000000000000008152fd5b9350600654603c810290808204603c149015171561294a57612944904261504f565b936123f3565b6024887f4e487b710000000000000000000000000000000000000000000000000000000081526011600452fd5b8789526004602052604089205462ffffff169450846123ed576004897f3733548a000000000000000000000000000000000000000000000000000000008152fd5b60048a7f4e38f95a000000000000000000000000000000000000000000000000000000008152fd5b503461043957806003193601126104395760206040516127108152f35b503461043957604060031936011261043957612a17614cf6565b73ffffffffffffffffffffffffffffffffffffffff612a34614dc3565b9116908115611e5f5762ffffff16606481141580612ad6575b80612aca575b80612abe575b610f055760207f5734dc08ec8c21bd34e0f102d90ea2d1a9dbdcf23e787dc8744d2d0dd227fc73918385526004825260408520817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000825416179055604051908152a280f35b50612710811415612a59565b50610bb8811415612a53565b506101f4811415612a4d565b5034610439576020600319360112610439576004359067ffffffffffffffff8211610439576020612b1a816104743660048701614c7e565b8101601481520301902054604051908152f35b5034610439576020600319360112610439576004359067ffffffffffffffff8211610439576020612b65816104743660048701614c7e565b8101601181520301902054604051908152f35b503461043957612b9b612b8a36614dd5565b91612b936154d7565b61235561552a565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f005580f35b50346104395760206003193601126104395760043565ffffffffffff811680820361043557612bef6152ab565b612bf842615b7a565b9065ffffffffffff612c08614fd8565b1680821115612e8457507ff1038c18cf84a56e432fdbfaf746924b7ea511dfe03a6506a0ceba4888788d9b929165ffffffffffff826206978080612c569510911802620697801816906154b9565b907feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698401548060d01c80612dc3575b5050612d13817fffffffffffff000000000000ffffffffffffffffffffffffffffffffffffffff79ffffffffffff00000000000000000000000000000000000000007feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698401549260a01b169116177feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840155565b612da18279ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff00000000000000000000000000000000000000000000000000007feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698401549260d01b169116177feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840155565b6040805165ffffffffffff928316815292909116602083015281908101611f06565b421115612e5a5779ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff00000000000000000000000000000000000000000000000000007feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698400549260301b169116177feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698400555b5f80612c83565b507f2b1fa2edafe6f7b9e97c1a9e0c3660e645beb2dcaa2d45bdbf9beaf5472e1ec58480a1612e53565b0365ffffffffffff8111612ebf577ff1038c18cf84a56e432fdbfaf746924b7ea511dfe03a6506a0ceba4888788d9b9291612c5691906154b9565b6024847f4e487b710000000000000000000000000000000000000000000000000000000081526011600452fd5b5034610e075760c0600319360112610e0757612f06614cf6565b602435612f11614d3c565b6064358015908115809103610e075760843562ffffff811690818103610e075760a43573ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016330361369b57612f796154d7565b612f8161552a565b612f8c86888a6155a1565b5f94156130d85750506040517f47e7ef2400000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff85166004820152602481018690529050602081806044810103818a73ffffffffffffffffffffffffffffffffffffffff8b165af19081156128275787916130b9575b50156130915773ffffffffffffffffffffffffffffffffffffffff7ffa6ff091ec99bdfd127d51e7786764f2ff7e39f866bbb2a2996e1597052641e49360609382935b6040519788526020880152604087015216941692a360017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f005580f35b6004867f66b2a6fe000000000000000000000000000000000000000000000000000000008152fd5b6130d2915060203d602011610dc957610dbb8183614c03565b5f613012565b809192939450156136735773ffffffffffffffffffffffffffffffffffffffff871691825f52600360205260ff60405f2054161561364b579215613609575b600654603c810290808204603c14901517156135dc57613137904261504f565b8042116135b4576131ef60208573ffffffffffffffffffffffffffffffffffffffff6007541673ffffffffffffffffffffffffffffffffffffffff600a541680881090815f146135ad578d915b156135a5576040517f1698ee8200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff92831660048201529116602482015262ffffff90921660448301529092839190829081906064820190565b03915afa80156134b05773ffffffffffffffffffffffffffffffffffffffff915f91613586575b50161561355e576040517f47e7ef24000000000000000000000000000000000000000000000000000000008152306004820152602481018890526020816044815f885af19081156134b0575f9161353f575b5015613517576132918773ffffffffffffffffffffffffffffffffffffffff6008541685615760565b62ffffff73ffffffffffffffffffffffffffffffffffffffff600a541694604051956132bc87614bb9565b858752602087015216604085015230606085015260808401528560a08401528060c08401525f60e084015260206133a661010473ffffffffffffffffffffffffffffffffffffffff60085416955f60405197889485937f414bf389000000000000000000000000000000000000000000000000000000008552600485019073ffffffffffffffffffffffffffffffffffffffff60e0809282815116855282602082015116602086015262ffffff60408201511660408601528260608201511660608601526080810151608086015260a081015160a086015260c081015160c0860152015116910152565b5af19283156134b0575f936134e3575b5082106134bb576133e09073ffffffffffffffffffffffffffffffffffffffff600854169061565a565b73ffffffffffffffffffffffffffffffffffffffff600a5416803b15610e07575f80916024604051809481937f2e1a7d4d0000000000000000000000000000000000000000000000000000000083528760048401525af180156134b05761349b575b508580808084875af1613453614e7b565b501561278a5773ffffffffffffffffffffffffffffffffffffffff7ffa6ff091ec99bdfd127d51e7786764f2ff7e39f866bbb2a2996e1597052641e493606093829390613055565b6134a89196505f90614c03565b5f945f613442565b6040513d5f823e3d90fd5b7f8199f5f3000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d60201161350f575b816134ff60209383614c03565b81010312610e075751915f6133b6565b3d91506134f2565b7f66b2a6fe000000000000000000000000000000000000000000000000000000005f5260045ffd5b613558915060203d602011610dc957610dbb8183614c03565b5f613268565b7f76ecffc0000000000000000000000000000000000000000000000000000000005f5260045ffd5b61359f915060203d602011610e8557610e778183614c03565b5f613216565b508c90610933565b8091613184565b7f1ab7da6b000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b9150805f52600460205262ffffff60405f2054169182613117577f3733548a000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f4e38f95a000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f22c50cbf000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f53e51723000000000000000000000000000000000000000000000000000000005f5260045ffd5b34610e07576020600319360112610e07576136dc614cf6565b6136e46152ab565b7f3377dc44241e779dd06afab5b788a35ca5f3b778836e2990bdb26a2a4b2e5ed6602061372161371342615b7a565b61371b614fd8565b906154b9565b65ffffffffffff73ffffffffffffffffffffffffffffffffffffffff6137897feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698400549065ffffffffffff73ffffffffffffffffffffffffffffffffffffffff83169260a01c1690565b96905016947feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840054867fffffffffffff000000000000000000000000000000000000000000000000000079ffffffffffff00000000000000000000000000000000000000008660a01b16921617177feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698400551661382f575b65ffffffffffff60405191168152a2005b7f8886ebfc4259abdbc16601dd8fb5678e54878f47b3c34836cfc51154a96051095f80a161381e565b34610e07575f600319360112610e0757602060ff7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f0330054166040519015158152f35b34610e07575f600319360112610e0757602073ffffffffffffffffffffffffffffffffffffffff60075416604051908152f35b34610e07576040600319360112610e075760043567ffffffffffffffff8111610e07576138fd903690600401614c7e565b73ffffffffffffffffffffffffffffffffffffffff61391a614d19565b1680156139d1577f0c7d242571a289736ea536c54ebe236d31ba62abfd4f22b8d54d2988dc0dd949916139c6915f6139a76020604051855190828181890193613964818387614d5f565b8101600c815203019020857fffffffffffffffffffffffff0000000000000000000000000000000000000000825416179055604051809381928851928391614d5f565b8101600b81520301902055604051928392604084526040840190614d80565b9060208301520390a1005b7fd92e233d000000000000000000000000000000000000000000000000000000005f5260045ffd5b34610e07576040600319360112610e0757613a12614cf6565b73ffffffffffffffffffffffffffffffffffffffff1660243581156139d15760207f911a025fb070fa2a29c37a3bf4c00d16acf15583cd050f17bdbacbab7e72320391835f52601382528060405f2055604051908152a2005b34610e07577f57ad858a99d9aee6f1fd395e454bb1659eb8500ccb081c729a103dc2247ba3a4613a9a36614cc4565b90816040516020818451613ab18183858901614d5f565b8101601581520301902055613acb60405192839283614eaa565b0390a1005b34610e07576020600319360112610e07576004355f526002602052602073ffffffffffffffffffffffffffffffffffffffff60405f205416604051908152f35b34610e07576020600319360112610e075760045f73ffffffffffffffffffffffffffffffffffffffff613b41614cf6565b16604051928380927fa0c50b690000000000000000000000000000000000000000000000000000000082525afa9081156134b0575f91613c9b575b5060405181519060208181850193613b95818387614d5f565b8101601581520301902054918215613c735773ffffffffffffffffffffffffffffffffffffffff6040516020818451613bcf818389614d5f565b8101600c81520301902054169182156139d1576020613bf991604051809381928651928391614d5f565b8101600b81520301902054908115613c4b57611c1d91613c1882615421565b613c228582614ec6565b94604051958695865260208601526040850152606084015260a0608084015260a0830190614d80565b7fe661aed0000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f9502a873000000000000000000000000000000000000000000000000000000005f5260045ffd5b90503d805f833e613cac8183614c03565b810190602081830312610e075780519067ffffffffffffffff8211610e07570181601f82011215610e07578051613ce281614c44565b92613cf06040519485614c03565b81845260208284010111610e0757613d0e9160208085019101614d5f565b81613b7c565b34610e07575f600319360112610e0757602073ffffffffffffffffffffffffffffffffffffffff60095416604051908152f35b34610e07576020600319360112610e07576004355f526001602052602073ffffffffffffffffffffffffffffffffffffffff60405f205416604051908152f35b34610e07576020600319360112610e075760043567ffffffffffffffff8111610e0757613dbe602061047481933690600401614c7e565b8101601881520301902054604051908152f35b34610e07576020600319360112610e075760043567ffffffffffffffff8111610e0757613e08602061047481933690600401614c7e565b8101601781520301902054604051908152f35b34610e07575f600319360112610e0757613e33615313565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f033005460ff811615613ece577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00167fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f03300557f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa6020604051338152a1005b7f8dfc202b000000000000000000000000000000000000000000000000000000005f5260045ffd5b34610e07576040600319360112610e0757600435613f12614d19565b811580614089575b613f6d575b3373ffffffffffffffffffffffffffffffffffffffff821603613f455761001a91615aa7565b7f6697b232000000000000000000000000000000000000000000000000000000005f5260045ffd5b7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d86984005465ffffffffffff60a082901c169073ffffffffffffffffffffffffffffffffffffffff1615801590614079575b8015614067575b61403357507fffffffffffff000000000000ffffffffffffffffffffffffffffffffffffffff7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840054167feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840055613f1f565b65ffffffffffff907f19ca5ebb000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b504265ffffffffffff82161015613fc3565b5065ffffffffffff811615613fbc565b5073ffffffffffffffffffffffffffffffffffffffff7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698401541673ffffffffffffffffffffffffffffffffffffffff821614613f1a565b34610e07576040600319360112610e07576004356140fb614d19565b8115614142578161413d61152261001a945f527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800602052600160405f20015490565b6151d0565b7f3fc3c27a000000000000000000000000000000000000000000000000000000005f5260045ffd5b34610e07576020600319360112610e07577f424b07caa75ce8e1c3985f334273f957db9ce138de114e48e50d8240d4d7300b602060043580600655604051908152a1005b34610e07576020600319360112610e075760206141f86004355f527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800602052600160405f20015490565b604051908152f35b34610e07577f507273e640affcefbad497278a9b264a65c62c430dd92d24dd0d58595529539c61422f36614cc4565b908160405160208184516142468183858901614d5f565b8101601781520301902055613acb60405192839283614eaa565b34610e07575f600319360112610e0757602073ffffffffffffffffffffffffffffffffffffffff600a5416604051908152f35b34610e07576020600319360112610e07576004355f525f602052602060405f2054604051908152f35b34610e07575f600319360112610e07576142d46152ab565b7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698401548060d01c8061433d575b7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698401805473ffffffffffffffffffffffffffffffffffffffff169055005b4211156143d45779ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff00000000000000000000000000000000000000000000000000007feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698400549260301b169116177feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698400555b8080614300565b507f2b1fa2edafe6f7b9e97c1a9e0c3660e645beb2dcaa2d45bdbf9beaf5472e1ec55f80a16143cd565b34610e07577f882f47825d4043cd04a564cad4f524a7fe00a604ae024c23dbc8065b77668b4761442d36614cc4565b908160405160208184516144448183858901614d5f565b8101601481520301902055613acb60405192839283614eaa565b34610e07576040600319360112610e0757614477614cf6565b61447f614d19565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005460ff8160401c1690811561491f575b506148f7577ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0080547fffffffffffffffffffffffffffffffffffffffffffffff000000000000000000166801000000000000000217905573ffffffffffffffffffffffffffffffffffffffff8216158080156148d9575b6139d157614533615874565b61453b615874565b6148ad57614813614819927c015180000000000000000000000000000000000000000000000000000079ffffffffffffffffffffffffffffffffffffffffffffffffffff7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d86984005416177feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698400556145ce8161505c565b507fe53b6cbb4204145187ea4c9b95f311e9ee4f2690cdb9b3a219f863f3a71e06c95f8181527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b6268006020527fe3e0a451f4d1f165b1071ce13280fc8d1dfe94c2663176890cdb309635afb92180547f0f6ee822d2ee125e4ce6edbae6c10a76fa9fd4617e0399ab687226fa3344210091829055909290917fbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff9080a47f97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b9295f8181527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b6268006020527f448256db8f8fb95ee3eaaf89c1051414494e85cebb6057fcf996cc3d0ccfb45780547f0f6ee822d2ee125e4ce6edbae6c10a76fa9fd4617e0399ab687226fa3344210091829055909290917fbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff9080a47f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a5f8181527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b6268006020527f75442b0a96088b5456bc4ed01394c96a4feec0f883c9494257d76b96ab1c9b6c80547f0f6ee822d2ee125e4ce6edbae6c10a76fa9fd4617e0399ab687226fa3344210091829055909290917fbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff9080a461480381615128565b5061480d81615152565b5061517c565b506151a6565b507fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054167ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00557fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602060405160028152a1005b7fc22c8022000000000000000000000000000000000000000000000000000000005f525f60045260245ffd5b5073ffffffffffffffffffffffffffffffffffffffff821615614527565b7ff92ee8a9000000000000000000000000000000000000000000000000000000005f5260045ffd5b6002915067ffffffffffffffff161015836144b0565b34610e07577f2d57170c913282d2886a5ace7e18bed8b1c53a069f2698ae9e048bd501f3af3b61496436614cc4565b9081604051602081845161497b8183858901614d5f565b8101601881520301902055613acb60405192839283614eaa565b34610e07576040600319360112610e075760043573ffffffffffffffffffffffffffffffffffffffff8116809103610e075760243581156139d1578015614a7057478111614a48575f80808084865af16149ed614e7b565b5015614a205760207f8fcd857d6e51ec18860a6c21c1772d69a342074fbae5225c8f11f8cdf00a049691604051908152a2005b7f90b8ec18000000000000000000000000000000000000000000000000000000005f5260045ffd5b7ff4d678b8000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f1f2a2005000000000000000000000000000000000000000000000000000000005f5260045ffd5b34610e07575f600319360112610e07576020604051620697808152f35b34610e07576020600319360112610e07576004357fffffffff000000000000000000000000000000000000000000000000000000008116809103610e0757807f314987860000000000000000000000000000000000000000000000000000000060209214908115614b2c575b506040519015158152f35b7f7965db0b00000000000000000000000000000000000000000000000000000000811491508115614b5f575b5082614b21565b7f01ffc9a70000000000000000000000000000000000000000000000000000000091501482614b58565b34610e07575f600319360112610e075760209073ffffffffffffffffffffffffffffffffffffffff600854168152f35b610100810190811067ffffffffffffffff821117614bd657604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117614bd657604052565b67ffffffffffffffff8111614bd657601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b81601f82011215610e0757803590614c9582614c44565b92614ca36040519485614c03565b82845260208383010111610e0757815f926020809301838601378301015290565b6040600319820112610e07576004359067ffffffffffffffff8211610e0757614cef91600401614c7e565b9060243590565b6004359073ffffffffffffffffffffffffffffffffffffffff82168203610e0757565b6024359073ffffffffffffffffffffffffffffffffffffffff82168203610e0757565b6044359073ffffffffffffffffffffffffffffffffffffffff82168203610e0757565b5f5b838110614d705750505f910152565b8181015183820152602001614d61565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602093614dbc81518092818752878088019101614d5f565b0116010190565b6024359062ffffff82168203610e0757565b6003196060910112610e075760043573ffffffffffffffffffffffffffffffffffffffff81168103610e0757906024359060443573ffffffffffffffffffffffffffffffffffffffff81168103610e075790565b34610e07575f600319360112610e0757602073ffffffffffffffffffffffffffffffffffffffff7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d86984015416604051908152f35b3d15614ea5573d90614e8c82614c44565b91614e9a6040519384614c03565b82523d5f602084013e565b606090565b929190614ec1602091604086526040860190614d80565b930152565b818102929181159184041417156135dc57565b90816020910312610e0757518015158103610e075790565b90602091614f7293614f048184846155a1565b5f73ffffffffffffffffffffffffffffffffffffffff6040518097819682957f47e7ef24000000000000000000000000000000000000000000000000000000008452600484016020909392919373ffffffffffffffffffffffffffffffffffffffff60408201951681520152565b0393165af19081156134b0575f91614f8d575b501561351757565b614fa6915060203d602011610dc957610dbb8183614c03565b5f614f85565b90816020910312610e07575173ffffffffffffffffffffffffffffffffffffffff81168103610e075790565b7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698401548060d01c8015159081615045575b501561501c5760a01c65ffffffffffff1690565b507feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d86984005460d01c90565b905042115f615008565b919082018092116135dc57565b73ffffffffffffffffffffffffffffffffffffffff7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d86984015416614142578061511f6151259273ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000007feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d86984015416177feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840155565b5f6158cb565b90565b615125907f0f6ee822d2ee125e4ce6edbae6c10a76fa9fd4617e0399ab687226fa334421006158cb565b615125907fe53b6cbb4204145187ea4c9b95f311e9ee4f2690cdb9b3a219f863f3a71e06c96158cb565b615125907f97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b9296158cb565b615125907f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a6158cb565b9081156151e1575b615125916158cb565b73ffffffffffffffffffffffffffffffffffffffff7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698401541661414257615125916152a48273ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000007feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d86984015416177feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840155565b91506151d8565b335f9081527fb7db2dd08fcb62d0c9e08c51941cae53c267786a0b75803fb7960902fc8ef97d602052604090205460ff16156152e357565b7fe2517d3f000000000000000000000000000000000000000000000000000000005f52336004525f60245260445ffd5b335f9081527f448256db8f8fb95ee3eaaf89c1051414494e85cebb6057fcf996cc3d0ccfb456602052604090205460ff161561534b57565b7fe2517d3f000000000000000000000000000000000000000000000000000000005f52336004527f97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b92960245260445ffd5b805f527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b62680060205260405f2073ffffffffffffffffffffffffffffffffffffffff33165f5260205260ff60405f205416156153f25750565b7fe2517d3f000000000000000000000000000000000000000000000000000000005f523360045260245260445ffd5b60405181519060208181850193615439818387614d5f565b81016016815203019020549182156154b45761546391602091604051938492839251928391614d5f565b8101601281520301902054615478828261504f565b4211615482575050565b7f2056463c000000000000000000000000000000000000000000000000000000005f526004524260245260445260645ffd5b505050565b9065ffffffffffff8091169116019065ffffffffffff82116135dc57565b60ff7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f03300541661550257565b7fd93c0665000000000000000000000000000000000000000000000000000000005f5260045ffd5b60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0054146155795760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b7f3ee5aeb5000000000000000000000000000000000000000000000000000000005f5260045ffd5b90919073ffffffffffffffffffffffffffffffffffffffff16156139d15773ffffffffffffffffffffffffffffffffffffffff1680156139d15773ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168114908115615650575b506156285715614a7057565b7f82d5d76a000000000000000000000000000000000000000000000000000000005f5260045ffd5b905030145f61561c565b6040519060205f73ffffffffffffffffffffffffffffffffffffffff828501957f095ea7b30000000000000000000000000000000000000000000000000000000087521694856024860152816044860152604485526156ba606486614c03565b84519082855af15f513d8261572e575b5050156156d657505050565b61572761572c93604051907f095ea7b300000000000000000000000000000000000000000000000000000000602083015260248201525f604482015260448152615721606482614c03565b82615bc2565b615bc2565b565b909150615758575073ffffffffffffffffffffffffffffffffffffffff81163b15155b5f806156ca565b600114615751565b6040517f095ea7b300000000000000000000000000000000000000000000000000000000602080830191825273ffffffffffffffffffffffffffffffffffffffff85166024840152604480840196909652948252929390925f906157c5606486614c03565b84519082855af15f513d82615842575b5050156157e157505050565b61572761572c9373ffffffffffffffffffffffffffffffffffffffff604051917f095ea7b30000000000000000000000000000000000000000000000000000000060208401521660248201525f604482015260448152615721606482614c03565b90915061586c575073ffffffffffffffffffffffffffffffffffffffff81163b15155b5f806157d5565b600114615865565b60ff7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005460401c16156158a357565b7fd7e6bcf8000000000000000000000000000000000000000000000000000000005f5260045ffd5b805f527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b62680060205260405f2073ffffffffffffffffffffffffffffffffffffffff83165f5260205260ff60405f205416155f146159d757805f527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b62680060205260405f2073ffffffffffffffffffffffffffffffffffffffff83165f5260205260405f2060017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0082541617905573ffffffffffffffffffffffffffffffffffffffff339216907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d5f80a4600190565b50505f90565b6151259073ffffffffffffffffffffffffffffffffffffffff7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698401541673ffffffffffffffffffffffffffffffffffffffff821614615a3c575b5f615c49565b7fffffffffffffffffffffffff00000000000000000000000000000000000000007feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840154167feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840155615a36565b9061512591801580615b24575b15615c49577fffffffffffffffffffffffff00000000000000000000000000000000000000007feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840154167feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d869840155615c49565b5073ffffffffffffffffffffffffffffffffffffffff7feef3dac4538c82c8ace4063ab0acd2d15cdb5883aa1dff7c2673abb3d8698401541673ffffffffffffffffffffffffffffffffffffffff831614615ab4565b65ffffffffffff8111615b925765ffffffffffff1690565b7f6dfcc650000000000000000000000000000000000000000000000000000000005f52603060045260245260445ffd5b905f602091828151910182855af1156134b0575f513d615c40575073ffffffffffffffffffffffffffffffffffffffff81163b155b615bfe5750565b73ffffffffffffffffffffffffffffffffffffffff907f5274afe7000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b60011415615bf7565b805f527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b62680060205260405f2073ffffffffffffffffffffffffffffffffffffffff83165f5260205260ff60405f2054165f146159d757805f527f02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b62680060205260405f2073ffffffffffffffffffffffffffffffffffffffff83165f5260205260405f207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00815416905573ffffffffffffffffffffffffffffffffffffffff339216907ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b5f80a460019056fea2646970667358221220448bc29af17350ab754979c778787c0fd9eafcddbd56cdda8cdfee5189d931b764736f6c634300081a0033" diff --git a/test/utils/contracts_setup.go b/test/utils/contracts_setup.go index 3a2534aa5..311ac083f 100644 --- a/test/utils/contracts_setup.go +++ b/test/utils/contracts_setup.go @@ -23,7 +23,7 @@ func setupUESystem( accounts TestAccounts, ) error { // Initialize UE genesis - app.UexecutorKeeper.InitGenesis(ctx, &uetypes.GenesisState{}) + app.UexecutorKeeper.InitGenesis(ctx, uetypes.DefaultGenesis()) // Parse factory ABI factoryABI, err := uetypes.ParseFactoryABI() diff --git a/testnet/core/setup/setup_genesis_validator.sh b/testnet/core/setup/setup_genesis_validator.sh index 3df505643..2c14d166d 100755 --- a/testnet/core/setup/setup_genesis_validator.sh +++ b/testnet/core/setup/setup_genesis_validator.sh @@ -120,7 +120,7 @@ echo "🛠️ Updating genesis parameters..." # EVM update_test_genesis `printf '.app_state["evm"]["params"]["evm_denom"]="%s"' $DENOM` # This seems duplicated since chain config already has this - update_test_genesis '.app_state["evm"]["params"]["active_static_precompiles"]=["0x00000000000000000000000000000000000000CB","0x00000000000000000000000000000000000000ca","0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805"]' + update_test_genesis '.app_state["evm"]["params"]["active_static_precompiles"]=["0x00000000000000000000000000000000000000ca","0x0000000000000000000000000000000000000100","0x0000000000000000000000000000000000000400","0x0000000000000000000000000000000000000800","0x0000000000000000000000000000000000000801","0x0000000000000000000000000000000000000802","0x0000000000000000000000000000000000000803","0x0000000000000000000000000000000000000804","0x0000000000000000000000000000000000000805","0xEC00000000000000000000000000000000000001"]' update_test_genesis '.app_state["evm"]["params"]["chain_config"]["homestead_block"]="0"' update_test_genesis '.app_state["evm"]["params"]["chain_config"]["dao_fork_block"]="0"' update_test_genesis '.app_state["evm"]["params"]["chain_config"]["dao_fork_support"]=true' diff --git a/universalClient/config/config_test.go b/universalClient/config/config_test.go index 1efc379da..b656c5190 100644 --- a/universalClient/config/config_test.go +++ b/universalClient/config/config_test.go @@ -321,3 +321,25 @@ func TestGetChainCleanupSettings(t *testing.T) { assert.Contains(t, err.Error(), "cleanup_interval_seconds") }) } + +func TestNetworkGating(t *testing.T) { + cases := []struct { + network string + wantTestnet bool + }{ + {"", false}, + {"mainnet", false}, + {"MAINNET", false}, + {"prod", false}, + {"testnet", true}, + {"TESTNET", true}, + {" testnet ", true}, + } + for _, tc := range cases { + t.Run("network="+tc.network, func(t *testing.T) { + c := &Config{PushNetwork: tc.network} + assert.Equal(t, tc.wantTestnet, c.IsTestnet()) + assert.Equal(t, tc.wantTestnet, c.AllowsZeroConfirmations()) + }) + } +} diff --git a/universalClient/config/default_config.json b/universalClient/config/default_config.json index 4355eace5..86867d20b 100644 --- a/universalClient/config/default_config.json +++ b/universalClient/config/default_config.json @@ -2,6 +2,7 @@ "log_level": 1, "log_format": "console", "log_sampler": false, + "push_network": "mainnet", "push_chain_id": "localchain_9000-1", "push_chain_grpc_urls": [ "localhost:9090" diff --git a/universalClient/config/types.go b/universalClient/config/types.go index 8a43a7091..7c1c39153 100644 --- a/universalClient/config/types.go +++ b/universalClient/config/types.go @@ -1,6 +1,9 @@ package config -import "fmt" +import ( + "fmt" + "strings" +) // KeyringBackend represents the type of keyring backend to use. type KeyringBackend string @@ -10,6 +13,24 @@ const ( KeyringBackendFile KeyringBackend = "file" ) +const NetworkTestnet = "testnet" + +// IsTestnet reports whether this node is on testnet. Any other value, including +// unset, is treated as mainnet. +func (c *Config) IsTestnet() bool { + return strings.EqualFold(strings.TrimSpace(c.PushNetwork), NetworkTestnet) +} + +// AllowsZeroConfirmations reports whether a registry confirmation depth of 0 is +// honored instead of falling back to a safe depth. +// +// A registry 0 is ambiguous: proto3 encodes a deliberate 0 and an unset field +// identically, so it cannot be read as "instant finality" on its own. Honoring +// it therefore needs an out-of-band signal, which today is a testnet deployment. +func (c *Config) AllowsZeroConfirmations() bool { + return c.IsTestnet() +} + // Config holds all configuration for the Universal Validator. type Config struct { // Logging @@ -27,6 +48,9 @@ type Config struct { ConfigRefreshIntervalSeconds int `json:"config_refresh_interval_seconds"` MaxRetries int `json:"max_retries"` + // PushNetwork is "mainnet" or "testnet"; unset/unknown is treated as mainnet. + PushNetwork string `json:"push_network"` + // Query Server QueryServerPort int `json:"query_server_port"` @@ -52,9 +76,9 @@ type ChainSpecificConfig struct { EventPollingIntervalSeconds *int `json:"event_polling_interval_seconds,omitempty"` EventStartFrom *int64 `json:"event_start_from,omitempty"` GasPriceIntervalSeconds *int `json:"gas_price_interval_seconds,omitempty"` - GasPriceMarkupPercent *int `json:"gas_price_markup_percent,omitempty"` // % markup on fetched gas price to handle spikes - ProtocolALT string `json:"protocol_alt,omitempty"` // Protocol ALT address (base58) for V0 transactions - TokenALTs map[string]string `json:"token_alts,omitempty"` // mint address → token ALT address (base58) + GasPriceMarkupPercent *int `json:"gas_price_markup_percent,omitempty"` // % markup on fetched gas price to handle spikes + ProtocolALT string `json:"protocol_alt,omitempty"` // Protocol ALT address (base58) for V0 transactions + TokenALTs map[string]string `json:"token_alts,omitempty"` // mint address → token ALT address (base58) // SVM rent reclaimer (orphaned StoredIxData PDA cleanup). Both default if unset. RentReclaimSweepIntervalSeconds *int `json:"rent_reclaim_sweep_interval_seconds,omitempty"` // how often to sweep diff --git a/universalClient/externalchains/chains.go b/universalClient/externalchains/chains.go index df25f2b85..be6d92d00 100644 --- a/universalClient/externalchains/chains.go +++ b/universalClient/externalchains/chains.go @@ -28,8 +28,15 @@ type Chains struct { // Chain client management chains map[string]common.ChainClient // key: CAIP-2 chain ID chainConfigs map[string]*uregistrytypes.ChainConfig // key: CAIP-2 chain ID - chainsMu sync.RWMutex - pushChainID string // Push chain ID (always present) + // Handle opened for each live chain, kept so removal can close it. Every + // getChainDB call opens a new pool, so a handle dropped without closing keeps + // its file descriptors until the process exits. + chainDBs map[string]*db.DB // key: CAIP-2 chain ID + chainsMu sync.RWMutex + pushChainID string // Push chain ID (always present) + + // Database opener, swapped in tests to observe handle lifecycle. + openDB func(dir, filename string, migrateSchema bool) (*db.DB, error) // Background control muRunning sync.Mutex @@ -57,6 +64,8 @@ func NewChains( logger: logger.With().Str("component", "chains").Logger(), chains: make(map[string]common.ChainClient), chainConfigs: make(map[string]*uregistrytypes.ChainConfig), + chainDBs: make(map[string]*db.DB), + openDB: db.OpenFileDB, pushChainID: cfg.PushChainID, } } @@ -187,19 +196,33 @@ func (c *Chains) fetchAndUpdate(parent context.Context) error { } } - // Remove stale chains + c.removeStaleChains(seenChains) + + return nil +} + +// removeStaleChains drops chains the registry no longer lists, never the Push chain. +// +// The ids are collected under the read lock and removed after releasing it. +// removeChain takes the write lock and sync.RWMutex is not reentrant, so removing +// from inside the loop would park the refresh goroutine forever while it still +// holds the read lock, taking every later reader of the registry down with it. +func (c *Chains) removeStaleChains(seenChains map[string]bool) { c.chainsMu.RLock() + var stale []string for chainID := range c.chains { - if !seenChains[chainID] { - c.logger.Info().Str("chain", chainID).Msg("removing chain no longer in config") - if err := c.removeChain(chainID); err != nil { - c.logger.Error().Err(err).Str("chain", chainID).Msg("failed to remove chain") - } + if chainID != c.pushChainID && !seenChains[chainID] { + stale = append(stale, chainID) } } c.chainsMu.RUnlock() - return nil + for _, chainID := range stale { + c.logger.Info().Str("chain", chainID).Msg("removing chain no longer in config") + if err := c.removeChain(chainID); err != nil { + c.logger.Error().Err(err).Str("chain", chainID).Msg("failed to remove chain") + } + } } // chainAction represents the action to take for a chain config @@ -259,6 +282,19 @@ func (c *Chains) addChain(ctx context.Context, cfg *uregistrytypes.ChainConfig) return fmt.Errorf("failed to get database for chain %s: %w", cfg.Chain, err) } + // Ownership passes to the registry only once the client is live. Until then + // close it on the way out, or a chain that cannot start leaks a handle on + // every refresh tick for as long as the misconfiguration lasts. + adopted := false + defer func() { + if adopted { + return + } + if cerr := chainDB.Close(); cerr != nil { + c.logger.Warn().Err(cerr).Str("chain", cfg.Chain).Msg("failed to close database after unsuccessful chain add") + } + }() + // Get chain-specific config chainConfig := c.config.GetChainConfig(cfg.Chain) @@ -266,9 +302,9 @@ func (c *Chains) addChain(ctx context.Context, cfg *uregistrytypes.ChainConfig) var client common.ChainClient switch cfg.VmType { case uregistrytypes.VmType_EVM: - client, err = evm.NewClient(cfg, chainDB, chainConfig, c.pushSigner, c.logger) + client, err = evm.NewClient(cfg, chainDB, chainConfig, c.pushSigner, c.config.AllowsZeroConfirmations(), c.logger) case uregistrytypes.VmType_SVM: - client, err = svm.NewClient(cfg, chainDB, chainConfig, c.pushSigner, c.config.NodeHome, c.logger) + client, err = svm.NewClient(cfg, chainDB, chainConfig, c.pushSigner, c.config.NodeHome, c.config.AllowsZeroConfirmations(), c.logger) default: return fmt.Errorf("unsupported VM type: %v", cfg.VmType) } @@ -286,7 +322,9 @@ func (c *Chains) addChain(ctx context.Context, cfg *uregistrytypes.ChainConfig) c.chainsMu.Lock() c.chains[cfg.Chain] = client c.chainConfigs[cfg.Chain] = cfg + c.chainDBs[cfg.Chain] = chainDB c.chainsMu.Unlock() + adopted = true c.logger.Info(). Str("chain", cfg.Chain). @@ -312,6 +350,14 @@ func (c *Chains) removeChain(chainID string) error { Msg("error stopping chain client during removal") } + // After Stop, so nothing is still reading through it. + if database, ok := c.chainDBs[chainID]; ok { + if err := database.Close(); err != nil { + c.logger.Error().Err(err).Str("chain", chainID).Msg("error closing chain database during removal") + } + delete(c.chainDBs, chainID) + } + delete(c.chains, chainID) delete(c.chainConfigs, chainID) @@ -338,9 +384,19 @@ func (c *Chains) StopAll() { } } + for chainID, database := range c.chainDBs { + if err := database.Close(); err != nil { + c.logger.Error(). + Err(err). + Str("chain", chainID). + Msg("error closing chain database") + } + } + // Clear the registry c.chains = make(map[string]common.ChainClient) c.chainConfigs = make(map[string]*uregistrytypes.ChainConfig) + c.chainDBs = make(map[string]*db.DB) } // GetClient returns the chain client for the specified chain ID @@ -401,7 +457,7 @@ func (c *Chains) getChainDB(chainID string) (*db.DB, error) { // Derive database base directory from NodeHome baseDir := filepath.Join(c.config.NodeHome, config.DatabasesSubdir) - database, err := db.OpenFileDB(baseDir, dbFilename, true) + database, err := c.openDB(baseDir, dbFilename, true) if err != nil { return nil, fmt.Errorf("failed to create database for chain %s: %w", chainID, err) } diff --git a/universalClient/externalchains/chains_test.go b/universalClient/externalchains/chains_test.go index 9426e0a86..72defe2df 100644 --- a/universalClient/externalchains/chains_test.go +++ b/universalClient/externalchains/chains_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/pushchain/push-chain-node/universalClient/config" + "github.com/pushchain/push-chain-node/universalClient/db" "github.com/pushchain/push-chain-node/universalClient/externalchains/common" uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types" ) @@ -1708,3 +1709,130 @@ func TestDetermineChainAction_PushChainID(t *testing.T) { assert.Equal(t, chainActionAdd, action) }) } + +// dbIsOpen reports whether the handle still answers queries. A closed *db.DB +// errors on use, which is how these tests tell a released handle from a leaked one. +func dbIsOpen(t *testing.T, database *db.DB) bool { + t.Helper() + sqlDB, err := database.Client().DB() + if err != nil { + return false + } + return sqlDB.Ping() == nil +} + +// A chain that drops out of the registry is removed under the write lock, so the +// stale sweep must not still be holding the read lock when it calls removeChain. +// sync.RWMutex is not reentrant: doing so parks the refresh goroutine forever +// and every later reader of the registry blocks behind it. +func TestFetchAndUpdate_StaleRemovalDoesNotDeadlock(t *testing.T) { + c := newTestChains() + c.chains["eip155:1"] = &mockChainClient{} + c.chainConfigs["eip155:1"] = &uregistrytypes.ChainConfig{Chain: "eip155:1"} + + // Drive the stale sweep directly: the chain is absent from seenChains, which + // is what a delisted chain looks like on the next config fetch. + done := make(chan struct{}) + go func() { + defer close(done) + c.removeStaleChains(map[string]bool{c.pushChainID: true}) + }() + + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("stale removal deadlocked: removeChain was called while the read lock was held") + } + + // The registry must be usable afterwards, not left with a held lock. + acquired := make(chan struct{}) + go func() { + c.chainsMu.Lock() + c.chainsMu.Unlock() + close(acquired) + }() + select { + case <-acquired: + case <-time.After(5 * time.Second): + t.Fatal("chainsMu still held after the stale sweep") + } + + _, err := c.GetClient("eip155:1") + assert.Error(t, err, "the delisted chain should be gone") +} + +// Every getChainDB call opens a fresh pool, so a handle that is dropped rather +// than closed keeps its descriptors for the life of the process. A chain that +// cannot start is retried on every refresh tick, which turns that into growth. +func TestAddChain_ClosesDatabaseWhenTheChainCannotStart(t *testing.T) { + c := newTestChains() + c.config.NodeHome = t.TempDir() + + // An unsupported VM type fails after the database has been opened. + cfg := &uregistrytypes.ChainConfig{ + Chain: "eip155:99", + VmType: uregistrytypes.VmType(9999), + Enabled: &uregistrytypes.ChainEnabled{IsInboundEnabled: true}, + } + + // Capture every handle addChain opens so we can assert each was released. + var opened []*db.DB + realOpen := c.openDB + c.openDB = func(dir, filename string, migrate bool) (*db.DB, error) { + database, err := realOpen(dir, filename, migrate) + if err == nil { + opened = append(opened, database) + } + return database, err + } + + for i := 0; i < 5; i++ { // five refresh ticks with the same broken config + err := c.addChain(context.Background(), cfg) + require.Error(t, err) + } + + require.Len(t, opened, 5, "each attempt opens its own handle") + for i, database := range opened { + assert.False(t, dbIsOpen(t, database), + "handle from attempt %d leaked; a persistent misconfiguration would grow one per tick", i) + } + assert.NotContains(t, c.chains, "eip155:99") +} + +// Removal has to release the handle too, not just drop the map entry. +func TestRemoveChain_ClosesTheDatabase(t *testing.T) { + c := newTestChains() + database, err := db.OpenFileDB(t.TempDir(), "eip155_1.db", true) + require.NoError(t, err) + + c.chains["eip155:1"] = &mockChainClient{} + c.chainConfigs["eip155:1"] = &uregistrytypes.ChainConfig{Chain: "eip155:1"} + c.chainDBs["eip155:1"] = database + require.True(t, dbIsOpen(t, database)) + + require.NoError(t, c.removeChain("eip155:1")) + + assert.False(t, dbIsOpen(t, database), "removal must close the handle") + assert.NotContains(t, c.chainDBs, "eip155:1") +} + +func TestStopAll_ClosesEveryDatabase(t *testing.T) { + c := newTestChains() + dir := t.TempDir() + + var opened []*db.DB + for _, id := range []string{"eip155:1", "eip155:2"} { + database, err := db.OpenFileDB(dir, sanitizeChainID(id)+".db", true) + require.NoError(t, err) + c.chains[id] = &mockChainClient{} + c.chainDBs[id] = database + opened = append(opened, database) + } + + c.StopAll() + + for i, database := range opened { + assert.False(t, dbIsOpen(t, database), "handle %d must be closed", i) + } + assert.Empty(t, c.chainDBs) +} diff --git a/universalClient/externalchains/common/confirmation.go b/universalClient/externalchains/common/confirmation.go new file mode 100644 index 000000000..438159197 --- /dev/null +++ b/universalClient/externalchains/common/confirmation.go @@ -0,0 +1,19 @@ +package common + +// Safe fallback confirmation depths used when the registry configures 0 and +// instant routes are not enabled. +const ( + DefaultFastConfirmations uint64 = 5 + DefaultStandardConfirmations uint64 = 12 +) + +// ConfirmationDepth returns latestHeight - txHeight + 1, the confirmation count +// with the inclusion block counted as one. ok is false when latestHeight < +// txHeight (a cross-RPC height skew); callers must defer rather than trust the +// depth, since the unchecked subtraction would underflow. +func ConfirmationDepth(latestHeight, txHeight uint64) (depth uint64, ok bool) { + if latestHeight < txHeight { + return 0, false + } + return latestHeight - txHeight + 1, true +} diff --git a/universalClient/externalchains/common/confirmation_test.go b/universalClient/externalchains/common/confirmation_test.go new file mode 100644 index 000000000..b50afbabc --- /dev/null +++ b/universalClient/externalchains/common/confirmation_test.go @@ -0,0 +1,42 @@ +package common + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfirmationDepth(t *testing.T) { + tests := []struct { + name string + latest uint64 + tx uint64 + wantDepth uint64 + wantOK bool + }{ + {"latest greater than tx", 110, 100, 11, true}, + {"latest equals tx (inclusion block)", 100, 100, 1, true}, + {"latest one below tx (skew)", 99, 100, 0, false}, + {"latest far below tx (skew)", 1, math.MaxUint64, 0, false}, + {"no underflow to near-2^64", 0, 1, 0, false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + depth, ok := ConfirmationDepth(tc.latest, tc.tx) + assert.Equal(t, tc.wantOK, ok) + assert.Equal(t, tc.wantDepth, depth) + }) + } +} + +// TestConfirmationDepth_SkewNeverSatisfiesThreshold guards the exact finding: +// a transaction one block ahead of the observed tip must not produce a depth +// that clears a realistic confirmation threshold. +func TestConfirmationDepth_SkewNeverSatisfiesThreshold(t *testing.T) { + const threshold = uint64(12) + depth, ok := ConfirmationDepth(500, 501) + assert.False(t, ok, "skewed read must be flagged not-ok") + assert.False(t, depth >= threshold, "skewed depth must not satisfy threshold") +} diff --git a/universalClient/externalchains/common/event_cleaner.go b/universalClient/externalchains/common/event_cleaner.go index b7843a42a..0d89d1083 100644 --- a/universalClient/externalchains/common/event_cleaner.go +++ b/universalClient/externalchains/common/event_cleaner.go @@ -3,6 +3,7 @@ package common import ( "context" "fmt" + "sync" "time" "github.com/pushchain/push-chain-node/universalClient/db" @@ -23,9 +24,14 @@ type EventCleaner struct { cleanupInterval time.Duration retentionPeriod time.Duration logger zerolog.Logger - ticker *time.Ticker - stopCh chan struct{} - running bool + + // mu guards running and stopCh, which Start and Stop both touch. The + // cleanup goroutine reads neither: it closes over its own copies, so the + // only cross-goroutine state is the channel it selects on. + mu sync.Mutex + running bool + stopCh chan struct{} + wg sync.WaitGroup } // NewEventCleaner creates a new event cleaner for a chain @@ -54,9 +60,16 @@ func NewEventCleaner( // Start begins the periodic cleanup process func (ec *EventCleaner) Start(ctx context.Context) error { + ec.mu.Lock() if ec.running { + ec.mu.Unlock() return fmt.Errorf("event cleaner is already running") } + stopCh := make(chan struct{}) + ec.running = true + ec.stopCh = stopCh + ec.wg.Add(1) + ec.mu.Unlock() ec.logger.Debug(). Str("cleanup_interval", ec.cleanupInterval.String()). @@ -69,21 +82,23 @@ func (ec *EventCleaner) Start(ctx context.Context) error { // Don't fail startup on cleanup error, just log it } - ec.running = true - ec.stopCh = make(chan struct{}) - ec.ticker = time.NewTicker(ec.cleanupInterval) + // The ticker and stop channel are the goroutine's own. Holding them on the + // struct let Stop write the fields while the goroutine was still reading + // them, which is the race this shape removes. + ticker := time.NewTicker(ec.cleanupInterval) go func() { - defer ec.ticker.Stop() + defer ec.wg.Done() + defer ticker.Stop() for { select { case <-ctx.Done(): ec.logger.Debug().Msg("context cancelled, stopping event cleaner") return - case <-ec.stopCh: + case <-stopCh: ec.logger.Debug().Msg("stop signal received, stopping event cleaner") return - case <-ec.ticker.C: + case <-ticker.C: if err := ec.performCleanup(); err != nil { ec.logger.Error().Err(err).Msg("failed to perform scheduled cleanup") } @@ -94,17 +109,24 @@ func (ec *EventCleaner) Start(ctx context.Context) error { return nil } -// Stop gracefully stops the event cleaner. No-op if not running. +// Stop gracefully stops the event cleaner and waits for the cleanup goroutine +// to exit. No-op if not running. +// +// Waiting matters on shutdown: the goroutine runs queries against the chain +// database, and returning before it finishes lets the caller close that +// database underneath an in-flight cleanup. func (ec *EventCleaner) Stop() { + ec.mu.Lock() if !ec.running { + ec.mu.Unlock() return } ec.logger.Debug().Msg("stopping event cleaner") - if ec.ticker != nil { - ec.ticker.Stop() - } - close(ec.stopCh) ec.running = false + close(ec.stopCh) + ec.mu.Unlock() + + ec.wg.Wait() } // performCleanup executes cleanup of terminal events (COMPLETED, REORGED, REVERTED) diff --git a/universalClient/externalchains/common/event_cleaner_test.go b/universalClient/externalchains/common/event_cleaner_test.go index bc28d2eae..15eee2be1 100644 --- a/universalClient/externalchains/common/event_cleaner_test.go +++ b/universalClient/externalchains/common/event_cleaner_test.go @@ -3,6 +3,7 @@ package common import ( "context" "fmt" + "sync" "testing" "time" @@ -75,8 +76,8 @@ func TestEventCleanerStruct(t *testing.T) { assert.Nil(t, ec.database) assert.Equal(t, time.Duration(0), ec.cleanupInterval) assert.Equal(t, time.Duration(0), ec.retentionPeriod) - assert.Nil(t, ec.ticker) assert.Nil(t, ec.stopCh) + assert.False(t, ec.running) }) } @@ -250,7 +251,7 @@ func TestEventCleanerStart(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) require.NoError(t, cleaner.Start(ctx)) - require.NotNil(t, cleaner.ticker) + require.NotNil(t, cleaner.stopCh) cancel() time.Sleep(100 * time.Millisecond) @@ -337,3 +338,138 @@ func TestEventCleanerStartStopLifecycle(t *testing.T) { time.Sleep(50 * time.Millisecond) }) } + +// Start and Stop race against the cleanup goroutine. Run under -race. +func TestEventCleaner_StartStopUnderRace(t *testing.T) { + for i := 0; i < 20; i++ { + database := newTestCleanerDB(t, nil) + cleaner := NewEventCleaner(database, intPtr(3600), intPtr(0), "test-chain", zerolog.Nop()) + // Fast enough that the goroutine is inside performCleanup while Stop runs. + cleaner.cleanupInterval = time.Millisecond + + require.NoError(t, cleaner.Start(context.Background())) + cleaner.Stop() + } +} + +// Concurrent Stop calls must not double close the channel or return before the +// goroutine has exited. +func TestEventCleaner_ConcurrentStop(t *testing.T) { + database := newTestCleanerDB(t, nil) + cleaner := NewEventCleaner(database, intPtr(3600), intPtr(0), "test-chain", zerolog.Nop()) + cleaner.cleanupInterval = time.Millisecond + + require.NoError(t, cleaner.Start(context.Background())) + + var wg sync.WaitGroup + for i := 0; i < 8; i++ { + wg.Add(1) + go func() { + defer wg.Done() + cleaner.Stop() + }() + } + wg.Wait() + + assert.False(t, cleaner.running) +} + +// Concurrent Start calls must leave exactly one goroutine running. +func TestEventCleaner_ConcurrentStart(t *testing.T) { + database := newTestCleanerDB(t, nil) + cleaner := NewEventCleaner(database, intPtr(3600), intPtr(0), "test-chain", zerolog.Nop()) + cleaner.cleanupInterval = time.Millisecond + + var mu sync.Mutex + started := 0 + + var wg sync.WaitGroup + for i := 0; i < 8; i++ { + wg.Add(1) + go func() { + defer wg.Done() + if err := cleaner.Start(context.Background()); err == nil { + mu.Lock() + started++ + mu.Unlock() + } + }() + } + wg.Wait() + + assert.Equal(t, 1, started, "more than one cleanup goroutine was started") + cleaner.Stop() +} + +// Stop must not return while a cleanup is still in flight, otherwise the caller +// can close the chain database underneath an in-flight query. +// +// Held open with a write transaction so the goroutine is genuinely blocked +// inside performCleanup while Stop is called. Without that, the goroutine exits +// so fast that a Stop which does not wait looks identical to one that does. +func TestEventCleaner_StopWaitsForInFlightCleanup(t *testing.T) { + database := newTestCleanerDB(t, nil) + cleaner := NewEventCleaner(database, intPtr(3600), intPtr(0), "test-chain", zerolog.Nop()) + cleaner.cleanupInterval = time.Millisecond + + // Start first: the initial cleanup is synchronous and would block on the lock. + require.NoError(t, cleaner.Start(context.Background())) + + // Take the write lock so the next ticked cleanup blocks on DELETE. + tx := database.Client().Begin() + require.NoError(t, tx.Error) + require.NoError(t, tx.Exec( + "CREATE TABLE IF NOT EXISTS lock_probe (id INTEGER PRIMARY KEY)").Error) + require.NoError(t, tx.Exec("INSERT INTO lock_probe (id) VALUES (1)").Error) + + time.Sleep(50 * time.Millisecond) // let a tick land and block + + stopped := make(chan struct{}) + go func() { + cleaner.Stop() + close(stopped) + }() + + select { + case <-stopped: + tx.Rollback() + t.Fatal("Stop returned while a cleanup was still in flight") + case <-time.After(200 * time.Millisecond): + } + + tx.Rollback() // release the lock; the cleanup can now finish + + select { + case <-stopped: + case <-time.After(5 * time.Second): + t.Fatal("Stop did not return after the cleanup finished") + } +} + +// Cancelling the context stops the goroutine, and a later Stop is still safe. +func TestEventCleaner_ContextCancelThenStop(t *testing.T) { + database := newTestCleanerDB(t, nil) + cleaner := NewEventCleaner(database, intPtr(3600), intPtr(0), "test-chain", zerolog.Nop()) + cleaner.cleanupInterval = time.Millisecond + + ctx, cancel := context.WithCancel(context.Background()) + require.NoError(t, cleaner.Start(ctx)) + cancel() + time.Sleep(20 * time.Millisecond) + + cleaner.Stop() // must not hang or panic + assert.False(t, cleaner.running) +} + +// Restart after Stop gets a fresh channel rather than reusing the closed one. +func TestEventCleaner_RestartAfterStop(t *testing.T) { + database := newTestCleanerDB(t, nil) + cleaner := NewEventCleaner(database, intPtr(3600), intPtr(0), "test-chain", zerolog.Nop()) + cleaner.cleanupInterval = time.Millisecond + + require.NoError(t, cleaner.Start(context.Background())) + cleaner.Stop() + + require.NoError(t, cleaner.Start(context.Background()), "restart was refused") + cleaner.Stop() +} diff --git a/universalClient/externalchains/common/inbound_observation_event_processor_test.go b/universalClient/externalchains/common/inbound_observation_event_processor_test.go index 4abfe0abc..f29af854e 100644 --- a/universalClient/externalchains/common/inbound_observation_event_processor_test.go +++ b/universalClient/externalchains/common/inbound_observation_event_processor_test.go @@ -212,3 +212,76 @@ func TestInboundHandleEvent(t *testing.T) { assert.Equal(t, int64(1), rows) }) } + +// The wire values the gateways emit are 0-indexed (Gas, GasAndPayload, Funds, +// FundsAndPayload) while the chain enum reserves 0 for UNSPECIFIED, so the +// mapping is shifted by one. A decoder that leaves TxType unset therefore does +// not produce "unknown", it produces GAS. +func TestBuildInboundObservation_TxTypeMapping(t *testing.T) { + processor := NewInboundObservationEventProcessor(nil, nil, zerolog.Nop()) + + for _, tc := range []struct { + wire uint + want uexecutortypes.TxType + }{ + {0, uexecutortypes.TxType_GAS}, + {1, uexecutortypes.TxType_GAS_AND_PAYLOAD}, + {2, uexecutortypes.TxType_FUNDS}, + {3, uexecutortypes.TxType_FUNDS_AND_PAYLOAD}, + {4, uexecutortypes.TxType_UNSPECIFIED_TX}, + {99, uexecutortypes.TxType_UNSPECIFIED_TX}, + } { + data, err := json.Marshal(InboundObservation{ + SourceChain: "solana:devnet", + Sender: "0xabc", + Recipient: "0xdef", + Amount: "5000000", + TxType: tc.wire, + }) + require.NoError(t, err) + + inbound, err := processor.buildInboundObservation(&store.Event{ + EventID: "sig:0", + EventData: data, + }) + require.NoError(t, err) + assert.Equal(t, tc.want, inbound.TxType, "wire value %d", tc.wire) + } +} + +// A FUNDS transfer must never reach the keeper as GAS. The two dispatch to +// different handlers: GAS mints and autoswaps into the sender UEA, FUNDS +// deposits PRC20 to the recipient, so the same amount lands with a different +// party. +func TestBuildInboundObservation_FundsNeverBecomesGas(t *testing.T) { + processor := NewInboundObservationEventProcessor(nil, nil, zerolog.Nop()) + + data, err := json.Marshal(InboundObservation{ + SourceChain: "solana:devnet", + Sender: "0xabc", + Recipient: "0xdef", + Amount: "5000000", + TxType: 2, // Funds, as the real devnet events carry + }) + require.NoError(t, err) + + inbound, err := processor.buildInboundObservation(&store.Event{ + EventID: "sig:0", + EventData: data, + }) + require.NoError(t, err) + + assert.Equal(t, uexecutortypes.TxType_FUNDS, inbound.TxType) + assert.NotEqual(t, uexecutortypes.TxType_GAS, inbound.TxType, + "a FUNDS transfer routed to GAS credits the sender instead of the recipient") +} + +// An event whose data never made it past the decoder must be refused outright +// rather than defaulted. +func TestBuildInboundObservation_RejectsEventWithoutData(t *testing.T) { + processor := NewInboundObservationEventProcessor(nil, nil, zerolog.Nop()) + + _, err := processor.buildInboundObservation(&store.Event{EventID: "sig:0"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "event data is missing") +} diff --git a/universalClient/externalchains/common/outbound_observation_event_processor_test.go b/universalClient/externalchains/common/outbound_observation_event_processor_test.go index af3db22c6..dcfd47b53 100644 --- a/universalClient/externalchains/common/outbound_observation_event_processor_test.go +++ b/universalClient/externalchains/common/outbound_observation_event_processor_test.go @@ -189,3 +189,30 @@ func TestOutboundHandleEvent(t *testing.T) { assert.Equal(t, int64(1), rows) }) } + +// A revert observation carries no gas fee from the chain: the gateway dropped +// gas_used from RevertUniversalTx. The vote must still say "0" rather than empty, +// because core rejects an empty gas_fee_used outright and the value is part of +// the outbound ballot key, so every validator has to produce the same one. +func TestBuildOutboundObservation_EmptyGasFeeUsedBecomesZero(t *testing.T) { + processor := NewOutboundObservationEventProcessor(nil, nil, zerolog.Nop()) + event := &store.Event{EventID: "sig:0", BlockHeight: 100} + + obs, err := processor.buildOutboundObservation(event, &OutboundObservation{ + TxID: "0x1234", + UniversalTxID: "0xabcd", + // GasFeeUsed intentionally unset, as a revert leaves it. + }) + require.NoError(t, err) + require.NotNil(t, obs) + + assert.Equal(t, "0", obs.GasFeeUsed, "an empty gas fee must not reach the vote") + + // Same input twice must give the same value: it feeds the ballot key, so a + // non-deterministic default would split validators across ballots. + again, err := processor.buildOutboundObservation(event, &OutboundObservation{ + TxID: "0x1234", UniversalTxID: "0xabcd", + }) + require.NoError(t, err) + assert.Equal(t, obs.GasFeeUsed, again.GasFeeUsed) +} diff --git a/universalClient/externalchains/common/types.go b/universalClient/externalchains/common/types.go index 6f258f524..bcfd49e58 100644 --- a/universalClient/externalchains/common/types.go +++ b/universalClient/externalchains/common/types.go @@ -49,13 +49,12 @@ type ChainClient interface { // FundMigrationData contains the data needed to build a fund migration transaction. // Populated by the coordinator from the migration event + derived addresses. type FundMigrationData struct { - From string // Old TSS address (derived from old pubkey) - To string // New TSS address (derived from current pubkey) - GasPrice *big.Int // Gas price from the migration event - GasLimit uint64 // Gas limit from the migration event - L1GasFee *big.Int // Extra L1 data-availability fee (wei); 0 for non-L2 chains - - Balance *big.Int // if nil, builder queries chain + From string // Old TSS address (derived from old pubkey) + To string // New TSS address (derived from current pubkey) + GasPrice *big.Int // Gas price from the migration event + GasLimit uint64 // Gas limit from the migration event + L1GasFee *big.Int // Extra L1 data-availability fee (wei); 0 for non-L2 chains + TransferAmount *big.Int // sweep amount pinned on chain; never derived from a live balance } // UnsignedSigningReq contains the request for signing an outbound or fund-migration transaction. @@ -63,10 +62,6 @@ type UnsignedSigningReq struct { SigningHash []byte // Hash to be signed by TSS Nonce uint64 // evm - TSS Address nonce | svm - PDA nonce - // TSSFundMigrationAmount is the native value swept for a fund-migration tx, fixed at - // signing time. Nil for outbound. Must be reused verbatim at broadcast — re-querying - // balance there races with a successful sweep from another validator. - TSSFundMigrationAmount *big.Int `json:"TSSFundMigrationAmount,omitempty"` } // TxBuilder builds and broadcasts transactions for outbound transfers @@ -83,7 +78,9 @@ type TxBuilder interface { // VerifyBroadcastedTx checks the status of a broadcasted transaction on the destination chain. // Returns (found, blockHeight, confirmations, status, error): - // - found=false: tx not found or not yet mined + // - err != nil: the chain could not be queried. Callers must retry and must not + // treat this as evidence about whether the tx executed. + // - found=false, err=nil: the chain answered and the tx is not there. // - found=true: tx exists on-chain // - blockHeight: the block in which the tx was mined // - confirmations: number of blocks since the tx was mined (0 = just mined) @@ -101,9 +98,10 @@ type TxBuilder interface { IsAlreadyExecuted(ctx context.Context, txID string) (executed bool, queryBlockTime int64, err error) // GetGasFeeUsed returns the gas fee used by a transaction on the destination chain. - // EVM: fetches receipt and returns gasUsed * effectiveGasPrice as decimal string. + // EVM: gasUsed * effectiveGasPrice + OP-Stack l1Fee, as a decimal string; errors + // when the fee cannot be determined so callers retry instead of recording an + // under-reported fee. // SVM: returns "0" (gas accounting is handled via vault gasFee reimbursement). - // Returns "0" if the transaction is not found. GetGasFeeUsed(ctx context.Context, txHash string) (string, error) // GetFundMigrationSigningRequest builds a native token transfer for fund migration, diff --git a/universalClient/externalchains/evm/client.go b/universalClient/externalchains/evm/client.go index 43e3ece41..b907a3c4e 100644 --- a/universalClient/externalchains/evm/client.go +++ b/universalClient/externalchains/evm/client.go @@ -21,10 +21,11 @@ import ( // Client implements the ChainClient interface for EVM chains type Client struct { // Core configuration - logger zerolog.Logger - chainIDStr string - registryConfig *uregistrytypes.ChainConfig - chainConfig *config.ChainSpecificConfig + logger zerolog.Logger + chainIDStr string + registryConfig *uregistrytypes.ChainConfig + chainConfig *config.ChainSpecificConfig + allowZeroConfirmations bool // Infrastructure rpcClient *RPCClient @@ -50,6 +51,7 @@ func NewClient( database *db.DB, chainConfig *config.ChainSpecificConfig, pushSigner *pushsigner.Signer, + allowZeroConfirmations bool, logger zerolog.Logger, ) (*Client, error) { if config == nil { @@ -69,12 +71,13 @@ func NewClient( } client := &Client{ - logger: log, - chainIDStr: chainIDStr, - registryConfig: config, - chainConfig: chainConfig, - database: database, - pushSigner: pushSigner, + logger: log, + chainIDStr: chainIDStr, + registryConfig: config, + chainConfig: chainConfig, + allowZeroConfirmations: allowZeroConfirmations, + database: database, + pushSigner: pushSigner, } client.eventCleaner = common.NewEventCleaner( @@ -363,8 +366,8 @@ func (c *Client) applyDefaults() componentConfig { config := componentConfig{ eventPollingInterval: 5, // default gasPriceInterval: 30, // default - fastConfirmations: 2, - standardConfirmations: 12, + fastConfirmations: common.DefaultFastConfirmations, + standardConfirmations: common.DefaultStandardConfirmations, } // Apply event polling interval @@ -388,6 +391,17 @@ func (c *Client) applyDefaults() componentConfig { config.standardConfirmations = uint64(c.registryConfig.BlockConfirmation.StandardInbound) } + // A registry-configured 0 disables the reorg-safety depth. Honor it only + // when instant routes are enabled; otherwise fall back to a safe default. + if !c.allowZeroConfirmations { + if config.fastConfirmations == 0 { + config.fastConfirmations = common.DefaultFastConfirmations + } + if config.standardConfirmations == 0 { + config.standardConfirmations = common.DefaultStandardConfirmations + } + } + return config } diff --git a/universalClient/externalchains/evm/client_test.go b/universalClient/externalchains/evm/client_test.go index 0f0f45a58..3a02e63b4 100644 --- a/universalClient/externalchains/evm/client_test.go +++ b/universalClient/externalchains/evm/client_test.go @@ -36,7 +36,7 @@ func TestClientInitialization(t *testing.T) { } chainSpecificConfig := testChainConfig([]string{"https://eth-mainnet.example.com"}) - client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger) + client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger) require.NoError(t, err) assert.NotNil(t, client) assert.Equal(t, chainConfig, client.GetConfig()) @@ -44,7 +44,7 @@ func TestClientInitialization(t *testing.T) { }) t.Run("Nil config", func(t *testing.T) { - client, err := NewClient(nil, nil, nil, nil, logger) + client, err := NewClient(nil, nil, nil, nil, false, logger) assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "config is nil") @@ -57,7 +57,7 @@ func TestClientInitialization(t *testing.T) { } chainSpecificConfig := testChainConfig([]string{}) - client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger) + client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger) assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "no RPC URLs configured") @@ -69,7 +69,7 @@ func TestClientInitialization(t *testing.T) { VmType: uregistrytypes.VmType_SVM, // Wrong VM type } - client, err := NewClient(chainConfig, nil, nil, nil, logger) + client, err := NewClient(chainConfig, nil, nil, nil, false, logger) assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "invalid VM type for EVM client") @@ -177,7 +177,7 @@ func TestClientStartStop(t *testing.T) { } chainSpecificConfig := testChainConfig([]string{server.URL}) - client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger) + client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger) require.NoError(t, err) ctx := context.Background() @@ -199,7 +199,7 @@ func TestClientStartStop(t *testing.T) { chainSpecificConfig := testChainConfig([]string{"http://invalid.localhost:99999"}) - client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger) + client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger) require.NoError(t, err) // Use context with timeout to ensure fast failure @@ -238,7 +238,7 @@ func TestClientStartStop(t *testing.T) { // Use valid URL but cancel context immediately chainSpecificConfig := testChainConfig([]string{server.URL}) - client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger) + client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger) require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) @@ -295,7 +295,7 @@ func TestClientIsHealthy(t *testing.T) { } chainSpecificConfig := testChainConfig([]string{server.URL}) - client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger) + client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger) require.NoError(t, err) // Start the client @@ -320,7 +320,7 @@ func TestClientIsHealthy(t *testing.T) { // Provide valid RPC URLs for NewClient to succeed // But don't start the client chainSpecificConfig := testChainConfig([]string{"https://eth-mainnet.example.com"}) - client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger) + client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger) require.NoError(t, err) healthy := client.IsHealthy() @@ -342,7 +342,7 @@ func TestApplyDefaults(t *testing.T) { assert.Equal(t, 5, cfg.eventPollingInterval) assert.Equal(t, 30, cfg.gasPriceInterval) assert.Equal(t, 0, cfg.gasPriceMarkupPercent) - assert.Equal(t, uint64(2), cfg.fastConfirmations) + assert.Equal(t, uint64(5), cfg.fastConfirmations) assert.Equal(t, uint64(12), cfg.standardConfirmations) }) @@ -412,7 +412,7 @@ func TestApplyDefaults(t *testing.T) { } cfg := client.applyDefaults() - assert.Equal(t, uint64(2), cfg.fastConfirmations) + assert.Equal(t, uint64(5), cfg.fastConfirmations) assert.Equal(t, uint64(12), cfg.standardConfirmations) }) @@ -426,11 +426,68 @@ func TestApplyDefaults(t *testing.T) { } cfg := client.applyDefaults() - assert.Equal(t, uint64(2), cfg.fastConfirmations) + assert.Equal(t, uint64(5), cfg.fastConfirmations) assert.Equal(t, uint64(12), cfg.standardConfirmations) }) } +// A registry-configured 0 falls back to a safe depth unless instant routes are +// enabled, in which case it is honored. +func TestApplyDefaults_ZeroConfirmations(t *testing.T) { + logger := zerolog.New(zerolog.NewTestWriter(t)) + + zeroRegistry := &uregistrytypes.ChainConfig{ + BlockConfirmation: &uregistrytypes.BlockConfirmation{ + FastInbound: 0, + StandardInbound: 0, + }, + } + + t.Run("mainnet falls back to safe depth", func(t *testing.T) { + client := &Client{ + logger: logger, + chainIDStr: "eip155:1", + registryConfig: zeroRegistry, + allowZeroConfirmations: false, + } + + cfg := client.applyDefaults() + assert.Equal(t, uint64(5), cfg.fastConfirmations, "zero fast must not disable depth on mainnet") + assert.Equal(t, uint64(12), cfg.standardConfirmations, "zero standard must not disable depth on mainnet") + }) + + t.Run("testnet honors zero as instant", func(t *testing.T) { + client := &Client{ + logger: logger, + chainIDStr: "eip155:1", + registryConfig: zeroRegistry, + allowZeroConfirmations: true, + } + + cfg := client.applyDefaults() + assert.Equal(t, uint64(0), cfg.fastConfirmations, "testnet instant route keeps zero") + assert.Equal(t, uint64(0), cfg.standardConfirmations, "testnet instant route keeps zero") + }) + + t.Run("nonzero registry values unaffected by flag", func(t *testing.T) { + client := &Client{ + logger: logger, + chainIDStr: "eip155:1", + registryConfig: &uregistrytypes.ChainConfig{ + BlockConfirmation: &uregistrytypes.BlockConfirmation{ + FastInbound: 3, + StandardInbound: 9, + }, + }, + allowZeroConfirmations: false, + } + + cfg := client.applyDefaults() + assert.Equal(t, uint64(3), cfg.fastConfirmations) + assert.Equal(t, uint64(9), cfg.standardConfirmations) + }) +} + // TestGetTxBuilderNil tests GetTxBuilder when txBuilder is not initialized func TestGetTxBuilderNil(t *testing.T) { logger := zerolog.New(zerolog.NewTestWriter(t)) @@ -441,7 +498,7 @@ func TestGetTxBuilderNil(t *testing.T) { } chainSpecificConfig := testChainConfig([]string{"https://eth-mainnet.example.com"}) - client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger) + client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger) require.NoError(t, err) // txBuilder is nil because gateway is not configured / Start not called @@ -464,7 +521,7 @@ func TestClientGetMethods(t *testing.T) { } chainSpecificConfig := testChainConfig([]string{"https://eth-sepolia.example.com"}) - client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger) + client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger) require.NoError(t, err) t.Run("ChainID", func(t *testing.T) { @@ -496,7 +553,7 @@ func TestClientConcurrency(t *testing.T) { } chainSpecificConfig := testChainConfig([]string{server.URL}) - client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger) + client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger) require.NoError(t, err) ctx := context.Background() diff --git a/universalClient/externalchains/evm/event_confirmer.go b/universalClient/externalchains/evm/event_confirmer.go index b4c73a832..f07b5e7eb 100644 --- a/universalClient/externalchains/evm/event_confirmer.go +++ b/universalClient/externalchains/evm/event_confirmer.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "math/big" "strings" "sync" "time" @@ -140,7 +139,7 @@ func (ec *EventConfirmer) processPendingEvents(ctx context.Context) error { // Get transaction receipt hash := ethcommon.HexToHash(txHash) receipt, err := ec.rpcClient.GetTransactionReceipt(ctx, hash) - if err != nil { + if err != nil || receipt == nil { // Transaction not found or not yet mined - skip continue } @@ -160,25 +159,33 @@ func (ec *EventConfirmer) processPendingEvents(ctx context.Context) error { // Check if transaction is confirmed based on confirmation type requiredConfirmations := ec.getRequiredConfirmations(event.ConfirmationType) - confirmations := latestBlock - receipt.BlockNumber.Uint64() + 1 + txBlock := receipt.BlockNumber + confirmations, ok := chaincommon.ConfirmationDepth(latestBlock, txBlock) + if !ok { + // RPC height skew: latest block is behind the tx block. Defer. + ec.logger.Debug(). + Str("event_id", event.EventID). + Uint64("latest_block", latestBlock). + Uint64("tx_block", txBlock). + Msg("latest block behind tx block (RPC height skew); deferring confirmation") + continue + } if confirmations >= requiredConfirmations { var rowsAffected int64 // For outbound events, enrich with gas fee before confirming if event.Type == store.EventTypeOutbound { - tx, _, txErr := ec.rpcClient.GetTransactionByHash(ctx, hash) - if txErr != nil { + if receipt.EffectiveGasPrice == nil { + // Receipt omitted effectiveGasPrice; skip rather than record a + // gas fee missing its L2 execution component. Retried next poll. ec.logger.Warn(). - Err(txErr). Str("event_id", event.EventID). Str("tx_hash", txHash). - Msg("failed to fetch transaction for gas fee, skipping confirmation") + Msg("receipt missing effectiveGasPrice, skipping confirmation") continue } - gasUsed := new(big.Int).SetUint64(receipt.GasUsed) - gasPrice := tx.GasPrice() - gasFeeUsed := new(big.Int).Mul(gasUsed, gasPrice).String() + gasFeeUsedStr := gasFeeUsed(receipt.GasUsed, receipt.EffectiveGasPrice, receipt.L1Fee).String() // Unmarshal, set GasFeeUsed, re-marshal var outboundEvent chaincommon.OutboundObservation @@ -189,7 +196,7 @@ func (ec *EventConfirmer) processPendingEvents(ctx context.Context) error { Msg("failed to unmarshal outbound event data") continue } - outboundEvent.GasFeeUsed = gasFeeUsed + outboundEvent.GasFeeUsed = gasFeeUsedStr updatedData, marshalErr := json.Marshal(outboundEvent) if marshalErr != nil { @@ -245,24 +252,13 @@ func (ec *EventConfirmer) getTxHashFromEventID(eventID string) string { return parts[0] } -// getRequiredConfirmations returns the required number of confirmations based on confirmation type +// getRequiredConfirmations returns the depth for a confirmation type. Values are +// resolved by applyDefaults, so a 0 here is an intentional instant route. func (ec *EventConfirmer) getRequiredConfirmations(confirmationType string) uint64 { switch confirmationType { case store.ConfirmationFast: - if ec.fastConfirmations >= 0 { - return ec.fastConfirmations - } - return 5 - case store.ConfirmationStandard: - if ec.standardConfirmations >= 0 { - return ec.standardConfirmations - } - return 12 + return ec.fastConfirmations default: - // Default to standard if unknown - if ec.standardConfirmations >= 0 { - return ec.standardConfirmations - } - return 12 + return ec.standardConfirmations } } diff --git a/universalClient/externalchains/evm/event_confirmer_test.go b/universalClient/externalchains/evm/event_confirmer_test.go index 55ae7f513..1804da79e 100644 --- a/universalClient/externalchains/evm/event_confirmer_test.go +++ b/universalClient/externalchains/evm/event_confirmer_test.go @@ -375,25 +375,27 @@ func TestEventConfirmer_PendingEventsWithBlockHeightZero(t *testing.T) { assert.Equal(t, uint64(0), pending[0].BlockHeight) } +// The confirmer honors whatever depth it is given; the fallback policy lives in +// applyDefaults, so a 0 here is an intentional instant route. func TestEventConfirmer_GetRequiredConfirmations_ZeroValues(t *testing.T) { logger := zerolog.Nop() - t.Run("zero fast confirmations returns 0", func(t *testing.T) { + t.Run("zero fast confirmations honored as instant", func(t *testing.T) { ec := NewEventConfirmer(nil, nil, "eip155:1", 5, 0, 12, logger) result := ec.getRequiredConfirmations(store.ConfirmationFast) assert.Equal(t, uint64(0), result) }) - t.Run("zero standard confirmations returns 0", func(t *testing.T) { + t.Run("zero standard confirmations honored as instant", func(t *testing.T) { ec := NewEventConfirmer(nil, nil, "eip155:1", 5, 5, 0, logger) result := ec.getRequiredConfirmations(store.ConfirmationStandard) assert.Equal(t, uint64(0), result) }) - t.Run("zero standard with unknown type returns 0", func(t *testing.T) { - ec := NewEventConfirmer(nil, nil, "eip155:1", 5, 5, 0, logger) + t.Run("unknown type uses standard depth", func(t *testing.T) { + ec := NewEventConfirmer(nil, nil, "eip155:1", 5, 5, 7, logger) result := ec.getRequiredConfirmations("INSTANT") - assert.Equal(t, uint64(0), result) + assert.Equal(t, uint64(7), result) }) } @@ -527,3 +529,76 @@ func TestProcessPendingEvents_FailedReceiptMarkedReverted(t *testing.T) { require.NoError(t, memDB.Client().Where("event_id = ?", pending.EventID).First(&got).Error) assert.Equal(t, store.StatusReverted, got.Status, "failed receipt must transition to REVERTED, not CONFIRMED") } + +// The skew this finding reports, end to end: the endpoint serving the receipt +// is ahead of the one serving the tip, so the tx block is above the latest +// block. Unchecked, latest-tx underflows to near 2^64 and clears any threshold. +// The event must stay PENDING and be retried, never confirmed. +func TestProcessPendingEvents_RPCHeightSkew_StaysPending(t *testing.T) { + txHash := "0x3333333333333333333333333333333333333333333333333333333333333333" + const ( + eventBlockHex = "0x96" // 150, the receipt endpoint is ahead + latestBlockHex = "0x64" // 100, the tip endpoint lags by 50 blocks + ) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + body := make([]byte, r.ContentLength) + r.Body.Read(body) + bodyStr := string(body) + + switch { + case strings.Contains(bodyStr, "eth_chainId"): + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"0x1"}`)) + case strings.Contains(bodyStr, "eth_blockNumber"): + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"` + latestBlockHex + `"}`)) + case strings.Contains(bodyStr, "eth_getTransactionReceipt"): + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{` + + `"transactionHash":"` + txHash + `",` + + `"blockNumber":"` + eventBlockHex + `",` + + `"blockHash":"0x4444444444444444444444444444444444444444444444444444444444444444",` + + `"transactionIndex":"0x0",` + + `"gasUsed":"0x5208",` + + `"cumulativeGasUsed":"0x5208",` + + `"logsBloom":"0x` + strings.Repeat("0", 512) + `",` + + `"logs":[],` + + `"status":"0x1",` + + `"type":"0x2"` + + `}}`)) + default: + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":null}`)) + } + })) + defer server.Close() + + logger := zerolog.Nop() + rpcClient, err := NewRPCClient([]string{server.URL}, 1, logger) + require.NoError(t, err) + defer rpcClient.Close() + + memDB, err := db.OpenInMemoryDB(true) + require.NoError(t, err) + defer memDB.Close() + + ec := NewEventConfirmer(rpcClient, memDB, "eip155:1", 5, 5, 12, logger) + cs := common.NewChainStore(memDB) + + pending := &store.Event{ + EventID: txHash + ":0", + BlockHeight: 150, + Type: store.EventTypeInbound, + ConfirmationType: store.ConfirmationStandard, + Status: store.StatusPending, + EventData: []byte(`{}`), + } + inserted, err := cs.InsertEventIfNotExists(pending) + require.NoError(t, err) + require.True(t, inserted) + + require.NoError(t, ec.processPendingEvents(context.Background())) + + var got store.Event + require.NoError(t, memDB.Client().Where("event_id = ?", pending.EventID).First(&got).Error) + assert.Equal(t, store.StatusPending, got.Status, + "a tx block above the observed tip must defer, not confirm") +} diff --git a/universalClient/externalchains/evm/event_listener.go b/universalClient/externalchains/evm/event_listener.go index d0104702e..d87775886 100644 --- a/universalClient/externalchains/evm/event_listener.go +++ b/universalClient/externalchains/evm/event_listener.go @@ -207,58 +207,94 @@ func (el *EventListener) processNewBlocks( } // Process blocks in range - if err := el.processBlockRange(ctx, *currentBlock, latestBlock, topics); err != nil { - return fmt.Errorf("failed to process block range: %w", err) + nextBlock, rangeErr := el.processBlockRange(ctx, *currentBlock, latestBlock, topics) + + // Commit whatever was covered even when a later chunk failed. Holding the + // cursor back would re-read the blocks already handled on every tick, so one + // unreadable window would sit in front of everything behind it indefinitely. + if nextBlock > *currentBlock { + if err := el.updateLastProcessedBlock(nextBlock - 1); err != nil { + el.logger.Error().Err(err).Msg("failed to update last processed block") + // Don't return error - continue processing + } + *currentBlock = nextBlock } - // Update last processed block in database - if err := el.updateLastProcessedBlock(latestBlock); err != nil { - el.logger.Error().Err(err).Msg("failed to update last processed block") - // Don't return error - continue processing + if rangeErr != nil { + return fmt.Errorf("failed to process block range: %w", rangeErr) } - - // Move to next block - *currentBlock = latestBlock + 1 return nil } -// processBlockRange processes events in a range of blocks +// Block span for a single eth_getLogs call. Providers cap the result set rather +// than the block count, so a dense window can be rejected at a span that is +// normally fine. maxBlockRange is the optimistic starting point and minBlockRange +// the floor we stop shrinking at. +const ( + maxBlockRange uint64 = 9000 // Safe under the 10000 RPC limit + minBlockRange uint64 = 100 +) + +// processBlockRange processes events in a range of blocks, returning the first +// block it did not cover. That is fromBlock when nothing was processed and +// toBlock+1 when everything was, so the caller can commit partial progress +// whether or not an error is also returned. +// +// A rejected query is retried over a smaller span rather than abandoned: the +// limit is on results, so halving until the window fits gets past a dense range +// that a fixed span cannot. Shrinking is linear rather than a recursive split, +// which would issue exponentially many calls against a range that keeps failing. func (el *EventListener) processBlockRange( ctx context.Context, fromBlock, toBlock uint64, topics []ethcommon.Hash, -) error { - const maxBlockRange uint64 = 9000 // Safe under the 10000 RPC limit +) (uint64, error) { + span := maxBlockRange + nextFrom := fromBlock - currentFrom := fromBlock - - // Process in chunks if the range is too large - for currentFrom <= toBlock { - currentTo := currentFrom + maxBlockRange - 1 - if currentTo > toBlock { + for nextFrom <= toBlock { + currentTo := nextFrom + span - 1 + if currentTo > toBlock || currentTo < nextFrom { // second test catches overflow currentTo = toBlock } - // Log chunk processing for large ranges - blockRange := currentTo - currentFrom + 1 + blockRange := currentTo - nextFrom + 1 if blockRange > 1000 { el.logger.Debug(). - Uint64("from_block", currentFrom). + Uint64("from_block", nextFrom). Uint64("to_block", currentTo). Uint64("range_size", blockRange). Msg("processing block chunk") } - // Process chunk - if err := el.processBlockChunk(ctx, currentFrom, currentTo, topics); err != nil { - return fmt.Errorf("failed to process chunk %d-%d: %w", currentFrom, currentTo, err) + if err := el.processBlockChunk(ctx, nextFrom, currentTo, topics); err != nil { + // Halve what was actually attempted, not the nominal span: near the end + // of a range the span is clamped to toBlock, so shrinking the span alone + // would resend the identical query until it dropped below the remainder. + if blockRange > minBlockRange { + span = blockRange / 2 + if span < minBlockRange { + span = minBlockRange + } + el.logger.Warn(). + Err(err). + Uint64("from_block", nextFrom). + Uint64("to_block", currentTo). + Uint64("retry_span", span). + Msg("log query failed, retrying the same start over a smaller span") + continue + } + + // At the floor the span is no longer the problem. Report the failure + // and leave the cursor here: skipping ahead would drop any deposits in + // these blocks permanently, which is worse than waiting for the RPC. + return nextFrom, fmt.Errorf("failed to process chunk %d-%d at minimum span: %w", nextFrom, currentTo, err) } - // Move to next chunk - currentFrom = currentTo + 1 + nextFrom = currentTo + 1 } - return nil + return nextFrom, nil } // processBlockChunk processes a single chunk of blocks diff --git a/universalClient/externalchains/evm/event_listener_test.go b/universalClient/externalchains/evm/event_listener_test.go index 848c7b162..477536f0f 100644 --- a/universalClient/externalchains/evm/event_listener_test.go +++ b/universalClient/externalchains/evm/event_listener_test.go @@ -2,6 +2,14 @@ package evm import ( "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "sort" + "strconv" + "strings" + "sync" "testing" "time" @@ -412,3 +420,248 @@ func TestEventListener_ContextCancellationStopsGoroutine(t *testing.T) { el.Stop() assert.False(t, el.IsRunning()) } + +// logQueryServer serves eth_getLogs, rejecting any query whose block span exceeds +// maxSpan the way a provider rejects an over-large result set, and recording the +// spans it was asked for so tests can assert how the client adapted. +type logQueryServer struct { + maxSpan uint64 + failFrom uint64 // when non-zero, reject any query overlapping this block onwards + mu sync.Mutex + asked [][2]uint64 + served [][2]uint64 // only the queries that actually returned logs +} + +func (s *logQueryServer) record(from, to uint64) { + s.mu.Lock() + defer s.mu.Unlock() + s.asked = append(s.asked, [2]uint64{from, to}) +} + +func (s *logQueryServer) spans() [][2]uint64 { + s.mu.Lock() + defer s.mu.Unlock() + return append([][2]uint64(nil), s.asked...) +} + +func (s *logQueryServer) servedSpans() [][2]uint64 { + s.mu.Lock() + defer s.mu.Unlock() + return append([][2]uint64(nil), s.served...) +} + +func (s *logQueryServer) recordServed(from, to uint64) { + s.mu.Lock() + defer s.mu.Unlock() + s.served = append(s.served, [2]uint64{from, to}) +} + +func (s *logQueryServer) start(t *testing.T) *RPCClient { + t.Helper() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + w.Header().Set("Content-Type", "application/json") + + if !strings.Contains(string(body), "eth_getLogs") { + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"0x1"}`)) + return + } + + var req struct { + Params []struct { + FromBlock string `json:"fromBlock"` + ToBlock string `json:"toBlock"` + } `json:"params"` + } + _ = json.Unmarshal(body, &req) + from, _ := strconv.ParseUint(strings.TrimPrefix(req.Params[0].FromBlock, "0x"), 16, 64) + to, _ := strconv.ParseUint(strings.TrimPrefix(req.Params[0].ToBlock, "0x"), 16, 64) + s.record(from, to) + + overSpan := to-from+1 > s.maxSpan + stuck := s.failFrom != 0 && to >= s.failFrom + if overSpan || stuck { + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"error":{"code":-32005,"message":"query returned more than 10000 results"}}`)) + return + } + s.recordServed(from, to) + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":[]}`)) + })) + t.Cleanup(srv.Close) + + rpcClient, err := NewRPCClient([]string{srv.URL}, 1, zerolog.Nop()) + require.NoError(t, err) + t.Cleanup(rpcClient.Close) + return rpcClient +} + +func newRangeListener(t *testing.T, rpcClient *RPCClient) *EventListener { + t.Helper() + el, err := NewEventListener(rpcClient, "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222", "eip155:1", nil, nil, testDB(t), 10, nil, zerolog.Nop()) + require.NoError(t, err) + return el +} + +// Providers cap the result set, not the block count, so a dense window is +// rejected at a span that is normally fine. A fixed span retries the same +// rejected query forever and the cursor never moves past it. +func TestProcessBlockRange_ShrinksSpanUntilTheQueryFits(t *testing.T) { + srv := &logQueryServer{maxSpan: 1000} // anything wider than 1000 blocks is rejected + el := newRangeListener(t, srv.start(t)) + + next, err := el.processBlockRange(context.Background(), 1, 2000, nil) + require.NoError(t, err) + assert.Equal(t, uint64(2001), next, "the whole range must end up covered") + + spans := srv.spans() + require.NotEmpty(t, spans) + + // The first attempt is optimistic, and every retry restarts at the same block + // rather than skipping the blocks that were rejected. + assert.Equal(t, uint64(1), spans[0][0]) + assert.Equal(t, uint64(2000), spans[0][1], "first attempt spans the whole range") + + var widths []uint64 + for _, s := range spans { + if s[0] == 1 { + widths = append(widths, s[1]-s[0]+1) + } + } + require.Greater(t, len(widths), 1, "must retry the same start over a smaller span") + for i := 1; i < len(widths); i++ { + assert.Less(t, widths[i], widths[i-1], "each retry must be narrower") + } +} + +// A window that cannot be read even at the floor must not be stepped over: +// the blocks may contain deposits, and skipping them loses those permanently. +func TestProcessBlockRange_DoesNotSkipAnUnreadableWindow(t *testing.T) { + srv := &logQueryServer{maxSpan: maxBlockRange, failFrom: 1} // every query fails + el := newRangeListener(t, srv.start(t)) + + next, err := el.processBlockRange(context.Background(), 1, 500, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "minimum span") + assert.Equal(t, uint64(1), next, "cursor must stay put, not advance past unread blocks") +} + +// Work already done must be committed. Holding the cursor at the start would +// re-read the earlier chunks on every tick, so one bad window would sit in front +// of everything behind it. +func TestProcessBlockRange_ReportsPartialProgressOnFailure(t *testing.T) { + // First 9000 blocks are readable; anything from 9001 always fails. + srv := &logQueryServer{maxSpan: maxBlockRange, failFrom: 9001} + el := newRangeListener(t, srv.start(t)) + + next, err := el.processBlockRange(context.Background(), 1, 20000, nil) + require.Error(t, err) + assert.Equal(t, uint64(9001), next, "must report the first block it could not cover") +} + +func TestProcessBlockRange_SinglePassWhenNothingIsRejected(t *testing.T) { + srv := &logQueryServer{maxSpan: maxBlockRange} + el := newRangeListener(t, srv.start(t)) + + next, err := el.processBlockRange(context.Background(), 1, 500, nil) + require.NoError(t, err) + assert.Equal(t, uint64(501), next) + assert.Len(t, srv.spans(), 1, "a range that fits must not be split") +} + +// assertExactCoverage checks that the served queries tile [from,to] with no gap +// and no block fetched twice. A gap is a block whose logs are never read, which +// for an inbound is a deposit nobody observes. +func assertExactCoverage(t *testing.T, served [][2]uint64, from, to uint64) { + t.Helper() + + sort.Slice(served, func(i, j int) bool { return served[i][0] < served[j][0] }) + + require.NotEmpty(t, served, "nothing was fetched for %d-%d", from, to) + assert.Equal(t, from, served[0][0], "coverage must start at the first block") + assert.Equal(t, to, served[len(served)-1][1], "coverage must end at the last block") + + for i := 1; i < len(served); i++ { + prevEnd, thisStart := served[i-1][1], served[i][0] + assert.Equal(t, prevEnd+1, thisStart, + "chunk %d starts at %d but the previous ended at %d", i, thisStart, prevEnd) + } + + var covered uint64 + for _, c := range served { + require.LessOrEqual(t, c[0], c[1], "chunk %d-%d is inverted", c[0], c[1]) + covered += c[1] - c[0] + 1 + } + assert.Equal(t, to-from+1, covered, "total blocks covered must equal the range size") +} + +// Every block in the range must be fetched exactly once, whatever the span ends +// up being. Off-by-one at a chunk boundary would silently skip a block. +func TestProcessBlockRange_CoversEveryBlockExactlyOnce(t *testing.T) { + cases := []struct { + name string + from, to uint64 + serverSpan uint64 // widest query the server will accept + }{ + {"single block", 1, 1, maxBlockRange}, + {"single block at zero", 0, 0, maxBlockRange}, + {"range starting at zero", 0, 500, maxBlockRange}, + {"exactly one full span", 1, maxBlockRange, maxBlockRange}, + {"one block past a full span", 1, maxBlockRange + 1, maxBlockRange}, + {"one block short of a full span", 1, maxBlockRange - 1, maxBlockRange}, + {"several full spans", 1, maxBlockRange * 3, maxBlockRange}, + {"several spans plus a remainder", 1, maxBlockRange*2 + 137, maxBlockRange}, + {"forced shrink, divisible", 1, 2000, 1000}, + {"forced shrink, not divisible", 1, 2500, 333}, + {"forced shrink to the floor", 1, 1000, minBlockRange}, + {"shrink with an odd start", 4097, 9999, 700}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + srv := &logQueryServer{maxSpan: tc.serverSpan} + el := newRangeListener(t, srv.start(t)) + + next, err := el.processBlockRange(context.Background(), tc.from, tc.to, nil) + require.NoError(t, err) + assert.Equal(t, tc.to+1, next, "must report the range as fully covered") + + assertExactCoverage(t, srv.servedSpans(), tc.from, tc.to) + }) + } +} + +// After a shrink the walk continues at the smaller span. The blocks either side +// of the failure boundary must still be covered exactly once. +func TestProcessBlockRange_NoGapAroundAShrink(t *testing.T) { + srv := &logQueryServer{maxSpan: 750} + el := newRangeListener(t, srv.start(t)) + + next, err := el.processBlockRange(context.Background(), 100, 3100, nil) + require.NoError(t, err) + assert.Equal(t, uint64(3101), next) + + assertExactCoverage(t, srv.servedSpans(), 100, 3100) +} + +// Across successive polls the caller resumes from the block the previous call +// reported, so a partial range must hand back a boundary that leaves no hole. +func TestProcessBlockRange_ResumeAfterPartialLeavesNoGap(t *testing.T) { + // Blocks from 5001 are unreadable, so the first call stops there. + srv := &logQueryServer{maxSpan: maxBlockRange, failFrom: 5001} + el := newRangeListener(t, srv.start(t)) + + next, err := el.processBlockRange(context.Background(), 1, 8000, nil) + require.Error(t, err) + assertExactCoverage(t, srv.servedSpans(), 1, next-1) + + // The obstruction clears and the caller resumes from where it stopped. + srv2 := &logQueryServer{maxSpan: maxBlockRange} + el2 := newRangeListener(t, srv2.start(t)) + + final, err := el2.processBlockRange(context.Background(), next, 8000, nil) + require.NoError(t, err) + assert.Equal(t, uint64(8001), final) + assertExactCoverage(t, srv2.servedSpans(), next, 8000) +} diff --git a/universalClient/externalchains/evm/event_parser.go b/universalClient/externalchains/evm/event_parser.go index a603f8ad7..d3427a492 100644 --- a/universalClient/externalchains/evm/event_parser.go +++ b/universalClient/externalchains/evm/event_parser.go @@ -28,7 +28,27 @@ const ( // ParseEvent parses a log into a store.Event based on the event type. // eventType should be one of: sendFunds, revertUniversalTx, finalizeUniversalTx, fundsRescued. -func ParseEvent(log *types.Log, eventType string, chainID string, logger zerolog.Logger) *store.Event { +// +// A panic in the decoders is contained here rather than allowed to unwind. Log +// data is supplied by an RPC and the listener runs on a background goroutine, so +// an unrecovered panic would take down every chain and the TSS node with it. A +// log we cannot decode is skipped like any other undecodable one. +func ParseEvent(log *types.Log, eventType string, chainID string, logger zerolog.Logger) (event *store.Event) { + defer func() { + if r := recover(); r != nil { + event = nil + logger.Error(). + Interface("panic", r). + Str("event_type", eventType). + Str("tx_hash", log.TxHash.Hex()). + Uint("log_index", log.Index). + Msg("panic while decoding log; skipping it") + } + }() + return parseEvent(log, eventType, chainID, logger) +} + +func parseEvent(log *types.Log, eventType string, chainID string, logger zerolog.Logger) *store.Event { if len(log.Topics) == 0 { return nil } @@ -74,8 +94,15 @@ func parseSendFundsEvent(log *types.Log, chainID string, logger zerolog.Logger) ExpiryBlockHeight: 0, // 0 means no expiry } - // Parse universal tx event data - parseUniversalTxEvent(event, log, chainID, logger) + // Parse universal tx event data. A malformed event is dropped rather than + // stored half-decoded: the zero values it would carry are not neutral. + if err := parseUniversalTxEvent(event, log, chainID, logger); err != nil { + logger.Warn(). + Err(err). + Str("event_id", eventID). + Msg("discarding malformed UniversalTx event") + return nil + } return event } @@ -162,10 +189,13 @@ func parseOutboundObservationEvent(log *types.Log, eventType string, logger zero } // parseUniversalTxEvent parses a UniversalTx event from log data. -func parseUniversalTxEvent(event *store.Event, log *types.Log, chainID string, logger zerolog.Logger) { +// +// Static words through txType are required. A truncated log is rejected rather +// than returned half-filled, since the zero values are not neutral: txType 0 is +// GAS, which routes funds to a different account than FUNDS does. +func parseUniversalTxEvent(event *store.Event, log *types.Log, chainID string, logger zerolog.Logger) error { if len(log.Topics) < 3 { - logger.Warn().Msg("not enough indexed fields; nothing to do") - return + return fmt.Errorf("need 3 indexed fields, got %d", len(log.Topics)) } payload := common.InboundObservation{ @@ -176,9 +206,7 @@ func parseUniversalTxEvent(event *store.Event, log *types.Log, chainID string, l } if len(log.Data) < 32*5 { - b, _ := json.Marshal(payload) - event.EventData = b - return + return fmt.Errorf("log data has %d bytes, need at least %d for the static words", len(log.Data), 32*5) } // Parse common static fields: token (Word 0), amount (Word 1) @@ -186,21 +214,28 @@ func parseUniversalTxEvent(event *store.Event, log *types.Log, chainID string, l payload.Amount = new(big.Int).SetBytes(log.Data[1*32 : 2*32]).String() dataOffset := new(big.Int).SetBytes(log.Data[2*32 : 3*32]).Uint64() - parseUniversalTx(event, log, dataOffset, &payload, logger) + return parseUniversalTx(event, log, dataOffset, &payload, logger) } // readDynamicBytes decodes ABI-encoded dynamic bytes at the given absolute offset in data. +// +// Both absOff and the length word are attacker-controlled: they come from the +// log data an RPC returns. Bounds are therefore checked by subtracting from the +// buffer length rather than adding to the offset — absOff+32 and dataStart+byteLen +// each wrap on a near-2^64 word and would pass an additive guard, then panic on +// the slice. func readDynamicBytes(data []byte, absOff uint64) (string, bool) { - if absOff+32 > uint64(len(data)) { + n := uint64(len(data)) + if absOff > n || n-absOff < 32 { return "", false } byteLen := new(big.Int).SetBytes(data[absOff : absOff+32]).Uint64() - dataStart := absOff + 32 - dataEnd := dataStart + byteLen - if dataEnd > uint64(len(data)) { + + dataStart := absOff + 32 // safe: absOff+32 <= n was just established + if n-dataStart < byteLen { return "", false } - return "0x" + hex.EncodeToString(data[dataStart:dataEnd]), true + return "0x" + hex.EncodeToString(data[dataStart:dataStart+byteLen]), true } // readWord returns the i-th 32-byte word from data, or nil if out of bounds. @@ -266,7 +301,7 @@ UniversalTx Event (V2 - upgraded chains): - signatureData (bytes) — Word 5 (offset) - fromCEA (bool) — Word 6 */ -func parseUniversalTx(event *store.Event, log *types.Log, dataOffset uint64, payload *common.InboundObservation, logger zerolog.Logger) { +func parseUniversalTx(event *store.Event, log *types.Log, dataOffset uint64, payload *common.InboundObservation, logger zerolog.Logger) error { data := log.Data decodePayload(data, dataOffset, payload, logger) @@ -276,10 +311,9 @@ func parseUniversalTx(event *store.Event, log *types.Log, dataOffset uint64, pay payload.RevertFundRecipient = ethcommon.BytesToAddress(w[12:32]).Hex() } - // txType (Word 4) - if w := readWord(data, 4); w != nil { - payload.TxType = uint(new(big.Int).SetBytes(w).Uint64()) - } + // txType (Word 4). Always present: the caller rejects anything shorter than + // five words, which is what stops this being left at 0 and read as GAS. + payload.TxType = uint(new(big.Int).SetBytes(readWord(data, 4)).Uint64()) // signatureData (Word 5 offset) if w := readWord(data, 5); w != nil { @@ -292,4 +326,5 @@ func parseUniversalTx(event *store.Event, log *types.Log, dataOffset uint64, pay } finalizeEvent(event, payload, logger) + return nil } diff --git a/universalClient/externalchains/evm/event_parser_test.go b/universalClient/externalchains/evm/event_parser_test.go index 88dbc8786..578a44ca7 100644 --- a/universalClient/externalchains/evm/event_parser_test.go +++ b/universalClient/externalchains/evm/event_parser_test.go @@ -3,6 +3,7 @@ package evm import ( "encoding/hex" "encoding/json" + "math" "math/big" "testing" @@ -182,7 +183,10 @@ func TestParseEventData(t *testing.T) { assert.NotNil(t, event.EventData) }) - t.Run("handles missing data gracefully", func(t *testing.T) { + // A log too short to carry txType is discarded rather than stored with the + // field left at 0, which is GAS on the wire and routes to a different + // account than FUNDS does. + t.Run("discards a log with no data", func(t *testing.T) { log := &types.Log{ Topics: []ethcommon.Hash{ ethcommon.HexToHash("0x1234"), @@ -192,9 +196,29 @@ func TestParseEventData(t *testing.T) { Data: []byte{}, // Empty data } - event := ParseEvent(log, EventTypeSendFunds, config.Chain, logger) - // Should still create event but with minimal data - require.NotNil(t, event) + assert.Nil(t, ParseEvent(log, EventTypeSendFunds, config.Chain, logger)) + }) + + t.Run("discards a log one word short of txType", func(t *testing.T) { + log := &types.Log{ + Topics: []ethcommon.Hash{ + ethcommon.HexToHash("0x1234"), + ethcommon.HexToHash("0x000000000000000000000000742d35cc6634c0532925a3b844bc9e7595f0beb7"), + ethcommon.HexToHash("0x000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7"), + }, + Data: make([]byte, 32*4), // words 0..3 present, txType (word 4) missing + } + + assert.Nil(t, ParseEvent(log, EventTypeSendFunds, config.Chain, logger)) + }) + + t.Run("discards a log without the indexed fields", func(t *testing.T) { + log := &types.Log{ + Topics: []ethcommon.Hash{ethcommon.HexToHash("0x1234")}, + Data: make([]byte, 32*8), + } + + assert.Nil(t, ParseEvent(log, EventTypeSendFunds, config.Chain, logger)) }) } @@ -700,3 +724,102 @@ func TestFinalizeEvent(t *testing.T) { assert.Equal(t, "1000", decoded.Amount) }) } + +// abiWord returns a 32-byte big-endian word holding v, for building hostile log data. +func abiWord(v *big.Int) []byte { + w := make([]byte, 32) + v.FillBytes(w) + return w +} + +// Both the offset and the length word come from the RPC, so both can be chosen +// to overflow uint64. Addition-based bounds wrap and pass, then the slice panics +// — and the listener has no caller between here and the goroutine root, so that +// panic would end the process. +func TestReadDynamicBytes_OverflowIsRejectedNotPanicked(t *testing.T) { + maxU64 := new(big.Int).SetUint64(math.MaxUint64) + + t.Run("offset near 2^64 does not wrap past the bounds check", func(t *testing.T) { + data := make([]byte, 128) + for _, off := range []uint64{ + math.MaxUint64, // absOff + 32 wraps to 31 + math.MaxUint64 - 16, // wraps to 15 + math.MaxUint64 - 31, // wraps to 0 + math.MaxUint64 - 32, // wraps to exactly 0 after the +32 + } { + _, ok := readDynamicBytes(data, off) + assert.False(t, ok, "offset %d must be rejected", off) + } + }) + + t.Run("length near 2^64 does not wrap the end below the start", func(t *testing.T) { + // Word at offset 0 is the length; make it enormous so dataStart+byteLen wraps. + data := make([]byte, 128) + copy(data[0:32], abiWord(maxU64)) + + _, ok := readDynamicBytes(data, 0) + assert.False(t, ok, "a length that wraps the end must be rejected") + }) + + t.Run("length just past the buffer is rejected without wrapping", func(t *testing.T) { + data := make([]byte, 128) + copy(data[0:32], abiWord(big.NewInt(97))) // 32 header + 97 > 128 + _, ok := readDynamicBytes(data, 0) + assert.False(t, ok) + }) + + t.Run("well formed input still decodes", func(t *testing.T) { + data := make([]byte, 128) + copy(data[0:32], abiWord(big.NewInt(4))) + copy(data[32:36], []byte{0xDE, 0xAD, 0xBE, 0xEF}) + + got, ok := readDynamicBytes(data, 0) + require.True(t, ok) + assert.Equal(t, "0xdeadbeef", got) + }) + + t.Run("zero length decodes to empty", func(t *testing.T) { + data := make([]byte, 64) + got, ok := readDynamicBytes(data, 0) + require.True(t, ok) + assert.Equal(t, "0x", got) + }) + + t.Run("exactly filling the buffer decodes", func(t *testing.T) { + data := make([]byte, 64) + copy(data[0:32], abiWord(big.NewInt(32))) + copy(data[32:64], abiWord(big.NewInt(1))) + + _, ok := readDynamicBytes(data, 32+32-32) // offset 32 is past the end for a 64-byte buffer + assert.False(t, ok) + + got, ok := readDynamicBytes(data, 0) + require.True(t, ok) + assert.Len(t, got, 2+64) + }) +} + +// End to end: a log carrying an overflowing payload offset must be skipped, not +// crash the listener goroutine. +func TestParseEvent_HostileLogDoesNotPanic(t *testing.T) { + // 5 words of data so the length guard passes, with word 2 (the payload + // offset) set to a value that overflows when 32 is added to it. + data := make([]byte, 32*5) + copy(data[2*32:3*32], abiWord(new(big.Int).SetUint64(math.MaxUint64))) + + log := &types.Log{ + Topics: []ethcommon.Hash{ + ethcommon.HexToHash("0x01"), + ethcommon.HexToHash("0x02"), + ethcommon.HexToHash("0x03"), + }, + Data: data, + TxHash: ethcommon.HexToHash("0xabc"), + Index: 7, + Address: ethcommon.HexToAddress("0xdead"), + } + + require.NotPanics(t, func() { + ParseEvent(log, EventTypeSendFunds, "eip155:1", zerolog.Nop()) + }) +} diff --git a/universalClient/externalchains/evm/l1fee_test.go b/universalClient/externalchains/evm/l1fee_test.go new file mode 100644 index 000000000..d6f8e8971 --- /dev/null +++ b/universalClient/externalchains/evm/l1fee_test.go @@ -0,0 +1,172 @@ +package evm + +import ( + "context" + "math/big" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// receiptRPC serves eth_chainId plus a single receipt for any receipt lookup. +func receiptRPC(t *testing.T, receiptJSON string) *RPCClient { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + body := make([]byte, r.ContentLength) + r.Body.Read(body) + switch { + case strings.Contains(string(body), "eth_chainId"): + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"0xaa36a7"}`)) // 11155111 + case strings.Contains(string(body), "eth_getTransactionReceipt"): + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":` + receiptJSON + `}`)) + default: + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":null}`)) + } + })) + t.Cleanup(server.Close) + + rc, err := NewRPCClient([]string{server.URL}, 11155111, zerolog.Nop()) + require.NoError(t, err) + t.Cleanup(func() { rc.Close() }) + return rc +} + +// receipt builds a minimal receipt JSON. gasUsed 0x5208 (21000), +// effectiveGasPrice 0x4a817c800 (20 gwei) unless withEffPrice is false; +// l1FeeField is "" for non-OP chains. +func receipt(l1FeeField string, withEffPrice bool) string { + eff := "" + if withEffPrice { + eff = `"effectiveGasPrice":"0x4a817c800",` + } + return `{"transactionHash":"0xabc","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222222",` + + `"blockNumber":"0x1","transactionIndex":"0x0","cumulativeGasUsed":"0x5208",` + + `"gasUsed":"0x5208",` + eff + `"status":"0x1","contractAddress":null,` + + `"logs":[],"logsBloom":"0x` + strings.Repeat("0", 512) + `",` + l1FeeField + `"type":"0x0"}` +} + +// A nonzero l1Fee must be added to GasFeeUsed so the core refund (gasFee − +// GasFeeUsed) shrinks by exactly that amount; both come from one receipt read. +func TestGetGasFeeUsed(t *testing.T) { + execFee := new(big.Int).Mul(big.NewInt(21000), big.NewInt(20_000_000_000)) // gasUsed * effectiveGasPrice + tb := func(rc *RPCClient) *TxBuilder { + return &TxBuilder{rpcClient: rc, chainID: "eip155:11155111", chainIDInt: 11155111, logger: zerolog.Nop()} + } + + t.Run("OP destination adds l1Fee", func(t *testing.T) { + got, err := tb(receiptRPC(t, receipt(`"l1Fee":"0x5208",`, true))).GetGasFeeUsed(context.Background(), "0xabc") + require.NoError(t, err) + assert.Equal(t, new(big.Int).Add(execFee, big.NewInt(0x5208)).String(), got) + }) + + t.Run("non-OP destination is execution fee only", func(t *testing.T) { + got, err := tb(receiptRPC(t, receipt(``, true))).GetGasFeeUsed(context.Background(), "0xabc") + require.NoError(t, err) + assert.Equal(t, execFee.String(), got) + }) + + // Errors rather than "0": a zero fee here would make core refund the full + // gasFee, the same over-refund this fix removes. Callers retry instead. + t.Run("missing effectiveGasPrice errors", func(t *testing.T) { + _, err := tb(receiptRPC(t, receipt(`"l1Fee":"0x5208",`, false))).GetGasFeeUsed(context.Background(), "0xabc") + require.Error(t, err) + assert.Contains(t, err.Error(), "missing effectiveGasPrice") + }) + + t.Run("missing receipt errors", func(t *testing.T) { + _, err := tb(receiptRPC(t, `null`)).GetGasFeeUsed(context.Background(), "0xabc") + require.Error(t, err) + assert.Contains(t, err.Error(), "receipt not found") + }) +} + +// GetTransactionReceipt surfaces effectiveGasPrice (nil when absent) and l1Fee. +func TestGetTransactionReceipt_Fields(t *testing.T) { + hash := ethcommon.HexToHash("0xabc") + + t.Run("effectiveGasPrice and l1Fee parsed", func(t *testing.T) { + r, err := receiptRPC(t, receipt(`"l1Fee":"0x5208",`, true)).GetTransactionReceipt(context.Background(), hash) + require.NoError(t, err) + require.NotNil(t, r) + assert.Equal(t, int64(20_000_000_000), r.EffectiveGasPrice.Int64()) + assert.Equal(t, int64(0x5208), r.L1Fee.Int64()) + }) + + t.Run("nil effectiveGasPrice when absent", func(t *testing.T) { + r, err := receiptRPC(t, receipt(``, false)).GetTransactionReceipt(context.Background(), hash) + require.NoError(t, err) + require.NotNil(t, r) + assert.Nil(t, r.EffectiveGasPrice) + assert.Equal(t, int64(0), r.L1Fee.Int64()) + }) +} + +// TestLive_GasFeeUsed exercises the real fetch + fee computation against public +// RPCs for two known txs (one non-OP, one OP). Skipped by default; run with: +// +// RUN_LIVE_RPC_TESTS=1 go test ./universalClient/externalchains/evm/ -run TestLive_GasFeeUsed -v +func TestLive_GasFeeUsed(t *testing.T) { + if os.Getenv("RUN_LIVE_RPC_TESTS") != "1" { + t.Skip("set RUN_LIVE_RPC_TESTS=1 to run live RPC test") + } + + cases := []struct { + name string + rpcURL string + chainID int64 + txHash string + wantFee string // gasUsed*effectiveGasPrice + l1Fee + wantL1 string + }{ + { + name: "Ethereum Sepolia (non-OP, l1Fee=0)", + rpcURL: "https://ethereum-sepolia-rpc.publicnode.com", + chainID: 11155111, + txHash: "0x489fb72d961e9bd69983fdaa52f0c9113705330f2e4bf4ac3fc46e1fb2977f08", + wantFee: "170830319373250", + wantL1: "0", + }, + { + name: "Base Sepolia (OP, nonzero l1Fee)", + rpcURL: "https://sepolia.base.org", + chainID: 84532, + txHash: "0x7b961e5cfbb6f8ddced1a0694773290ddb0d32caaaf8494f850d7ad07ddc0c30", + wantFee: "1032488370864", + wantL1: "14015970864", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + rc, err := NewRPCClient([]string{tc.rpcURL}, tc.chainID, zerolog.Nop()) + require.NoError(t, err) + defer rc.Close() + + receipt, err := rc.GetTransactionReceipt(context.Background(), ethcommon.HexToHash(tc.txHash)) + require.NoError(t, err) + require.NotNil(t, receipt, "tx not found on chain") + require.NotNil(t, receipt.EffectiveGasPrice, "receipt missing effectiveGasPrice") + + fee := gasFeeUsed(receipt.GasUsed, receipt.EffectiveGasPrice, receipt.L1Fee) + t.Logf("gasUsed=%d effectiveGasPrice=%s l1Fee=%s => GasFeeUsed=%s", + receipt.GasUsed, receipt.EffectiveGasPrice, receipt.L1Fee, fee) + + assert.Equal(t, tc.wantL1, receipt.L1Fee.String(), "l1Fee") + assert.Equal(t, tc.wantFee, fee.String(), "GasFeeUsed") + + // Full path through the TxBuilder entrypoint. + tb := &TxBuilder{rpcClient: rc, chainIDInt: tc.chainID, logger: zerolog.Nop()} + got, err := tb.GetGasFeeUsed(context.Background(), tc.txHash) + require.NoError(t, err) + assert.Equal(t, tc.wantFee, got) + }) + } +} diff --git a/universalClient/externalchains/evm/rpc_client.go b/universalClient/externalchains/evm/rpc_client.go index 433a8ef3c..e19dad083 100644 --- a/universalClient/externalchains/evm/rpc_client.go +++ b/universalClient/externalchains/evm/rpc_client.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/rs/zerolog" @@ -232,15 +233,53 @@ func (rc *RPCClient) FilterLogs(ctx context.Context, query ethereum.FilterQuery) return logs, err } -// GetTransactionReceipt fetches a transaction receipt -func (rc *RPCClient) GetTransactionReceipt(ctx context.Context, txHash ethcommon.Hash) (*types.Receipt, error) { - var receipt *types.Receipt +// Receipt holds the transaction-receipt fields the universal client needs, +// including the OP-Stack L1 data fee that go-ethereum's typed receipt omits. +type Receipt struct { + Status uint64 + BlockNumber uint64 + GasUsed uint64 + EffectiveGasPrice *big.Int // nil if the receipt omits the field (pre-London / non-compliant RPC) + L1Fee *big.Int // OP-Stack L1 data fee; 0 on non-OP chains +} + +// GetTransactionReceipt fetches a transaction receipt in a single raw call, +// reading the OP-Stack l1Fee alongside the standard fields. Returns (nil, nil) +// if the tx is not found (receipt is null). +func (rc *RPCClient) GetTransactionReceipt(ctx context.Context, txHash ethcommon.Hash) (*Receipt, error) { + var raw struct { + Status *hexutil.Uint64 `json:"status"` + BlockNumber *hexutil.Big `json:"blockNumber"` + GasUsed *hexutil.Uint64 `json:"gasUsed"` + EffectiveGasPrice *hexutil.Big `json:"effectiveGasPrice"` + L1Fee *hexutil.Big `json:"l1Fee"` + } err := rc.executeWithFailover(ctx, "get_transaction_receipt", func(client *ethclient.Client) error { - var innerErr error - receipt, innerErr = client.TransactionReceipt(ctx, txHash) - return innerErr + return client.Client().CallContext(ctx, &raw, "eth_getTransactionReceipt", txHash) }) - return receipt, err + if err != nil { + return nil, err + } + if raw.GasUsed == nil { + return nil, nil // not found + } + r := &Receipt{ + GasUsed: uint64(*raw.GasUsed), + L1Fee: big.NewInt(0), + } + if raw.Status != nil { + r.Status = uint64(*raw.Status) + } + if raw.BlockNumber != nil { + r.BlockNumber = (*big.Int)(raw.BlockNumber).Uint64() + } + if raw.EffectiveGasPrice != nil { + r.EffectiveGasPrice = (*big.Int)(raw.EffectiveGasPrice) + } + if raw.L1Fee != nil { + r.L1Fee = (*big.Int)(raw.L1Fee) + } + return r, nil } // GetTransactionByHash returns a transaction by its hash. diff --git a/universalClient/externalchains/evm/tx_builder.go b/universalClient/externalchains/evm/tx_builder.go index d74198516..c934c77bf 100644 --- a/universalClient/externalchains/evm/tx_builder.go +++ b/universalClient/externalchains/evm/tx_builder.go @@ -251,14 +251,18 @@ func (tb *TxBuilder) VerifyBroadcastedTx(ctx context.Context, txHash string) (fo hash := ethcommon.HexToHash(txHash) receipt, err := tb.rpcClient.GetTransactionReceipt(ctx, hash) if err != nil { + // Reporting a not-found verdict here would let the resolver vote failure against a tx that already executed. + return false, 0, 0, 0, err + } + if receipt == nil { return false, 0, 0, 0, nil } - receiptBlock := receipt.BlockNumber.Uint64() + receiptBlock := receipt.BlockNumber var confs uint64 - latestBlock, err := tb.rpcClient.GetLatestBlock(ctx) - if err == nil && latestBlock >= receiptBlock { + latestBlock, blockErr := tb.rpcClient.GetLatestBlock(ctx) + if blockErr == nil && latestBlock >= receiptBlock { confs = latestBlock - receiptBlock + 1 } @@ -448,38 +452,35 @@ func (tb *TxBuilder) IsAlreadyExecuted(ctx context.Context, txID string) (bool, return false, 0, nil } -// GetGasFeeUsed returns the gas fee used by a transaction on the EVM chain. -// Fetches the receipt for gasUsed and the transaction for gasPrice, then returns -// gasUsed * gasPrice as a decimal string. Returns "0" if not found. +// GetGasFeeUsed returns the gas fee used by a transaction on the EVM chain: +// L2 execution (gasUsed * effectiveGasPrice) plus the OP-Stack L1 data fee +// (0 on non-OP chains). Errors when the fee cannot be determined so callers +// retry rather than record an under-reported fee. func (tb *TxBuilder) GetGasFeeUsed(ctx context.Context, txHash string) (string, error) { - hash := ethcommon.HexToHash(txHash) - receipt, err := tb.rpcClient.GetTransactionReceipt(ctx, hash) + receipt, err := tb.rpcClient.GetTransactionReceipt(ctx, ethcommon.HexToHash(txHash)) if err != nil { - return "0", nil + return "", fmt.Errorf("failed to fetch receipt for %s: %w", txHash, err) } - - tx, _, err := tb.rpcClient.GetTransactionByHash(ctx, hash) - if err != nil { - return "0", nil + if receipt == nil { + return "", fmt.Errorf("receipt not found for %s", txHash) } - - gasUsed := new(big.Int).SetUint64(receipt.GasUsed) - gasPrice := tx.GasPrice() - if gasPrice == nil || gasPrice.Sign() == 0 { - return "0", nil + if receipt.EffectiveGasPrice == nil { + return "", fmt.Errorf("receipt for %s missing effectiveGasPrice", txHash) } + return gasFeeUsed(receipt.GasUsed, receipt.EffectiveGasPrice, receipt.L1Fee).String(), nil +} - gasFeeUsed := new(big.Int).Mul(gasUsed, gasPrice) - return gasFeeUsed.String(), nil +// gasFeeUsed returns the full destination cost of an included tx: L2 execution +// (gasUsed * effectiveGasPrice) plus the OP-Stack L1 data fee. +func gasFeeUsed(gasUsed uint64, gasPrice, l1Fee *big.Int) *big.Int { + fee := new(big.Int).Mul(new(big.Int).SetUint64(gasUsed), gasPrice) + return fee.Add(fee, l1Fee) } -// GetFundMigrationSigningRequest builds a native token transfer for fund migration, -// transferring the maximum possible balance (balance minus gas cost minus L1 fee). -// Fund migration only triggers when outbound is disabled and no pending outbounds remain, -// so the balance at signing time will equal the balance at broadcast time. -// L1GasFee covers OP-stack sequencer data-availability charges; 0 for non-L2 chains. +// GetFundMigrationSigningRequest builds the native transfer sweeping the old TSS +// balance to the current one. The amount is pinned on chain, so this makes no RPC +// call and stays reproducible after the sweep has already landed. func (tb *TxBuilder) GetFundMigrationSigningRequest(ctx context.Context, data *common.FundMigrationData, nonce uint64) (*common.UnsignedSigningReq, error) { - fromAddr := ethcommon.HexToAddress(data.From) toAddr := ethcommon.HexToAddress(data.To) if data.GasPrice == nil || data.GasPrice.Sign() == 0 { @@ -489,26 +490,14 @@ func (tb *TxBuilder) GetFundMigrationSigningRequest(ctx context.Context, data *c return nil, fmt.Errorf("gas limit must be provided for fund migration") } - var balance *big.Int - if data.Balance != nil { - balance = new(big.Int).Set(data.Balance) - } else { - queried, err := tb.rpcClient.GetBalance(ctx, fromAddr) - if err != nil { - return nil, fmt.Errorf("failed to get balance of %s: %w", data.From, err) - } - balance = queried - } - - maxTransfer, err := computeFundMigrationTransfer(balance, data.GasPrice, data.GasLimit, data.L1GasFee) - if err != nil { - return nil, err + if data.TransferAmount == nil || data.TransferAmount.Sign() <= 0 { + return nil, fmt.Errorf("fund migration transfer amount is required") } + maxTransfer := new(big.Int).Set(data.TransferAmount) tb.logger.Debug(). Str("from", data.From). Str("to", data.To). - Str("balance", balance.String()). Str("gas_price", data.GasPrice.String()). Uint64("gas_limit", data.GasLimit). Str("l1_gas_fee", l1GasFeeString(data.L1GasFee)). @@ -527,17 +516,13 @@ func (tb *TxBuilder) GetFundMigrationSigningRequest(ctx context.Context, data *c signer := types.NewEIP155Signer(big.NewInt(tb.chainIDInt)) txHash := signer.Hash(tx).Bytes() - // TSSFundMigrationAmount rides alongside Nonce in the req — both are signing-time-decided - // values that must reach broadcast unchanged so the signed tx is reproduced exactly. return &common.UnsignedSigningReq{ - SigningHash: txHash, - Nonce: nonce, - TSSFundMigrationAmount: new(big.Int).Set(maxTransfer), + SigningHash: txHash, + Nonce: nonce, }, nil } // BroadcastFundMigrationTx assembles and broadcasts a signed fund migration transaction. -// Uses req.TSSFundMigrationAmount fixed at signing time — do not re-query balance. func (tb *TxBuilder) BroadcastFundMigrationTx(ctx context.Context, req *common.UnsignedSigningReq, data *common.FundMigrationData, signature []byte) (string, error) { if len(signature) != 65 { return "", fmt.Errorf("signature must be 65 bytes [r(32)|s(32)|v(1)], got %d", len(signature)) @@ -550,13 +535,11 @@ func (tb *TxBuilder) BroadcastFundMigrationTx(ctx context.Context, req *common.U return "", fmt.Errorf("gas limit must be provided for fund migration") } - // Use the exact amount fixed at signing time. Re-querying balance here would race - // with a successful broadcast from another validator (balance goes to 0 post-sweep). - if req.TSSFundMigrationAmount == nil || req.TSSFundMigrationAmount.Sign() <= 0 { - return "", fmt.Errorf("req.TSSFundMigrationAmount must be set for fund migration broadcast") + if data.TransferAmount == nil || data.TransferAmount.Sign() <= 0 { + return "", fmt.Errorf("fund migration transfer amount is required for broadcast") } toAddr := ethcommon.HexToAddress(data.To) - maxTransfer := new(big.Int).Set(req.TSSFundMigrationAmount) + maxTransfer := new(big.Int).Set(data.TransferAmount) tx := types.NewTransaction( req.Nonce, @@ -586,25 +569,6 @@ func (tb *TxBuilder) BroadcastFundMigrationTx(ctx context.Context, req *common.U return txHashStr, nil } -// computeFundMigrationTransfer returns the native amount to sweep from the old -// TSS address to the new one: balance - (gasPrice * gasLimit) - l1GasFee. -// The l1GasFee covers OP-stack sequencer data-availability charges (0 for -// non-L2 chains). All validators must compute the same value — any drift -// here breaks the TSS signing hash. -func computeFundMigrationTransfer(balance, gasPrice *big.Int, gasLimit uint64, l1GasFee *big.Int) (*big.Int, error) { - gasCost := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gasLimit)) - totalFee := new(big.Int).Set(gasCost) - if l1GasFee != nil && l1GasFee.Sign() > 0 { - totalFee.Add(totalFee, l1GasFee) - } - maxTransfer := new(big.Int).Sub(balance, totalFee) - if maxTransfer.Sign() <= 0 { - return nil, fmt.Errorf("insufficient balance for gas: balance=%s gasCost=%s l1GasFee=%s", - balance.String(), gasCost.String(), l1GasFeeString(l1GasFee)) - } - return maxTransfer, nil -} - // l1GasFeeString returns a stable decimal representation of the L1 gas fee // for logging / error messages, treating nil as "0". func l1GasFeeString(v *big.Int) string { diff --git a/universalClient/externalchains/evm/tx_builder_test.go b/universalClient/externalchains/evm/tx_builder_test.go index 9f28671c0..e2f489ecc 100644 --- a/universalClient/externalchains/evm/tx_builder_test.go +++ b/universalClient/externalchains/evm/tx_builder_test.go @@ -3,13 +3,20 @@ package evm import ( "context" "encoding/hex" + "encoding/json" + "io" "math/big" + "net/http" + "net/http/httptest" "os" + "strings" "testing" "time" "github.com/ethereum/go-ethereum/accounts/abi" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" @@ -17,6 +24,7 @@ import ( "github.com/pushchain/push-chain-node/universalClient/externalchains/common" uetypes "github.com/pushchain/push-chain-node/x/uexecutor/types" + utsstypes "github.com/pushchain/push-chain-node/x/utss/types" ) // testVaultAddress is a non-zero address used as the vault in tests @@ -943,6 +951,130 @@ func TestSimulateBSC_RescueFunds_ERC20(t *testing.T) { // BSC simulation tests above). // --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// VerifyBroadcastedTx +// --------------------------------------------------------------------------- + +const testReceiptTxHash = "0x1111111111111111111111111111111111111111111111111111111111111111" + +// newReceiptRPCBuilder drives the real RPCClient against a local JSON-RPC +// server, so these exercise the same path production takes rather than a mock +// that can return errors the real client never produces. +func newReceiptRPCBuilder(t *testing.T, respond func(method string, w http.ResponseWriter)) *TxBuilder { + t.Helper() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + w.Header().Set("Content-Type", "application/json") + switch { + case strings.Contains(string(body), "eth_getTransactionReceipt"): + respond("eth_getTransactionReceipt", w) + case strings.Contains(string(body), "eth_blockNumber"): + respond("eth_blockNumber", w) + default: + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"0x1"}`)) + } + })) + t.Cleanup(server.Close) + + rpcClient, err := NewRPCClient([]string{server.URL}, 1, zerolog.Nop()) + require.NoError(t, err) + t.Cleanup(rpcClient.Close) + + return &TxBuilder{rpcClient: rpcClient, chainID: "eip155:1", chainIDInt: 1, logger: zerolog.Nop()} +} + +// A receipt RPC failure is not evidence about the transaction. Reported as +// not-found it combines with a consumed nonce to look like "never executed", +// and the resolver votes failure against an outbound the destination already paid. +func TestVerifyBroadcastedTx_ReceiptRPCFailureIsNotAVerdict(t *testing.T) { + t.Run("json-rpc error", func(t *testing.T) { + tb := newReceiptRPCBuilder(t, func(_ string, w http.ResponseWriter) { + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"error":{"code":-32005,"message":"rate limited"}}`)) + }) + + found, _, _, _, err := tb.VerifyBroadcastedTx(context.Background(), testReceiptTxHash) + require.Error(t, err, "an unreachable chain must not be reported as a verdict") + assert.False(t, found) + }) + + t.Run("transport failure", func(t *testing.T) { + tb := newReceiptRPCBuilder(t, func(_ string, w http.ResponseWriter) { + w.WriteHeader(http.StatusInternalServerError) + }) + + found, _, _, _, err := tb.VerifyBroadcastedTx(context.Background(), testReceiptTxHash) + require.Error(t, err) + assert.False(t, found) + }) + + t.Run("null receipt is a real not-found", func(t *testing.T) { + tb := newReceiptRPCBuilder(t, func(_ string, w http.ResponseWriter) { + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":null}`)) + }) + + found, _, _, _, err := tb.VerifyBroadcastedTx(context.Background(), testReceiptTxHash) + require.NoError(t, err, "the chain answered, so this is a verdict and not a failure") + assert.False(t, found) + }) +} + +func TestVerifyBroadcastedTx_ReceiptFound(t *testing.T) { + const receipt = `{"jsonrpc":"2.0","id":1,"result":{"status":"0x1","blockNumber":"0x64","gasUsed":"0x5208","effectiveGasPrice":"0x3b9aca00"}}` + + t.Run("reports block height, confirmations and status", func(t *testing.T) { + tb := newReceiptRPCBuilder(t, func(method string, w http.ResponseWriter) { + if method == "eth_blockNumber" { + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"0x6e"}`)) // 110 + return + } + _, _ = w.Write([]byte(receipt)) + }) + + found, blockHeight, confs, status, err := tb.VerifyBroadcastedTx(context.Background(), testReceiptTxHash) + require.NoError(t, err) + assert.True(t, found) + assert.Equal(t, uint64(100), blockHeight) + assert.Equal(t, uint64(11), confs) + assert.Equal(t, uint8(1), status) + }) + + // The block number is only needed for the confirmation count. Failing it must + // not discard the receipt we already have: zero confirmations makes the + // resolver wait, which is the correct outcome. + t.Run("block number failure keeps the tx found with zero confirmations", func(t *testing.T) { + tb := newReceiptRPCBuilder(t, func(method string, w http.ResponseWriter) { + if method == "eth_blockNumber" { + w.WriteHeader(http.StatusInternalServerError) + return + } + _, _ = w.Write([]byte(receipt)) + }) + + found, blockHeight, confs, status, err := tb.VerifyBroadcastedTx(context.Background(), testReceiptTxHash) + require.NoError(t, err) + assert.True(t, found) + assert.Equal(t, uint64(100), blockHeight) + assert.Zero(t, confs) + assert.Equal(t, uint8(1), status) + }) + + t.Run("reverted receipt reports status zero", func(t *testing.T) { + tb := newReceiptRPCBuilder(t, func(method string, w http.ResponseWriter) { + if method == "eth_blockNumber" { + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"0x6e"}`)) + return + } + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{"status":"0x0","blockNumber":"0x64","gasUsed":"0x5208"}}`)) + }) + + found, _, _, status, err := tb.VerifyBroadcastedTx(context.Background(), testReceiptTxHash) + require.NoError(t, err) + assert.True(t, found) + assert.Equal(t, uint8(0), status) + }) +} + // --------------------------------------------------------------------------- // parseGasLimit — additional edge-case coverage // --------------------------------------------------------------------------- @@ -1062,74 +1194,6 @@ func TestNewTxBuilderZeroGatewayAddress(t *testing.T) { // Fund migration transfer math // --------------------------------------------------------------------------- -// TestComputeFundMigrationTransfer covers the sweep-amount formula -// balance - (gasPrice * gasLimit) - l1GasFee for both L1 and L2-style chains. -// All validators must compute the same value — any drift breaks the TSS hash. -func TestComputeFundMigrationTransfer(t *testing.T) { - t.Run("no L1 fee (mainnet-style) nil", func(t *testing.T) { - // balance 1 ETH, gasPrice 20 gwei, gasLimit 21000 → gasCost = 420000 gwei - balance := new(big.Int).SetUint64(1_000_000_000_000_000_000) - gasPrice := new(big.Int).SetUint64(20_000_000_000) - got, err := computeFundMigrationTransfer(balance, gasPrice, 21000, nil) - require.NoError(t, err) - want := new(big.Int).Sub(balance, new(big.Int).Mul(gasPrice, big.NewInt(21000))) - assert.Equal(t, want.String(), got.String()) - }) - - t.Run("zero L1 fee (mainnet-style) treated as zero", func(t *testing.T) { - balance := new(big.Int).SetUint64(1_000_000_000_000_000_000) - gasPrice := new(big.Int).SetUint64(20_000_000_000) - got, err := computeFundMigrationTransfer(balance, gasPrice, 21000, big.NewInt(0)) - require.NoError(t, err) - want := new(big.Int).Sub(balance, new(big.Int).Mul(gasPrice, big.NewInt(21000))) - assert.Equal(t, want.String(), got.String()) - }) - - t.Run("non-zero L1 fee (OP-stack) is subtracted on top of L2 gas cost", func(t *testing.T) { - // 1 ETH balance, L2 gasCost=420000 gwei, L1 data-availability fee=150 gwei - balance := new(big.Int).SetUint64(1_000_000_000_000_000_000) - gasPrice := new(big.Int).SetUint64(20_000_000_000) - l1Fee := new(big.Int).SetUint64(150_000_000_000) - got, err := computeFundMigrationTransfer(balance, gasPrice, 21000, l1Fee) - require.NoError(t, err) - gasCost := new(big.Int).Mul(gasPrice, big.NewInt(21000)) - want := new(big.Int).Sub(balance, new(big.Int).Add(gasCost, l1Fee)) - assert.Equal(t, want.String(), got.String()) - }) - - t.Run("balance exactly equals total fee → insufficient", func(t *testing.T) { - gasPrice := new(big.Int).SetUint64(20_000_000_000) - l1Fee := big.NewInt(100) - gasCost := new(big.Int).Mul(gasPrice, big.NewInt(21000)) - balance := new(big.Int).Add(gasCost, l1Fee) - _, err := computeFundMigrationTransfer(balance, gasPrice, 21000, l1Fee) - require.Error(t, err) - assert.Contains(t, err.Error(), "insufficient balance") - }) - - t.Run("L1 fee tips balance into insufficient", func(t *testing.T) { - // Without L1 fee, balance covers gas and leaves 100 wei. With L1 fee of 200, it's insufficient. - gasPrice := new(big.Int).SetUint64(20_000_000_000) - gasCost := new(big.Int).Mul(gasPrice, big.NewInt(21000)) - balance := new(big.Int).Add(gasCost, big.NewInt(100)) - _, err := computeFundMigrationTransfer(balance, gasPrice, 21000, big.NewInt(200)) - require.Error(t, err) - assert.Contains(t, err.Error(), "insufficient balance") - }) - - t.Run("deterministic across equivalent l1 fee representations", func(t *testing.T) { - // big.NewInt(0) and nil must produce identical results — the TSS signing - // hash depends on it. - balance := new(big.Int).SetUint64(500_000_000_000_000_000) - gasPrice := new(big.Int).SetUint64(15_000_000_000) - withNil, err := computeFundMigrationTransfer(balance, gasPrice, 21000, nil) - require.NoError(t, err) - withZero, err := computeFundMigrationTransfer(balance, gasPrice, 21000, big.NewInt(0)) - require.NoError(t, err) - assert.Equal(t, withNil.String(), withZero.String()) - }) -} - func TestL1GasFeeString(t *testing.T) { assert.Equal(t, "0", l1GasFeeString(nil)) assert.Equal(t, "0", l1GasFeeString(big.NewInt(0))) @@ -1153,96 +1217,90 @@ func TestGetFundMigrationSigningRequest_RejectsZeroGasLimit(t *testing.T) { } // TestBroadcastFundMigrationTx_RejectsMissingAmount verifies broadcast refuses -// to assemble a tx without the signing-time amount. +// Broadcast refuses without the chain-pinned amount rather than re-deriving it. func TestBroadcastFundMigrationTx_RejectsMissingAmount(t *testing.T) { tb := newTestTxBuilder(t) - data := &common.FundMigrationData{ - From: "0x1111111111111111111111111111111111111111", - To: "0x2222222222222222222222222222222222222222", - GasPrice: big.NewInt(20_000_000_000), - GasLimit: 21000, - } - sig := make([]byte, 65) // valid length; bytes don't have to be a real ECDSA sig - - t.Run("nil amount rejected", func(t *testing.T) { - req := &common.UnsignedSigningReq{ - SigningHash: []byte{0x01}, - Nonce: 0, - // TSSFundMigrationAmount intentionally nil - } - _, err := tb.BroadcastFundMigrationTx(context.Background(), req, data, sig) - require.Error(t, err) - assert.Contains(t, err.Error(), "TSSFundMigrationAmount must be set") - }) - - t.Run("zero amount rejected", func(t *testing.T) { - req := &common.UnsignedSigningReq{ - SigningHash: []byte{0x01}, - Nonce: 0, - TSSFundMigrationAmount: big.NewInt(0), - } - _, err := tb.BroadcastFundMigrationTx(context.Background(), req, data, sig) - require.Error(t, err) - assert.Contains(t, err.Error(), "TSSFundMigrationAmount must be set") - }) + sig := make([]byte, 65) + req := &common.UnsignedSigningReq{SigningHash: []byte{0x01}, Nonce: 0} - t.Run("negative amount rejected", func(t *testing.T) { - req := &common.UnsignedSigningReq{ - SigningHash: []byte{0x01}, - Nonce: 0, - TSSFundMigrationAmount: big.NewInt(-1), - } - _, err := tb.BroadcastFundMigrationTx(context.Background(), req, data, sig) - require.Error(t, err) - assert.Contains(t, err.Error(), "TSSFundMigrationAmount must be set") - }) + for _, tc := range []struct { + name string + amount *big.Int + }{ + {"nil amount rejected", nil}, + {"zero amount rejected", big.NewInt(0)}, + {"negative amount rejected", big.NewInt(-1)}, + } { + t.Run(tc.name, func(t *testing.T) { + data := &common.FundMigrationData{ + From: "0x1111111111111111111111111111111111111111", + To: "0x2222222222222222222222222222222222222222", + GasPrice: big.NewInt(20_000_000_000), + GasLimit: 21000, + L1GasFee: big.NewInt(0), + TransferAmount: tc.amount, + } + _, err := tb.BroadcastFundMigrationTx(context.Background(), req, data, sig) + require.Error(t, err) + assert.Contains(t, err.Error(), "transfer amount is required") + }) + } } -// TestGetFundMigrationSigningRequest_UsesProvidedBalance verifies that when -// data.Balance is non-nil the builder uses it verbatim and skips the RPC -// GetBalance call. This is the determinism guarantee the coordinator's -// verification path depends on. -func TestGetFundMigrationSigningRequest_UsesProvidedBalance(t *testing.T) { - tb := newTestTxBuilder(t) - +// The builder signs the chain-pinned amount verbatim. +func TestGetFundMigrationSigningRequest_UsesPinnedAmount(t *testing.T) { gasPrice := big.NewInt(20_000_000_000) gasLimit := uint64(21000) expectedAmount := big.NewInt(1_000_000_000_000_000) gasCost := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gasLimit)) balance := new(big.Int).Add(expectedAmount, gasCost) + // Live balance is sufficient (equal to the provided balance). + tb := txBuilderWithBalance(t, new(big.Int).Set(balance)) + data := &common.FundMigrationData{ - From: "0x1111111111111111111111111111111111111111", - To: "0x2222222222222222222222222222222222222222", - GasPrice: gasPrice, - GasLimit: gasLimit, - L1GasFee: big.NewInt(0), - Balance: balance, + From: "0x1111111111111111111111111111111111111111", + To: "0x2222222222222222222222222222222222222222", + GasPrice: gasPrice, + GasLimit: gasLimit, + L1GasFee: big.NewInt(0), + TransferAmount: expectedAmount, } req, err := tb.GetFundMigrationSigningRequest(context.Background(), data, 42) require.NoError(t, err) - assert.Equal(t, 0, expectedAmount.Cmp(req.TSSFundMigrationAmount)) assert.NotEmpty(t, req.SigningHash) assert.Equal(t, uint64(42), req.Nonce) } -// TestGetFundMigrationSigningRequest_ProvidedBalanceInsufficient verifies the -// insufficient-balance check fires on caller-provided Balance below gas cost. -func TestGetFundMigrationSigningRequest_ProvidedBalanceInsufficient(t *testing.T) { - tb := newTestTxBuilder(t) - - data := &common.FundMigrationData{ - From: "0x1111111111111111111111111111111111111111", - To: "0x2222222222222222222222222222222222222222", - GasPrice: big.NewInt(20_000_000_000), - GasLimit: 21000, - L1GasFee: big.NewInt(0), - Balance: big.NewInt(1), +// A missing pinned amount must be refused, not filled from a live balance. +func TestGetFundMigrationSigningRequest_RequiresPinnedAmount(t *testing.T) { + base := func() *common.FundMigrationData { + return &common.FundMigrationData{ + From: "0x1111111111111111111111111111111111111111", + To: "0x2222222222222222222222222222222222222222", + GasPrice: big.NewInt(20_000_000_000), + GasLimit: 21000, + L1GasFee: big.NewInt(0), + } + } + for _, tc := range []struct { + name string + amount *big.Int + }{ + {"nil", nil}, + {"zero", big.NewInt(0)}, + {"negative", big.NewInt(-1)}, + } { + t.Run(tc.name, func(t *testing.T) { + tb := txBuilderWithBalance(t, new(big.Int).SetUint64(1_000_000_000_000_000_000)) + data := base() + data.TransferAmount = tc.amount + _, err := tb.GetFundMigrationSigningRequest(context.Background(), data, 0) + require.Error(t, err) + assert.Contains(t, err.Error(), "transfer amount is required") + }) } - _, err := tb.GetFundMigrationSigningRequest(context.Background(), data, 0) - require.Error(t, err) - assert.Contains(t, err.Error(), "insufficient balance") } // TestBroadcastFundMigrationTx_DoesNotQueryBalance asserts broadcast never @@ -1257,9 +1315,8 @@ func TestBroadcastFundMigrationTx_DoesNotQueryBalance(t *testing.T) { L1GasFee: big.NewInt(0), } req := &common.UnsignedSigningReq{ - SigningHash: []byte{0x01}, - Nonce: 0, - TSSFundMigrationAmount: big.NewInt(1_000_000_000_000_000), // 0.001 ETH + SigningHash: []byte{0x01}, + Nonce: 0, // 0.001 ETH } sig := make([]byte, 65) @@ -1269,6 +1326,202 @@ func TestBroadcastFundMigrationTx_DoesNotQueryBalance(t *testing.T) { assert.NotContains(t, err.Error(), "failed to get balance", "broadcast must not call GetBalance") } +// txBuilderWithBalance returns a TxBuilder whose RPC pool answers eth_getBalance +// with the given wei value (Sepolia chain id). +func txBuilderWithBalance(t *testing.T, balanceWei *big.Int) *TxBuilder { + t.Helper() + balHex := "0x" + balanceWei.Text(16) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + body := make([]byte, r.ContentLength) + r.Body.Read(body) + switch { + case strings.Contains(string(body), "eth_chainId"): + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"0xaa36a7"}`)) // 11155111 + case strings.Contains(string(body), "eth_getBalance"): + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"` + balHex + `"}`)) + default: + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":null}`)) + } + })) + t.Cleanup(server.Close) + + rc, err := NewRPCClient([]string{server.URL}, 11155111, zerolog.Nop()) + require.NoError(t, err) + t.Cleanup(func() { rc.Close() }) + + return &TxBuilder{ + rpcClient: rc, + chainID: "eip155:11155111", + chainIDInt: 11155111, + logger: zerolog.Nop(), + } +} + +// A dust transfer to the old TSS EOA between the coordinator's build and a +// follower's verify must not change the pinned-balance signing hash, and a +// pinned amount larger than the live balance must be rejected. +func TestGetFundMigrationSigningRequest_PinnedBalance(t *testing.T) { + from := "0x1111111111111111111111111111111111111111" + to := "0x2222222222222222222222222222222222222222" + gasPrice := big.NewInt(20_000_000_000) + gasLimit := uint64(21000) + amount := big.NewInt(1_000_000_000_000_000) // 0.001 ETH + fees := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gasLimit)) + pinned := new(big.Int).Add(amount, fees) // balance = amount + fees + + data := func() *common.FundMigrationData { + return &common.FundMigrationData{ + From: from, + To: to, + GasPrice: gasPrice, + GasLimit: gasLimit, + L1GasFee: big.NewInt(0), + TransferAmount: new(big.Int).Set(amount), + } + } + + t.Run("hash unchanged by +1 wei dust inflow", func(t *testing.T) { + tbExact := txBuilderWithBalance(t, new(big.Int).Set(pinned)) + reqExact, err := tbExact.GetFundMigrationSigningRequest(context.Background(), data(), 7) + require.NoError(t, err) + + tbDust := txBuilderWithBalance(t, new(big.Int).Add(pinned, big.NewInt(1))) + reqDust, err := tbDust.GetFundMigrationSigningRequest(context.Background(), data(), 7) + require.NoError(t, err) + + assert.Equal(t, reqExact.SigningHash, reqDust.SigningHash) + }) + + // Reproducible after the sweep landed and the balance is gone, which the ACK + // verify path depends on. + t.Run("hash unchanged once the balance is swept away", func(t *testing.T) { + tbFunded := txBuilderWithBalance(t, new(big.Int).Set(pinned)) + reqFunded, err := tbFunded.GetFundMigrationSigningRequest(context.Background(), data(), 7) + require.NoError(t, err) + + tbDrained := txBuilderWithBalance(t, big.NewInt(0)) + reqDrained, err := tbDrained.GetFundMigrationSigningRequest(context.Background(), data(), 7) + require.NoError(t, err) + + assert.Equal(t, reqFunded.SigningHash, reqDrained.SigningHash) + }) +} + +// End to end for the pinned sweep amount: the value the chain pinned is the +// value that gets signed, and the value that reaches the wire. It no longer +// travels with the signature, so nothing but the event determines it. +func TestFundMigration_PinnedAmountReachesTheWire(t *testing.T) { + const ( + fromAddr = "0x1111111111111111111111111111111111111111" + toAddr = "0x2222222222222222222222222222222222222222" + nonce = uint64(7) + ) + pinned := big.NewInt(1_234_567_890_000_000) + gasPrice := big.NewInt(20_000_000_000) + gasLimit := uint64(21000) + + // The event as the chain pins it at initiate time. + migration := utsstypes.FundMigrationInitiatedEventData{ + Chain: "eip155:11155111", + GasPrice: gasPrice.String(), + GasLimit: gasLimit, + L1GasFee: "0", + TransferAmount: pinned.String(), + } + + dataFromEvent := func() *common.FundMigrationData { + amount, ok := new(big.Int).SetString(migration.TransferAmount, 10) + require.True(t, ok) + gp, ok := new(big.Int).SetString(migration.GasPrice, 10) + require.True(t, ok) + l1, ok := new(big.Int).SetString(migration.L1GasFee, 10) + require.True(t, ok) + return &common.FundMigrationData{ + From: fromAddr, + To: toAddr, + GasPrice: gp, + GasLimit: migration.GasLimit, + L1GasFee: l1, + TransferAmount: amount, + } + } + + // Coordinator and a follower on a chain whose balance has since moved. + coordinator := txBuilderWithBalance(t, new(big.Int).Add(pinned, big.NewInt(1_000_000_000_000_000))) + follower := txBuilderWithBalance(t, big.NewInt(0)) + + coordReq, err := coordinator.GetFundMigrationSigningRequest(context.Background(), dataFromEvent(), nonce) + require.NoError(t, err) + followerReq, err := follower.GetFundMigrationSigningRequest(context.Background(), dataFromEvent(), nonce) + require.NoError(t, err) + require.Equal(t, coordReq.SigningHash, followerReq.SigningHash, + "validators must agree on the hash regardless of what the balance is doing") + + key, err := crypto.HexToECDSA("4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318") + require.NoError(t, err) + signature, err := crypto.Sign(coordReq.SigningHash, key) + require.NoError(t, err) + + sent, sender := txBuilderCapturingSend(t) + txHash, err := sender.BroadcastFundMigrationTx(context.Background(), coordReq, dataFromEvent(), signature) + require.NoError(t, err) + require.NotEmpty(t, txHash) + + raw := <-sent + var broadcast types.Transaction + require.NoError(t, broadcast.UnmarshalBinary(raw)) + + assert.Equal(t, pinned.String(), broadcast.Value().String(), "the wire value must be the pinned amount") + assert.Equal(t, toAddr, strings.ToLower(broadcast.To().Hex())) + assert.Equal(t, nonce, broadcast.Nonce()) + assert.Equal(t, gasLimit, broadcast.Gas()) + + // The signature covers exactly this transaction. + recovered, err := types.Sender(types.NewEIP155Signer(big.NewInt(11155111)), &broadcast) + require.NoError(t, err) + assert.Equal(t, crypto.PubkeyToAddress(key.PublicKey), recovered) +} + +// txBuilderCapturingSend returns a builder whose RPC accepts eth_sendRawTransaction +// and hands the raw bytes back on the channel. +func txBuilderCapturingSend(t *testing.T) (chan []byte, *TxBuilder) { + t.Helper() + sent := make(chan []byte, 1) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + body, _ := io.ReadAll(r.Body) + switch { + case strings.Contains(string(body), "eth_chainId"): + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"0xaa36a7"}`)) + case strings.Contains(string(body), "eth_sendRawTransaction"): + var req struct { + Params []string `json:"params"` + } + require.NoError(t, json.Unmarshal(body, &req)) + require.Len(t, req.Params, 1) + decoded, err := hexutil.Decode(req.Params[0]) + require.NoError(t, err) + sent <- decoded + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"0x` + strings.Repeat("ab", 32) + `"}`)) + default: + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":null}`)) + } + })) + t.Cleanup(server.Close) + + rc, err := NewRPCClient([]string{server.URL}, 11155111, zerolog.Nop()) + require.NoError(t, err) + t.Cleanup(func() { rc.Close() }) + + return sent, &TxBuilder{ + rpcClient: rc, + chainID: "eip155:11155111", + chainIDInt: 11155111, + logger: zerolog.Nop(), + } +} + // A PC20 inbound-revert/rescue re-mints the wrapper on EVM. The Vault detects PC20 // by the token being a factory wrapper and requires msg.value==0. UV has no // PC20-specific revert code — the generic path must produce token=wrapper, value=0. diff --git a/universalClient/externalchains/svm/client.go b/universalClient/externalchains/svm/client.go index e0afd3a01..acd5ae321 100644 --- a/universalClient/externalchains/svm/client.go +++ b/universalClient/externalchains/svm/client.go @@ -19,11 +19,12 @@ import ( // Client implements the ChainClient interface for Solana chains type Client struct { // Core configuration - logger zerolog.Logger - chainIDStr string - genesisHash string - registryConfig *uregistrytypes.ChainConfig - chainConfig *config.ChainSpecificConfig + logger zerolog.Logger + chainIDStr string + genesisHash string + registryConfig *uregistrytypes.ChainConfig + chainConfig *config.ChainSpecificConfig + allowZeroConfirmations bool // Infrastructure rpcClient *RPCClient @@ -52,6 +53,7 @@ func NewClient( chainConfig *config.ChainSpecificConfig, pushSigner *pushsigner.Signer, nodeHome string, + allowZeroConfirmations bool, logger zerolog.Logger, ) (*Client, error) { if config == nil { @@ -77,14 +79,15 @@ func NewClient( } client := &Client{ - logger: log, - chainIDStr: chainIDStr, - genesisHash: genesisHash, - registryConfig: config, - chainConfig: chainConfig, - database: database, - pushSigner: pushSigner, - nodeHome: nodeHome, + logger: log, + chainIDStr: chainIDStr, + genesisHash: genesisHash, + registryConfig: config, + chainConfig: chainConfig, + allowZeroConfirmations: allowZeroConfirmations, + database: database, + pushSigner: pushSigner, + nodeHome: nodeHome, } client.eventCleaner = common.NewEventCleaner( @@ -372,8 +375,8 @@ func (c *Client) applyDefaults() componentConfig { config := componentConfig{ eventPollingInterval: 5, // default gasPriceInterval: 30, // default - fastConfirmations: 5, // Solana fast confirmations - standardConfirmations: 12, // Solana standard confirmations + fastConfirmations: common.DefaultFastConfirmations, + standardConfirmations: common.DefaultStandardConfirmations, rentReclaimSweepInterval: rentReclaimSweepInterval, rentReclaimMinPDAAge: rentReclaimMinPDAAge, } @@ -415,6 +418,17 @@ func (c *Client) applyDefaults() componentConfig { config.standardConfirmations = uint64(c.registryConfig.BlockConfirmation.StandardInbound) } + // A registry-configured 0 disables the reorg-safety depth. Honor it only + // when instant routes are enabled; otherwise fall back to a safe default. + if !c.allowZeroConfirmations { + if config.fastConfirmations == 0 { + config.fastConfirmations = common.DefaultFastConfirmations + } + if config.standardConfirmations == 0 { + config.standardConfirmations = common.DefaultStandardConfirmations + } + } + return config } diff --git a/universalClient/externalchains/svm/client_test.go b/universalClient/externalchains/svm/client_test.go index 50f1084f7..36a5a91bf 100644 --- a/universalClient/externalchains/svm/client_test.go +++ b/universalClient/externalchains/svm/client_test.go @@ -35,7 +35,7 @@ func validChainConfig() *uregistrytypes.ChainConfig { func TestNewClient_NilConfig(t *testing.T) { logger := zerolog.New(zerolog.NewTestWriter(t)) - client, err := NewClient(nil, nil, nil, nil, "", logger) + client, err := NewClient(nil, nil, nil, nil, "", false, logger) assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "config is nil") @@ -49,7 +49,7 @@ func TestNewClient_InvalidVMType(t *testing.T) { VmType: uregistrytypes.VmType_EVM, // wrong VM type } - client, err := NewClient(cfg, nil, nil, nil, "", logger) + client, err := NewClient(cfg, nil, nil, nil, "", false, logger) assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "invalid VM type for Solana client") @@ -63,7 +63,7 @@ func TestNewClient_InvalidChainID(t *testing.T) { VmType: uregistrytypes.VmType_SVM, } - client, err := NewClient(cfg, nil, testChainConfig([]string{"https://rpc.example.com"}), nil, "", logger) + client, err := NewClient(cfg, nil, testChainConfig([]string{"https://rpc.example.com"}), nil, "", false, logger) assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "failed to parse chain ID") @@ -74,7 +74,7 @@ func TestNewClient_NoRPCURLs_NilChainConfig(t *testing.T) { cfg := validChainConfig() - client, err := NewClient(cfg, nil, nil, nil, "", logger) + client, err := NewClient(cfg, nil, nil, nil, "", false, logger) assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "no RPC URLs configured") @@ -85,7 +85,7 @@ func TestNewClient_NoRPCURLs_EmptySlice(t *testing.T) { cfg := validChainConfig() - client, err := NewClient(cfg, nil, testChainConfig([]string{}), nil, "", logger) + client, err := NewClient(cfg, nil, testChainConfig([]string{}), nil, "", false, logger) assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "no RPC URLs configured") @@ -103,7 +103,7 @@ func TestNewClient_ValidCreation(t *testing.T) { chainSpecific := testChainConfig([]string{"https://api.mainnet-beta.solana.com"}) - client, err := NewClient(cfg, nil, chainSpecific, nil, "/tmp/node", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "/tmp/node", false, logger) require.NoError(t, err) require.NotNil(t, client) @@ -122,7 +122,7 @@ func TestNewClient_WithDatabase(t *testing.T) { cfg := validChainConfig() chainSpecific := testChainConfig([]string{"https://api.mainnet-beta.solana.com"}) - client, err := NewClient(cfg, database, chainSpecific, nil, "", logger) + client, err := NewClient(cfg, database, chainSpecific, nil, "", false, logger) require.NoError(t, err) require.NotNil(t, client) assert.Equal(t, database, client.database) @@ -134,7 +134,7 @@ func TestChainID(t *testing.T) { cfg := validChainConfig() chainSpecific := testChainConfig([]string{"https://rpc.example.com"}) - client, err := NewClient(cfg, nil, chainSpecific, nil, "", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "", false, logger) require.NoError(t, err) assert.Equal(t, validSVMChainID(), client.ChainID()) @@ -150,7 +150,7 @@ func TestGetConfig(t *testing.T) { } chainSpecific := testChainConfig([]string{"https://rpc.example.com"}) - client, err := NewClient(cfg, nil, chainSpecific, nil, "", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "", false, logger) require.NoError(t, err) got := client.GetConfig() @@ -164,7 +164,7 @@ func TestGetTxBuilder_NilBeforeStart(t *testing.T) { cfg := validChainConfig() chainSpecific := testChainConfig([]string{"https://rpc.example.com"}) - client, err := NewClient(cfg, nil, chainSpecific, nil, "", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "", false, logger) require.NoError(t, err) txb, err := client.GetTxBuilder() @@ -179,7 +179,7 @@ func TestIsHealthy_NotStarted(t *testing.T) { cfg := validChainConfig() chainSpecific := testChainConfig([]string{"https://rpc.example.com"}) - client, err := NewClient(cfg, nil, chainSpecific, nil, "", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "", false, logger) require.NoError(t, err) // rpcClient is nil before Start @@ -192,7 +192,7 @@ func TestStop_BeforeStart(t *testing.T) { cfg := validChainConfig() chainSpecific := testChainConfig([]string{"https://rpc.example.com"}) - client, err := NewClient(cfg, nil, chainSpecific, nil, "", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "", false, logger) require.NoError(t, err) // Calling Stop before Start should not panic @@ -206,7 +206,7 @@ func TestStop_CalledTwice(t *testing.T) { cfg := validChainConfig() chainSpecific := testChainConfig([]string{"https://rpc.example.com"}) - client, err := NewClient(cfg, nil, chainSpecific, nil, "", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "", false, logger) require.NoError(t, err) // Double stop should be safe @@ -220,7 +220,7 @@ func TestApplyDefaults_AllDefaults(t *testing.T) { cfg := validChainConfig() chainSpecific := testChainConfig([]string{"https://rpc.example.com"}) - client, err := NewClient(cfg, nil, chainSpecific, nil, "", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "", false, logger) require.NoError(t, err) defaults := client.applyDefaults() @@ -242,7 +242,7 @@ func TestApplyDefaults_EventPollingOverride(t *testing.T) { } cfg := validChainConfig() - client, err := NewClient(cfg, nil, chainSpecific, nil, "", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "", false, logger) require.NoError(t, err) defaults := client.applyDefaults() @@ -261,7 +261,7 @@ func TestApplyDefaults_GasPriceOverride(t *testing.T) { } cfg := validChainConfig() - client, err := NewClient(cfg, nil, chainSpecific, nil, "", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "", false, logger) require.NoError(t, err) defaults := client.applyDefaults() @@ -282,7 +282,7 @@ func TestApplyDefaults_BlockConfirmationOverride(t *testing.T) { } chainSpecific := testChainConfig([]string{"https://rpc.example.com"}) - client, err := NewClient(cfg, nil, chainSpecific, nil, "", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "", false, logger) require.NoError(t, err) defaults := client.applyDefaults() @@ -305,7 +305,7 @@ func TestApplyDefaults_ZeroValueNotApplied(t *testing.T) { } cfg := validChainConfig() - client, err := NewClient(cfg, nil, chainSpecific, nil, "", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "", false, logger) require.NoError(t, err) defaults := client.applyDefaults() @@ -315,6 +315,45 @@ func TestApplyDefaults_ZeroValueNotApplied(t *testing.T) { assert.Equal(t, 0, defaults.gasPriceMarkupPercent) // 0 is the default too } +// A registry-configured 0 falls back to a safe depth unless instant routes are +// enabled, in which case it is honored. +func TestApplyDefaults_ZeroConfirmations(t *testing.T) { + logger := zerolog.New(zerolog.NewTestWriter(t)) + + zeroRegistry := &uregistrytypes.ChainConfig{ + BlockConfirmation: &uregistrytypes.BlockConfirmation{ + FastInbound: 0, + StandardInbound: 0, + }, + } + + t.Run("mainnet falls back to safe depth", func(t *testing.T) { + client := &Client{ + logger: logger, + chainIDStr: "solana:mainnet", + registryConfig: zeroRegistry, + allowZeroConfirmations: false, + } + + defaults := client.applyDefaults() + assert.Equal(t, uint64(5), defaults.fastConfirmations, "zero fast must not disable depth on mainnet") + assert.Equal(t, uint64(12), defaults.standardConfirmations, "zero standard must not disable depth on mainnet") + }) + + t.Run("testnet honors zero as instant", func(t *testing.T) { + client := &Client{ + logger: logger, + chainIDStr: "solana:mainnet", + registryConfig: zeroRegistry, + allowZeroConfirmations: true, + } + + defaults := client.applyDefaults() + assert.Equal(t, uint64(0), defaults.fastConfirmations, "testnet instant route keeps zero") + assert.Equal(t, uint64(0), defaults.standardConfirmations, "testnet instant route keeps zero") + }) +} + func TestParseSolanaChainID(t *testing.T) { tests := []struct { name string @@ -404,7 +443,7 @@ func TestNewClient_FullConfigGetters(t *testing.T) { GasPriceMarkupPercent: &gasMarkup, } - client, err := NewClient(cfg, nil, chainSpecific, nil, "/tmp/home", logger) + client, err := NewClient(cfg, nil, chainSpecific, nil, "/tmp/home", false, logger) require.NoError(t, err) // Verify all getters diff --git a/universalClient/externalchains/svm/event_confirmer.go b/universalClient/externalchains/svm/event_confirmer.go index 24d7f00e9..6f3d1e656 100644 --- a/universalClient/externalchains/svm/event_confirmer.go +++ b/universalClient/externalchains/svm/event_confirmer.go @@ -180,7 +180,16 @@ func (ec *EventConfirmer) processPendingEvents(ctx context.Context) error { // Check if transaction is confirmed based on confirmation type requiredConfirmations := ec.getRequiredConfirmations(event.ConfirmationType) - confirmations := latestSlot - txSlot + 1 + confirmations, ok := chaincommon.ConfirmationDepth(latestSlot, txSlot) + if !ok { + // RPC height skew: latest slot is behind the tx slot. Defer. + ec.logger.Debug(). + Str("event_id", event.EventID). + Uint64("latest_slot", latestSlot). + Uint64("tx_slot", txSlot). + Msg("latest slot behind tx slot (RPC height skew); deferring confirmation") + continue + } if confirmations >= requiredConfirmations { // GasFeeUsed for outbound events is already set by the event parser from the on-chain event data @@ -225,24 +234,13 @@ func (ec *EventConfirmer) getTxSignatureFromEventID(eventID string) string { return parts[0] } -// getRequiredConfirmations returns the required number of confirmations based on confirmation type +// getRequiredConfirmations returns the depth for a confirmation type. Values are +// resolved by applyDefaults, so a 0 here is an intentional instant route. func (ec *EventConfirmer) getRequiredConfirmations(confirmationType string) uint64 { switch confirmationType { case store.ConfirmationFast: - if ec.fastConfirmations > 0 { - return ec.fastConfirmations - } - return 5 - case store.ConfirmationStandard: - if ec.standardConfirmations > 0 { - return ec.standardConfirmations - } - return 12 + return ec.fastConfirmations default: - // Default to standard if unknown - if ec.standardConfirmations > 0 { - return ec.standardConfirmations - } - return 12 + return ec.standardConfirmations } } diff --git a/universalClient/externalchains/svm/event_confirmer_test.go b/universalClient/externalchains/svm/event_confirmer_test.go index a1027bffc..0c48afc72 100644 --- a/universalClient/externalchains/svm/event_confirmer_test.go +++ b/universalClient/externalchains/svm/event_confirmer_test.go @@ -129,10 +129,11 @@ func TestEventConfirmerGetRequiredConfirmations(t *testing.T) { assert.Equal(t, uint64(5), confirmations) }) - t.Run("FAST confirmation type with zero uses default", func(t *testing.T) { + t.Run("FAST confirmation type with zero honored as instant", func(t *testing.T) { + // Fallback policy lives in applyDefaults; the confirmer honors a resolved 0. confirmer := NewEventConfirmer(nil, nil, "solana:mainnet", 5, 0, 12, logger) confirmations := confirmer.getRequiredConfirmations(store.ConfirmationFast) - assert.Equal(t, uint64(5), confirmations) // Default is 5 + assert.Equal(t, uint64(0), confirmations) }) t.Run("STANDARD confirmation type with custom value", func(t *testing.T) { @@ -141,10 +142,10 @@ func TestEventConfirmerGetRequiredConfirmations(t *testing.T) { assert.Equal(t, uint64(20), confirmations) }) - t.Run("STANDARD confirmation type with zero uses default", func(t *testing.T) { + t.Run("STANDARD confirmation type with zero honored as instant", func(t *testing.T) { confirmer := NewEventConfirmer(nil, nil, "solana:mainnet", 5, 5, 0, logger) confirmations := confirmer.getRequiredConfirmations(store.ConfirmationStandard) - assert.Equal(t, uint64(12), confirmations) // Default is 12 + assert.Equal(t, uint64(0), confirmations) }) t.Run("unknown type defaults to standard configured", func(t *testing.T) { @@ -153,10 +154,10 @@ func TestEventConfirmerGetRequiredConfirmations(t *testing.T) { assert.Equal(t, uint64(25), confirmations) }) - t.Run("unknown type with zero falls back to default 12", func(t *testing.T) { + t.Run("unknown type with zero standard honored as instant", func(t *testing.T) { confirmer := NewEventConfirmer(nil, nil, "solana:mainnet", 5, 0, 0, logger) confirmations := confirmer.getRequiredConfirmations("UNKNOWN") - assert.Equal(t, uint64(12), confirmations) + assert.Equal(t, uint64(0), confirmations) }) t.Run("empty type defaults to standard", func(t *testing.T) { @@ -326,16 +327,16 @@ func TestEventConfirmerGetRequiredConfirmations_MoreEdgeCases(t *testing.T) { assert.Equal(t, uint64(10), unknown) }) - t.Run("zero fast falls back to default 5", func(t *testing.T) { + t.Run("zero fast honored as instant", func(t *testing.T) { ec := NewEventConfirmer(nil, nil, "solana:mainnet", 5, 0, 20, logger) result := ec.getRequiredConfirmations(store.ConfirmationFast) - assert.Equal(t, uint64(5), result) // default 5 + assert.Equal(t, uint64(0), result) }) - t.Run("zero standard falls back to default 12", func(t *testing.T) { + t.Run("zero standard honored as instant", func(t *testing.T) { ec := NewEventConfirmer(nil, nil, "solana:mainnet", 5, 10, 0, logger) result := ec.getRequiredConfirmations(store.ConfirmationStandard) - assert.Equal(t, uint64(12), result) // default 12 + assert.Equal(t, uint64(0), result) }) } @@ -441,3 +442,74 @@ func TestEventConfirmer_StartStop_ZeroPollInterval(t *testing.T) { t.Fatal("event confirmer did not stop after context cancellation with zero poll interval") } } + +// The skew this finding reports, end to end: the endpoint serving the +// transaction is ahead of the one serving the slot, so the tx slot is above the +// latest slot. Unchecked, latest-tx underflows to near 2^64 and clears any +// threshold. The event must stay PENDING and be retried, never confirmed. +func TestProcessPendingEvents_RPCHeightSkew_StaysPending(t *testing.T) { + sigStr := strings.Repeat("1", 64) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + body := make([]byte, r.ContentLength) + r.Body.Read(body) + bodyStr := string(body) + + switch { + case strings.Contains(bodyStr, `"getHealth"`): + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"ok"}`)) + case strings.Contains(bodyStr, `"getSlot"`): + // The tip endpoint lags well behind the tx endpoint. + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":100}`)) + case strings.Contains(bodyStr, `"getTransaction"`): + // Successful tx, but at a slot the observed tip has not reached. + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{` + + `"slot":1000,` + + `"meta":{` + + `"err":null,` + + `"fee":5000,` + + `"preBalances":[],` + + `"postBalances":[],` + + `"logMessages":[],` + + `"status":{"Ok":null}` + + `},` + + `"transaction":["AQ==","base64"]` + + `}}`)) + default: + w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":null}`)) + } + })) + defer server.Close() + + logger := zerolog.Nop() + rpcClient, err := NewRPCClient([]string{server.URL}, "", logger) + require.NoError(t, err) + defer rpcClient.Close() + + memDB, err := db.OpenInMemoryDB(true) + require.NoError(t, err) + defer memDB.Close() + + ec := NewEventConfirmer(rpcClient, memDB, "solana:mainnet", 5, 5, 12, logger) + cs := common.NewChainStore(memDB) + + pending := &store.Event{ + EventID: sigStr + ":0", + BlockHeight: 1000, + Type: store.EventTypeInbound, + ConfirmationType: store.ConfirmationStandard, + Status: store.StatusPending, + EventData: []byte(`{}`), + } + inserted, err := cs.InsertEventIfNotExists(pending) + require.NoError(t, err) + require.True(t, inserted) + + require.NoError(t, ec.processPendingEvents(context.Background())) + + var got store.Event + require.NoError(t, memDB.Client().Where("event_id = ?", pending.EventID).First(&got).Error) + assert.Equal(t, store.StatusPending, got.Status, + "a tx slot above the observed tip must defer, not confirm") +} diff --git a/universalClient/externalchains/svm/event_listener.go b/universalClient/externalchains/svm/event_listener.go index d95ff46c2..8456e73dd 100644 --- a/universalClient/externalchains/svm/event_listener.go +++ b/universalClient/externalchains/svm/event_listener.go @@ -1,6 +1,7 @@ package svm import ( + "bytes" "context" "encoding/base64" "encoding/hex" @@ -295,35 +296,42 @@ func (el *EventListener) processSignatureBatch( continue } - // Process each log in the transaction - if tx != nil && tx.Meta != nil && len(tx.Meta.LogMessages) > 0 { - for logIndex, log := range tx.Meta.LogMessages { - // Determine event type based on discriminator - eventType := el.determineEventType(log) - if eventType == "" { - continue - } - - // Parse gateway event from individual log - event := ParseEvent(log, sig.Signature.String(), sig.Slot, uint(logIndex), eventType, el.chainID, el.logger) - if event != nil { - // Insert event if it doesn't already exist - if stored, err := el.chainStore.InsertEventIfNotExists(event); err != nil { - el.logger.Error(). - Err(err). - Str("event_id", event.EventID). - Str("type", event.Type). - Uint64("slot", event.BlockHeight). - Msg("failed to store event") - } else if stored { - el.logger.Debug(). - Str("event_id", event.EventID). - Str("type", event.Type). - Uint64("slot", event.BlockHeight). - Str("confirmation_type", event.ConfirmationType). - Msg("stored new event") - } - } + // Events come from emit_cpi inner instructions, not logs, so log + // truncation cannot drop one. + // A failed tx still records what ran before it aborted, and all of it was + // rolled back. + if tx != nil && tx.Meta != nil && tx.Meta.Err != nil { + el.logger.Debug(). + Str("signature", sig.Signature.String()). + Msg("skipping failed transaction") + continue + } + + for payloadIndex, payload := range gatewayEventPayloads(tx, el.gatewayAddress) { + eventType := el.determineEventType(payload) + if eventType == "" { + continue + } + + event := ParseEvent(payload, sig.Signature.String(), sig.Slot, uint(payloadIndex), eventType, el.chainID, el.logger) + if event == nil { + continue + } + + if stored, err := el.chainStore.InsertEventIfNotExists(event); err != nil { + el.logger.Error(). + Err(err). + Str("event_id", event.EventID). + Str("type", event.Type). + Uint64("slot", event.BlockHeight). + Msg("failed to store event") + } else if stored { + el.logger.Debug(). + Str("event_id", event.EventID). + Str("type", event.Type). + Uint64("slot", event.BlockHeight). + Str("confirmation_type", event.ConfirmationType). + Msg("stored new event") } } } @@ -395,7 +403,48 @@ func (el *EventListener) getPollingInterval() time.Duration { return 5 * time.Second // default } -// determineEventType determines the event type based on the log discriminator +// eventIxTag prefixes the data of every Anchor emit_cpi instruction. +var eventIxTag = []byte{0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d} + +// gatewayEventPayloads returns the gateway's emit_cpi events in the +// "Program data:" form the parsers take. Event data is eventIxTag || +// discriminator || borsh, so dropping the tag leaves the old payload. +// +// Only instructions the gateway itself ran are read. A discriminator is a +// schema tag, not proof of who emitted it. +func gatewayEventPayloads(tx *solanarpc.GetTransactionResult, gatewayAddress string) []string { + if tx == nil || tx.Meta == nil || len(tx.Meta.InnerInstructions) == 0 { + return nil + } + gateway, err := solana.PublicKeyFromBase58(gatewayAddress) + if err != nil { + return nil + } + parsed, txErr := tx.Transaction.GetTransaction() + if txErr != nil || parsed == nil { + return nil + } + + // Index order: static keys, then ALT writable, then ALT readonly. + keys := append(solana.PublicKeySlice{}, parsed.Message.AccountKeys...) + keys = append(keys, tx.Meta.LoadedAddresses.Writable...) + keys = append(keys, tx.Meta.LoadedAddresses.ReadOnly...) + + var payloads []string + for _, group := range tx.Meta.InnerInstructions { + for _, ix := range group.Instructions { + if int(ix.ProgramIDIndex) >= len(keys) || !keys[ix.ProgramIDIndex].Equals(gateway) { + continue + } + if len(ix.Data) < len(eventIxTag) || !bytes.Equal(ix.Data[:len(eventIxTag)], eventIxTag) { + continue + } + payloads = append(payloads, "Program data: "+base64.StdEncoding.EncodeToString(ix.Data[len(eventIxTag):])) + } + } + return payloads +} + func (el *EventListener) determineEventType(log string) string { if !strings.HasPrefix(log, "Program data: ") { return "" diff --git a/universalClient/externalchains/svm/event_listener_test.go b/universalClient/externalchains/svm/event_listener_test.go index 62065a9b2..e4abba1e2 100644 --- a/universalClient/externalchains/svm/event_listener_test.go +++ b/universalClient/externalchains/svm/event_listener_test.go @@ -5,17 +5,22 @@ import ( "context" "encoding/base64" "encoding/hex" + "encoding/json" + "fmt" "strings" "testing" "time" "github.com/gagliardetto/solana-go" solanarpc "github.com/gagliardetto/solana-go/rpc" + "github.com/mr-tron/base58" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pushchain/push-chain-node/universalClient/db" + "github.com/pushchain/push-chain-node/universalClient/externalchains/common" + "github.com/pushchain/push-chain-node/universalClient/store" uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types" ) @@ -676,3 +681,324 @@ func TestEventListener_StartWhileRunning(t *testing.T) { cancel() el.wg.Wait() } + +const ( + testGatewayProgram = "CFVSincHYbETh2k7w6u1ENEkjbSLtveRCEBupKidw2VS" + testAttackerProgram = "AttackerProgram1111111111111111111111111111" +) + +type forgeryRPC struct { + slot uint64 + sig solana.Signature + txJSON string +} + +func (m *forgeryRPC) GetLatestSlot(context.Context) (uint64, error) { return m.slot, nil } + +func (m *forgeryRPC) GetSignaturesForAddress(context.Context, solana.PublicKey, solana.Signature) ([]*solanarpc.TransactionSignature, error) { + return []*solanarpc.TransactionSignature{{Signature: m.sig, Slot: m.slot}}, nil +} + +func (m *forgeryRPC) GetTransaction(context.Context, solana.Signature) (*solanarpc.GetTransactionResult, error) { + var tx solanarpc.GetTransactionResult + if err := json.Unmarshal([]byte(m.txJSON), &tx); err != nil { + return nil, err + } + return &tx, nil +} + +// txWithEmittedEvent builds a tx whose only inner instruction is an emit_cpi +// event from `emitter`. +func txWithEmittedEvent(t *testing.T, emitter string, payload []byte, logs []string) string { + t.Helper() + data := append(append([]byte{}, eventIxTag...), payload...) + logJSON, err := json.Marshal(logs) + require.NoError(t, err) + return fmt.Sprintf(`{ + "slot": 100, + "transaction": { + "signatures": ["%s"], + "message": { + "header": {"numRequiredSignatures":1,"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":1}, + "accountKeys": ["%s","%s"], + "recentBlockhash": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", + "instructions": [] + } + }, + "meta": { + "err": null, + "logMessages": %s, + "innerInstructions": [ + {"index":0,"instructions":[{"programIdIndex":1,"accounts":[],"data":"%s","stackHeight":2}]} + ] + } + }`, mkSig(7).String(), testGatewayProgram, emitter, string(logJSON), base58.Encode(data)) +} + +// End-to-end proof that the listener drops a forged event. The same valid +// send_funds payload is served twice: emitted by an attacker program it must be +// ignored, emitted by the gateway it must be stored. +func TestProcessSignatureBatch_RejectsForgedGatewayEvent(t *testing.T) { + discriminator := "0000000000000000" // buildSendFundsPayload zeroes the discriminator + payload := buildSendFundsPayload( + [32]byte{1}, [20]byte{2}, [32]byte{3}, 1_000_000, + nil, [32]byte{4}, 0, nil, false, + ) + + run := func(t *testing.T, emitter string) int { + t.Helper() + database, err := db.OpenInMemoryDB(true) + require.NoError(t, err) + t.Cleanup(func() { database.Close() }) + + methods := []*uregistrytypes.GatewayMethods{ + {Name: EventTypeSendFunds, EventIdentifier: discriminator}, + } + rpc := &forgeryRPC{slot: 100, sig: mkSig(7), txJSON: txWithEmittedEvent(t, emitter, payload, nil)} + el, err := NewEventListener(rpc, testGatewayProgram, "solana:test", methods, database, 10, nil, zerolog.Nop()) + require.NoError(t, err) + + _, err = el.processSignatureBatch(context.Background(), []*solanarpc.TransactionSignature{ + {Signature: mkSig(7), Slot: 100}, + }, 0, 200) + require.NoError(t, err) + + events, err := common.NewChainStore(database).GetPendingEvents(100) + require.NoError(t, err) + return len(events) + } + + t.Run("forged by attacker program is not stored", func(t *testing.T) { + assert.Zero(t, run(t, testAttackerProgram), "forged gateway event must not become an inbound") + }) + + t.Run("same payload from the gateway is stored", func(t *testing.T) { + assert.Equal(t, 1, run(t, testGatewayProgram), "genuine gateway event must be observed") + }) +} + +// A failed tx still lists what ran before it aborted, and all of it was +// rolled back. +func TestProcessSignatureBatch_SkipsFailedTransactions(t *testing.T) { + payload := buildSendFundsPayload( + [32]byte{1}, [20]byte{2}, [32]byte{3}, 1_000_000, + nil, [32]byte{4}, 0, nil, false, + ) + methods := []*uregistrytypes.GatewayMethods{ + {Name: EventTypeSendFunds, EventIdentifier: "0000000000000000"}, + } + + run := func(t *testing.T, txJSON string) int { + t.Helper() + database, err := db.OpenInMemoryDB(true) + require.NoError(t, err) + t.Cleanup(func() { database.Close() }) + + rpc := &forgeryRPC{slot: 100, sig: mkSig(7), txJSON: txJSON} + el, err := NewEventListener(rpc, testGatewayProgram, "solana:test", methods, database, 10, nil, zerolog.Nop()) + require.NoError(t, err) + + _, err = el.processSignatureBatch(context.Background(), []*solanarpc.TransactionSignature{ + {Signature: mkSig(7), Slot: 100}, + }, 0, 200) + require.NoError(t, err) + + events, err := common.NewChainStore(database).GetPendingEvents(100) + require.NoError(t, err) + return len(events) + } + + ok := txWithEmittedEvent(t, testGatewayProgram, payload, nil) + + t.Run("succeeded transaction is stored", func(t *testing.T) { + assert.Equal(t, 1, run(t, ok)) + }) + + t.Run("failed transaction is skipped", func(t *testing.T) { + failed := strings.Replace(ok, `"err": null`, `"err": {"InstructionError":[0,{"Custom":6020}]}`, 1) + require.NotEqual(t, ok, failed, "the fixture must actually carry a failure") + assert.Zero(t, run(t, failed), + "an event emitted before the abort must not be observed") + }) +} + +// The observation half of F-2026-18817: the event is an inner instruction, so +// a flooded log buffer cannot take it down. +func TestProcessSignatureBatch_TruncatedLogsStillYieldEvent(t *testing.T) { + database, err := db.OpenInMemoryDB(true) + require.NoError(t, err) + t.Cleanup(func() { database.Close() }) + + payload := buildSendFundsPayload( + [32]byte{1}, [20]byte{2}, [32]byte{3}, 1_000_000, + nil, [32]byte{4}, 0, nil, false, + ) + truncated := []string{ + "Program " + testGatewayProgram + " invoke [1]", + "Program log: truncated", + } + + methods := []*uregistrytypes.GatewayMethods{ + {Name: EventTypeSendFunds, EventIdentifier: "0000000000000000"}, + } + rpc := &forgeryRPC{slot: 100, sig: mkSig(7), txJSON: txWithEmittedEvent(t, testGatewayProgram, payload, truncated)} + el, err := NewEventListener(rpc, testGatewayProgram, "solana:test", methods, database, 10, nil, zerolog.Nop()) + require.NoError(t, err) + + _, err = el.processSignatureBatch(context.Background(), []*solanarpc.TransactionSignature{ + {Signature: mkSig(7), Slot: 100}, + }, 0, 200) + require.NoError(t, err) + + events, err := common.NewChainStore(database).GetPendingEvents(100) + require.NoError(t, err) + assert.Len(t, events, 1, "a truncated log buffer must not cost us the event") +} + +// A real devnet finalize, signature 4ye6nTo4oKcEctDza44Zr7QAwHmqhH4qfeBkDjHqE2aFtgxuhdF9dfs1EmBbYiTfwXMvWUupJ592DQQQAGx5Abus. +// It carries no "Program data:" line at all. +const devnetFinalizeTx = `{"blockTime":1788253699,"meta":{"computeUnitsConsumed":132988,"costUnits":136994,"err":null,"fee":5000,"innerInstructions":[{"index":0,"instructions":[{"accounts":[0,5],"data":"11114pZy3PBZenKB1vn2UptrP91MCBmdVNUDneZKAWH7B8Sg5eCB3E67uKRhm1xbvr4ACz","programIdIndex":10,"stackHeight":2},{"accounts":[0,9],"data":"1111NuBxPLg6vZ28hQWMLnv7auFnJFYGTC4L1MUjrNkN9xikCBvdVStP4KiJr9vvBcWPa","programIdIndex":10,"stackHeight":2},{"accounts":[9,15],"data":"18ukwGkxoTJPx4HkLTz6ZybMUmX1yt47MVERA5H6yQGUdgK","programIdIndex":16,"stackHeight":2},{"accounts":[0,3],"data":"11113ahNe3Yfn6gi8hZhcH9k4YUezY3FJNRHx6tPLUTkr868BMzwWAZDTUtWcrGK2cw1Fx","programIdIndex":10,"stackHeight":2},{"accounts":[0,4,7,9,10,16],"data":"1","programIdIndex":13,"stackHeight":2},{"accounts":[9],"data":"84eT","programIdIndex":16,"stackHeight":3},{"accounts":[0,4],"data":"11113z11NKiYBjwDfL71F9myuy3cwdtTaatpiC6rj9zomYP4qbzHXZVjhEFkM5qi31QeQg","programIdIndex":10,"stackHeight":3},{"accounts":[4],"data":"P","programIdIndex":16,"stackHeight":3},{"accounts":[4,9],"data":"6VKrKvV2EjfdgaesMejJQDnusTVKHbdt2BPiddZq2WzGf","programIdIndex":16,"stackHeight":3},{"accounts":[9,4,9],"data":"6YF7VVXZihvw","programIdIndex":16,"stackHeight":2},{"accounts":[2,0],"data":"3Bxs4R98mv6mmz5M","programIdIndex":10,"stackHeight":2},{"accounts":[2,0],"data":"3Bxs4PckVVt51W8w","programIdIndex":10,"stackHeight":2},{"accounts":[11],"data":"9opCxkAgBxqeR8UTbeow8YC7933mDbBMCEMKiYsmMRqLs1N8zRudTsxXqkeWaXpNdpt2QZhCyZprcPNHfS7EwMkBnfrzuFhd3zMg8ZLA6Uk1BVxrx5nq9s2EYueLPKVCWCvzyKsZqph76dduPdkHBQs7TdFtP5FtvJssMKvwPu5zShUSegxsJLaYKZCjZAcrYscVbEmLzrvehE2s4qYdMGyW58rkamNjPC4BgKoZpPt136NYv31ftYXB4sVrTdjkJogbp9HpWA1BnewRmXuWddeYyGxyiFG3X44mG5ALa7rTuPgcZukdFmahFVfE2Txj","programIdIndex":14,"stackHeight":2}]}],"loadedAddresses":{"readonly":[],"writable":[]},"logMessages":["Program DJoFYDpgbTfxbXBv1QYhYGc9FK4J5FUKpYXAfSkHryXp invoke [1]","Program log: Instruction: FinalizeUniversalTxWithIxDataRef","Program 11111111111111111111111111111111 invoke [2]","Program 11111111111111111111111111111111 success","Program 11111111111111111111111111111111 invoke [2]","Program 11111111111111111111111111111111 success","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 86 of 148619 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program 11111111111111111111111111111111 invoke [2]","Program 11111111111111111111111111111111 success","Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [2]","Program log: Create","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 179 of 93967 compute units","Program return: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA pQAAAAAAAAA=","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program 11111111111111111111111111111111 invoke [3]","Program 11111111111111111111111111111111 success","Program log: Initialize the associated token account","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 37 of 88878 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 233 of 86415 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 15010 of 100888 compute units","Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 122 of 82575 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program 11111111111111111111111111111111 invoke [2]","Program 11111111111111111111111111111111 success","Program 11111111111111111111111111111111 invoke [2]","Program 11111111111111111111111111111111 success","Program DJoFYDpgbTfxbXBv1QYhYGc9FK4J5FUKpYXAfSkHryXp invoke [2]","Program DJoFYDpgbTfxbXBv1QYhYGc9FK4J5FUKpYXAfSkHryXp consumed 2517 of 71342 compute units","Program DJoFYDpgbTfxbXBv1QYhYGc9FK4J5FUKpYXAfSkHryXp success","Program DJoFYDpgbTfxbXBv1QYhYGc9FK4J5FUKpYXAfSkHryXp consumed 132988 of 200000 compute units","Program DJoFYDpgbTfxbXBv1QYhYGc9FK4J5FUKpYXAfSkHryXp success"],"postBalances":[5010154600,2793266460,17563563083,1203270,1855569,861288,0,0,1566000,1329930,1,0,2832720,5938070540,1141440,1009200,15367267856],"postTokenBalances":[{"accountIndex":4,"mint":"ZTgXiGpKZjEopH1mSqZ1GjY8k9G6dQaJoCm8iUkab7V","owner":"9C9ezHVSSpMrKAmqZa74jpUUxbjUBtcKUUYn8z9DDqqh","programId":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","uiTokenAmount":{"amount":"10000000","decimals":6,"uiAmount":10.0,"uiAmountString":"10"}}],"preBalances":[5006672783,2793266460,17568823140,0,0,0,3476817,0,1566000,0,1,0,2832720,5938070540,1141440,1009200,15367267856],"preTokenBalances":[],"rewards":[],"status":{"Ok":null}},"slot":491383584,"transaction":{"message":{"accountKeys":["4QbAt2CJ8QqCHeps2RmMjG1nWUCmPqjkEyYQMjrJaVtH","2EEYH6e1PtCdWzZaag9buJmDDS79gvrm1aQm9yEcgWdR","4sQLizYQ1ZJjc2doLqTQsk1Kj8XVS5uviJykpHNNMSi5","4VC5j3WgPU7TTF8YzgikNdpF8zwnGkqAjf9iWfA5xCi7","59oiUm1Anavheg38XWDDdv3GAjD4XYSUhQBY8qyxXnTc","5BT6kxRRU2jSVbvVdeEBAszUoCXTzUpHWeruS5kYkqz","5PZEHeEx9mhMNPneusoQvLunGDLgHAQcyGmnoT1jJCEk","9C9ezHVSSpMrKAmqZa74jpUUxbjUBtcKUUYn8z9DDqqh","FDxeNn8YT8DoWrJ5GzqNTW8rjx8cLFNFBgHpBTMifeYJ","ZTgXiGpKZjEopH1mSqZ1GjY8k9G6dQaJoCm8iUkab7V","11111111111111111111111111111111","5FRwYKUHLYoSq6uNgrjZ2sq436AAzPnv77fv9e7EiFPi","7QAS73zgRm7KMt85XMGWbWDnmhZR1UyTULhhxR254YYR","ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL","DJoFYDpgbTfxbXBv1QYhYGc9FK4J5FUKpYXAfSkHryXp","SysvarRent111111111111111111111111111111111","TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],"header":{"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":7,"numRequiredSignatures":1},"instructions":[{"accounts":[0,12,2,7,8,5,10,10,1,14,4,14,16,15,13,14,14,14,6,0,11,14,3,9],"data":"42AXCSarXAyth9mmjqkpxW8qtkNjcbj5KuupCERvjRCapw5ydhidmiLmMnypfiZFedPUhVNQyeLPfF17ZtqeBJUS83v6DJRgwFHuVMK1dDy4MFW7QhMwVxfJwuZ1hgv9TtXcmdmixaHukCq77ikrN37w3UR2P12aY9ERa5tXuQGkxXAenYcsNHQuw7y99mXT2icCjvcVd3yGRtWgvnbrd4Gf36pzqRpZkrNgKdJzvSDYgoarEXdPq6m3GjZxRkvsgQJ2sZTsKbNCeRrL5tdxTRgd7YZXqXVDJ4ShaYAKqLXBqMGf1LbynGK2G8HUriKn41xVEFK2Rg37E3dykeHSDS","programIdIndex":14,"stackHeight":1}],"recentBlockhash":"2CEDFZcp6d5ug1BgDjuNzMPRXqi4WX8QTZskNr9yJicY"},"signatures":["4ye6nTo4oKcEctDza44Zr7QAwHmqhH4qfeBkDjHqE2aFtgxuhdF9dfs1EmBbYiTfwXMvWUupJ592DQQQAGx5Abus"]},"transactionIndex":12,"version":"legacy"}` + +func TestGatewayEventPayloads_Rejects(t *testing.T) { + var tx solanarpc.GetTransactionResult + require.NoError(t, json.Unmarshal([]byte(devnetFinalizeTx), &tx)) + + t.Run("unparseable gateway address", func(t *testing.T) { + assert.Nil(t, gatewayEventPayloads(&tx, "not-a-pubkey")) + }) + + t.Run("nil transaction and nil meta", func(t *testing.T) { + assert.Nil(t, gatewayEventPayloads(nil, testGatewayProgram)) + assert.Nil(t, gatewayEventPayloads(&solanarpc.GetTransactionResult{}, testGatewayProgram)) + }) + + t.Run("inner instruction that is not an event", func(t *testing.T) { + // Right emitter, no EVENT_IX_TAG: every ordinary gateway self-CPI. + var plain solanarpc.GetTransactionResult + require.NoError(t, json.Unmarshal([]byte(txWithRawInnerData(t, testGatewayProgram, []byte{1, 2, 3})), &plain)) + assert.Empty(t, gatewayEventPayloads(&plain, testGatewayProgram)) + }) +} + +// Index order is static keys, then ALT writable, then ALT readonly. The +// gateway comes through the ALT, so a wrong order misattributes events. +func TestGatewayEventPayloads_ResolvesLookupTableKeys(t *testing.T) { + payload := buildSendFundsPayload( + [32]byte{1}, [20]byte{2}, [32]byte{3}, 1_000_000, + nil, [32]byte{4}, 0, nil, false, + ) + data := append(append([]byte{}, eventIxTag...), payload...) + other := "11111111111111111111111111111111" + + // index 0 = static, 1 = ALT writable, 2 = ALT readonly (the gateway). + mk := func(programIdIndex int) string { + return fmt.Sprintf(`{ + "slot": 100, + "transaction": { + "signatures": ["%s"], + "message": { + "header": {"numRequiredSignatures":1,"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":0}, + "accountKeys": ["%s"], + "recentBlockhash": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", + "instructions": [] + } + }, + "meta": { + "err": null, + "logMessages": [], + "loadedAddresses": {"writable": ["%s"], "readonly": ["%s"]}, + "innerInstructions": [ + {"index":0,"instructions":[{"programIdIndex":%d,"accounts":[],"data":"%s","stackHeight":2}]} + ] + } + }`, mkSig(7).String(), other, testAttackerProgram, testGatewayProgram, programIdIndex, base58.Encode(data)) + } + + t.Run("gateway resolved from the readonly segment", func(t *testing.T) { + var tx solanarpc.GetTransactionResult + require.NoError(t, json.Unmarshal([]byte(mk(2)), &tx)) + assert.Len(t, gatewayEventPayloads(&tx, testGatewayProgram), 1) + }) + + t.Run("writable segment is not mistaken for the gateway", func(t *testing.T) { + var tx solanarpc.GetTransactionResult + require.NoError(t, json.Unmarshal([]byte(mk(1)), &tx)) + assert.Empty(t, gatewayEventPayloads(&tx, testGatewayProgram), + "index 1 is the ALT writable entry, not the gateway") + }) + + t.Run("an index past the key list is ignored", func(t *testing.T) { + var tx solanarpc.GetTransactionResult + require.NoError(t, json.Unmarshal([]byte(mk(99)), &tx)) + assert.Empty(t, gatewayEventPayloads(&tx, testGatewayProgram)) + }) +} + +// txWithRawInnerData builds a tx whose inner instruction data has no tag. +func txWithRawInnerData(t *testing.T, emitter string, data []byte) string { + t.Helper() + return fmt.Sprintf(`{ + "slot": 100, + "transaction": { + "signatures": ["%s"], + "message": { + "header": {"numRequiredSignatures":1,"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":1}, + "accountKeys": ["%s","%s"], + "recentBlockhash": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", + "instructions": [] + } + }, + "meta": { + "err": null, + "logMessages": [], + "innerInstructions": [ + {"index":0,"instructions":[{"programIdIndex":1,"accounts":[],"data":"%s","stackHeight":2}]} + ] + } + }`, mkSig(7).String(), testGatewayProgram, emitter, base58.Encode(data)) +} + +// Pinned to a real tx: the layout is the deployed program's, not ours. Here +// gas_fee - gas_used == gas_to_refund, so the offsets check themselves. +func TestGatewayEventPayloads_RealDevnetTransaction(t *testing.T) { + const gateway = "DJoFYDpgbTfxbXBv1QYhYGc9FK4J5FUKpYXAfSkHryXp" + + var tx solanarpc.GetTransactionResult + require.NoError(t, json.Unmarshal([]byte(devnetFinalizeTx), &tx)) + + require.NotNil(t, tx.Meta) + for _, l := range tx.Meta.LogMessages { + require.False(t, strings.HasPrefix(l, "Program data: "), + "this transaction predates emit_cpi if it still logs event data") + } + + payloads := gatewayEventPayloads(&tx, gateway) + require.Len(t, payloads, 1, "the finalize emits exactly one gateway event") + + el := &EventListener{ + gatewayAddress: gateway, + // sha256("event:UniversalTxFinalized")[:8], as emitted on chain. + discriminatorToEventType: map[string]string{"b3409670758c9c25": EventTypeFinalizeUniversalTx}, + chainID: "solana:devnet", + logger: zerolog.Nop(), + } + eventType := el.determineEventType(payloads[0]) + require.Equal(t, EventTypeFinalizeUniversalTx, eventType) + + event := ParseEvent(payloads[0], "sig", 42, 0, eventType, "solana:devnet", zerolog.Nop()) + require.NotNil(t, event) + + var payload common.OutboundObservation + require.NoError(t, json.Unmarshal(event.EventData, &payload)) + assert.Equal(t, "5260057", payload.GasFeeUsed, "gas_used must come from offset 112, not from wrapper_address") + assert.Equal(t, store.EventTypeOutbound, event.Type) +} + +// Any program can put a gateway discriminator in its own inner instruction. +func TestGatewayEventPayloads_IgnoresForeignEmitter(t *testing.T) { + var tx solanarpc.GetTransactionResult + require.NoError(t, json.Unmarshal([]byte(devnetFinalizeTx), &tx)) + + assert.Empty(t, gatewayEventPayloads(&tx, "11111111111111111111111111111111"), + "an event emitted by another program must not be read as the gateway's") +} diff --git a/universalClient/externalchains/svm/event_parser.go b/universalClient/externalchains/svm/event_parser.go index cc65ea4c1..5b794a1b3 100644 --- a/universalClient/externalchains/svm/event_parser.go +++ b/universalClient/externalchains/svm/event_parser.go @@ -93,8 +93,15 @@ func parseSendFundsEvent(log string, signature string, slot uint64, logIndex uin ExpiryBlockHeight: 0, // Will be set based on confirmation type if needed } - // Parse event data from this log - parseUniversalTxEvent(event, decoded, logIndex, chainID, logger) + // Parse event data from this log. A malformed event is dropped rather than + // stored half-decoded: the zero values it would carry are not neutral. + if err := parseUniversalTxEvent(event, decoded, logIndex, chainID, logger); err != nil { + logger.Warn(). + Err(err). + Str("event_id", eventID). + Msg("discarding malformed UniversalTx event") + return nil + } return event } @@ -104,15 +111,14 @@ func parseSendFundsEvent(log string, signature string, slot uint64, logIndex uin // - discriminator (8 bytes) // - sub_tx_id (32 bytes) // - universal_tx_id (32 bytes) -// - gas_fee (8 bytes, u64 lamports) — prepaid budget -// - gas_used (8 bytes, u64 lamports) — actual lamports consumed -// - gas_to_refund (8 bytes, u64 lamports) — gas_fee - gas_used returned to caller -// - ata_created (1 byte, bool) — true if SPL ATA was newly created -// - push_account (20 bytes) -// - target (32 bytes, Pubkey) -// - token (32 bytes, Pubkey) -// - amount (8 bytes, u64) -// - payload (4 bytes length + data, Vec) +// The events diverge after universal_tx_id: +// +// UniversalTxFinalized ... wrapper_address(32) gas_fee(8) gas_used -> 112 +// FundsRescued ... token(32) amount(8) -> 112 +// RevertUniversalTx ... revert_recipient(32) token(32) amount(8) -> no gas_used +// +// Revert carries no gas_used, and core never refunds one (applyGasRefund returns +// early for INBOUND_REVERT), so nothing needs the value. func parseOutboundObservationEvent(log string, signature string, slot uint64, logIndex uint, eventType string, logger zerolog.Logger) *store.Event { if !strings.HasPrefix(log, "Program data: ") { return nil @@ -124,12 +130,26 @@ func parseOutboundObservationEvent(log string, signature string, slot uint64, lo return nil } - // Minimum: 8 disc + 32 sub_tx_id + 32 universal_tx_id + 8 gas_fee + 8 gas_used - // + 8 gas_to_refund + 1 ata_created = 97 bytes. - if len(decoded) < 97 { + // -1 means the event carries no gas_used field. + gasUsedOffset := -1 + switch eventType { + case EventTypeFinalizeUniversalTx, EventTypeFundsRescued: + gasUsedOffset = 112 + case EventTypeRevertUniversalTx: + default: + return nil + } + + need := 72 // the shared prefix + if gasUsedOffset >= 0 { + need = gasUsedOffset + 8 + } + if len(decoded) < need { logger.Warn(). Int("data_len", len(decoded)). - Msg("data too short for outboundObservation event; need at least 97 bytes") + Int("need", need). + Str("event_type", eventType). + Msg("data too short for outboundObservation event") return nil } @@ -143,44 +163,29 @@ func parseOutboundObservationEvent(log string, signature string, slot uint64, lo Uint64("slot", slot). Msg("processing outboundObservation event") - // All three events share the first two fields: disc(8) sub_tx_id(32) universal_tx_id(32). txID := "0x" + hex.EncodeToString(decoded[8:40]) universalTxID := "0x" + hex.EncodeToString(decoded[40:72]) + gasFeeUsed := "" + if gasUsedOffset >= 0 { + gasFeeUsed = fmt.Sprintf("%d", binary.LittleEndian.Uint64(decoded[gasUsedOffset:gasUsedOffset+8])) + } - // gas_used sits at a different offset per event (the structs diverge after - // universal_tx_id). Only UniversalTxFinalized carries wrapper_address. - var gasUsed uint64 + // Only the finalize event carries wrapper_address, at 72..104. The length + // check above already covers that range. Zero is Pubkey::default(), the + // non-PC20 path. var wrapperAddr string - readGasUsed := func(offset int) { - if len(decoded) >= offset+8 { - gasUsed = binary.LittleEndian.Uint64(decoded[offset : offset+8]) + if eventType == EventTypeFinalizeUniversalTx { + var wrap [32]byte + copy(wrap[:], decoded[72:104]) + if wrap != ([32]byte{}) { + wrapperAddr = solana.PublicKeyFromBytes(wrap[:]).String() } } - switch eventType { - case EventTypeFinalizeUniversalTx: - // ...universal_tx_id(32) wrapper_address(32) gas_fee(8) gas_used(8) ... - const wrapperOffset = 8 + 32 + 32 // 72 - readGasUsed(wrapperOffset + 32 + 8) // 112 (skip wrapper_address + gas_fee) - if len(decoded) >= wrapperOffset+32 { - var wrap [32]byte - copy(wrap[:], decoded[wrapperOffset:wrapperOffset+32]) - if wrap != ([32]byte{}) { // Pubkey::default() = non-PC20 path, no wrapper - wrapperAddr = solana.PublicKeyFromBytes(wrap[:]).String() - } - } - case EventTypeRevertUniversalTx: - // ...universal_tx_id(32) revert_recipient(32) token(32) amount(8) gas_used(8) ... - readGasUsed(8 + 32 + 32 + 32 + 32 + 8) // 144 - case EventTypeFundsRescued: - // ...universal_tx_id(32) token(32) amount(8) gas_used(8) ... - readGasUsed(8 + 32 + 32 + 32 + 8) // 112 - } - // Create OutboundEvent payload payload := common.OutboundObservation{ TxID: txID, UniversalTxID: universalTxID, - GasFeeUsed: fmt.Sprintf("%d", gasUsed), + GasFeeUsed: gasFeeUsed, Pc20WrapperAddress: wrapperAddr, } @@ -209,7 +214,7 @@ func parseOutboundObservationEvent(log string, signature string, slot uint64, lo Str("event_id", eventID). Str("tx_id", txID). Str("universal_tx_id", universalTxID). - Str("gas_used", fmt.Sprintf("%d", gasUsed)). + Str("gas_fee_used", gasFeeUsed). Msg("parsed outboundObservation event") return event @@ -217,15 +222,11 @@ func parseOutboundObservationEvent(log string, signature string, slot uint64, lo // parseUniversalTxEvent extracts specific data from a single log event // For TxWithFunds events, it JSON-marshals the decoded fields into event.EventData. -func parseUniversalTxEvent(event *store.Event, decoded []byte, logIndex uint, chainID string, logger zerolog.Logger) { - // Parse the TxWithFunds event +func parseUniversalTxEvent(event *store.Event, decoded []byte, logIndex uint, chainID string, logger zerolog.Logger) error { + // Parse the UniversalTx event payload, err := decodeUniversalTxEvent(decoded, logger) if err != nil { - logger.Warn(). - Err(err). - Uint("log_index", logIndex). - Msg("failed to decode TxWithFunds event") - return + return fmt.Errorf("decode UniversalTx event: %w", err) } // Set source chain and log index @@ -233,13 +234,11 @@ func parseUniversalTxEvent(event *store.Event, decoded []byte, logIndex uint, ch payload.LogIndex = logIndex // Marshal and store into event.EventData - if b, err := json.Marshal(payload); err == nil { - event.EventData = b - } else { - logger.Warn(). - Err(err). - Msg("failed to marshal universal tx payload") + b, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshal universal tx payload: %w", err) } + event.EventData = b // if TxType is 0 or 1, use FAST else use STANDARD if payload.TxType == 0 || payload.TxType == 1 { @@ -247,16 +246,20 @@ func parseUniversalTxEvent(event *store.Event, decoded []byte, logIndex uint, ch } else { event.ConfirmationType = store.ConfirmationStandard } + + return nil } -// decodeUniversalTxEvent decodes a TxWithFunds event +// decodeUniversalTxEvent decodes the gateway's Borsh-encoded UniversalTx event: +// +// sender 32, recipient 20, token 32, amount u64, payload (u32 len + bytes), +// revert_recipient 32, tx_type 1, signature_data (u32 len + bytes), from_cea 1 +// +// Every field through signature_data is required. A truncated event is rejected +// rather than returned half-filled, since the zero values are not neutral: +// tx_type 0 is GAS, which routes funds to a different account than FUNDS does. +// Only from_cea is optional, defaulting to false as its absence cannot misroute. func decodeUniversalTxEvent(data []byte, logger zerolog.Logger) (*common.InboundObservation, error) { - if len(data) < 120 { - logger.Warn(). - Int("data_len", len(data)). - Msg("data might be too short for complete TxWithFunds event") - } - offset := 8 payload := &common.InboundObservation{} @@ -302,19 +305,14 @@ func decodeUniversalTxEvent(data []byte, logger zerolog.Logger) (*common.Inbound // Parse data field length (4 bytes) if len(data) < offset+4 { - logger.Warn().Msg("not enough data for data field length") - return payload, nil + return nil, fmt.Errorf("not enough data for data field length") } dataLen := binary.LittleEndian.Uint32(data[offset : offset+4]) offset += 4 // Parse data field if len(data) < offset+int(dataLen) { - logger.Warn(). - Uint32("expected_len", dataLen). - Int("available", len(data)-offset). - Msg("not enough data for data field") - return payload, nil + return nil, fmt.Errorf("data field claims %d bytes, only %d available", dataLen, len(data)-offset) } if dataLen > 0 { dataField := data[offset : offset+int(dataLen)] @@ -324,18 +322,19 @@ func decodeUniversalTxEvent(data []byte, logger zerolog.Logger) (*common.Inbound // Parse revert_recipient (Pubkey) if len(data) < offset+32 { - logger.Warn().Msg("not enough data for revert recipient") - return payload, nil + return nil, fmt.Errorf("not enough data for revert recipient") } revertRecipient := solana.PublicKey(data[offset : offset+32]) payload.RevertFundRecipient = revertRecipient.String() offset += 32 // Parse tx_type (TxType enum) + // + // No default. Wire 0 is GAS, which credits the sender UEA via swap rather + // than depositing to the recipient, and also selects fast confirmation. + // Guessing it on a truncated event silently changes where the money goes. if len(data) <= offset { - logger.Warn().Msg("not enough data for tx_type, defaulting to Funds") - payload.TxType = uint(0) - return payload, nil + return nil, fmt.Errorf("not enough data for tx_type") } txType := data[offset] payload.TxType = uint(txType) @@ -343,19 +342,14 @@ func decodeUniversalTxEvent(data []byte, logger zerolog.Logger) (*common.Inbound // Parse signature data length (4 bytes) if len(data) < offset+4 { - logger.Warn().Msg("not enough data for signature length") - return payload, nil + return nil, fmt.Errorf("not enough data for signature length") } sigLen := binary.LittleEndian.Uint32(data[offset : offset+4]) offset += 4 remainingBytes := len(data) - offset if int(sigLen) > remainingBytes { - logger.Warn(). - Uint32("expected_len", sigLen). - Int("available", remainingBytes). - Msg("signature data length exceeds available data, skipping") - return payload, nil + return nil, fmt.Errorf("signature data claims %d bytes, only %d available", sigLen, remainingBytes) } if sigLen > 0 { diff --git a/universalClient/externalchains/svm/event_parser_test.go b/universalClient/externalchains/svm/event_parser_test.go index f7c6f0600..6fd10fd86 100644 --- a/universalClient/externalchains/svm/event_parser_test.go +++ b/universalClient/externalchains/svm/event_parser_test.go @@ -227,10 +227,16 @@ func TestParseEvent_Routing(t *testing.T) { assert.Equal(t, store.EventTypeOutbound, event.Type) }) - t.Run("revert_universal_tx routes to outbound parser", func(t *testing.T) { - event := ParseEvent(outboundLog, sig, 100, 0, EventTypeRevertUniversalTx, chainID, logger) + t.Run("revert_universal_tx routes to outbound parser without a gas fee", func(t *testing.T) { + revertLog := wrapAsLog(buildRevertPayload(txID, utxID, 5000)) + event := ParseEvent(revertLog, sig, 100, 0, EventTypeRevertUniversalTx, chainID, logger) require.NotNil(t, event) assert.Equal(t, store.EventTypeOutbound, event.Type) + + var outbound common.OutboundObservation + require.NoError(t, json.Unmarshal(event.EventData, &outbound)) + assert.Empty(t, outbound.GasFeeUsed, + "the revert event carries no gas_used; reading one would report revert_instruction bytes") }) t.Run("unknown event type returns nil", func(t *testing.T) { @@ -244,6 +250,58 @@ func TestParseEvent_Routing(t *testing.T) { }) } +// Each outbound event has its own gas_used offset, so "long enough" differs +// by type. +func TestParseOutboundObservationEvent_LengthIsPerEventType(t *testing.T) { + logger := nopLogger() + chainID := "solana:devnet" + sig := "sig" + var txID, utxID [32]byte + + t.Run("finalize needs 120 bytes", func(t *testing.T) { + assert.Nil(t, ParseEvent(wrapAsLog(make([]byte, 119)), sig, 1, 0, EventTypeFinalizeUniversalTx, chainID, logger)) + assert.NotNil(t, ParseEvent(wrapAsLog(make([]byte, 120)), sig, 1, 0, EventTypeFinalizeUniversalTx, chainID, logger)) + }) + + t.Run("rescue needs 120 bytes", func(t *testing.T) { + assert.Nil(t, ParseEvent(wrapAsLog(make([]byte, 119)), sig, 1, 0, EventTypeFundsRescued, chainID, logger)) + assert.NotNil(t, ParseEvent(wrapAsLog(make([]byte, 120)), sig, 1, 0, EventTypeFundsRescued, chainID, logger)) + }) + + t.Run("revert needs only the shared prefix", func(t *testing.T) { + assert.Nil(t, ParseEvent(wrapAsLog(make([]byte, 71)), sig, 1, 0, EventTypeRevertUniversalTx, chainID, logger)) + assert.NotNil(t, ParseEvent(wrapAsLog(make([]byte, 72)), sig, 1, 0, EventTypeRevertUniversalTx, chainID, logger)) + }) + + t.Run("an unroutable event type is refused", func(t *testing.T) { + assert.Nil(t, parseOutboundObservationEvent( + wrapAsLog(buildOutboundPayload(txID, utxID, 1)), sig, 1, 0, "send_funds", logger), + "only the three outbound events are routable here") + }) +} + +// Each event type must read gas_used from its own offset. +func TestParseOutboundObservationEvent_ReadsItsOwnOffset(t *testing.T) { + logger := nopLogger() + var txID, utxID [32]byte + + for _, tc := range []struct { + eventType string + payload []byte + }{ + {EventTypeFinalizeUniversalTx, buildOutboundPayload(txID, utxID, 4242)}, + {EventTypeFundsRescued, buildOutboundPayload(txID, utxID, 4242)}, + } { + t.Run(tc.eventType, func(t *testing.T) { + event := ParseEvent(wrapAsLog(tc.payload), "sig", 1, 0, tc.eventType, "solana:devnet", logger) + require.NotNil(t, event) + var outbound common.OutboundObservation + require.NoError(t, json.Unmarshal(event.EventData, &outbound)) + assert.Equal(t, "4242", outbound.GasFeeUsed) + }) + } +} + func TestParseSendFundsEvent(t *testing.T) { logger := nopLogger() chainID := "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" @@ -379,22 +437,30 @@ func TestParseSendFundsEvent_TruncatedData(t *testing.T) { chainID := "solana:devnet" sig := "truncSig" - t.Run("data too short for sender returns event with nil EventData", func(t *testing.T) { + // A truncated event is discarded rather than stored. Every field it fails to + // reach would otherwise be left at its zero value, and tx_type 0 is GAS, + // which credits the sender UEA instead of depositing to the recipient. + t.Run("data too short for sender is discarded", func(t *testing.T) { // Only discriminator (8 bytes), no sender data := make([]byte, 8) event := ParseEvent(wrapAsLog(data), sig, 1, 0, EventTypeSendFunds, chainID, logger) - require.NotNil(t, event) - // Event is created but parseUniversalTxEvent will fail to decode, - // so EventData may be nil - assert.Equal(t, store.EventTypeInbound, event.Type) + assert.Nil(t, event) }) - t.Run("data truncated after sender still returns event", func(t *testing.T) { + t.Run("data truncated after sender is discarded", func(t *testing.T) { // 8 disc + 32 sender = 40 bytes, missing recipient data := make([]byte, 40) event := ParseEvent(wrapAsLog(data), sig, 1, 0, EventTypeSendFunds, chainID, logger) - require.NotNil(t, event) - assert.Equal(t, store.EventTypeInbound, event.Type) + assert.Nil(t, event) + }) + + t.Run("truncated one byte before tx_type is discarded", func(t *testing.T) { + // Everything through revert_recipient, then nothing. This is the exact + // shape that used to decode as TxType 0 and route to GAS. + data := make([]byte, 136) + binary.LittleEndian.PutUint32(data[100:104], 0) + event := ParseEvent(wrapAsLog(data), sig, 1, 0, EventTypeSendFunds, chainID, logger) + assert.Nil(t, event) }) } @@ -455,7 +521,7 @@ func TestParseOutboundObservationEvent(t *testing.T) { assert.Empty(t, outbound.Pc20WrapperAddress) }) - t.Run("revert event reads gas_used (@144) and no wrapper", func(t *testing.T) { + t.Run("revert event reports no gas_used and no wrapper", func(t *testing.T) { var txID, utxID [32]byte data := buildRevertPayload(txID, utxID, 7777) event := ParseEvent(wrapAsLog(data), signature, 1, 0, EventTypeRevertUniversalTx, chainID, logger) @@ -464,7 +530,8 @@ func TestParseOutboundObservationEvent(t *testing.T) { var outbound common.OutboundObservation require.NoError(t, json.Unmarshal(event.EventData, &outbound)) assert.Empty(t, outbound.Pc20WrapperAddress) - assert.Equal(t, "7777", outbound.GasFeeUsed) + assert.Empty(t, outbound.GasFeeUsed, + "the gateway dropped gas_used from RevertUniversalTx") }) t.Run("rescue event reads gas_used (@112) and no wrapper", func(t *testing.T) { @@ -495,7 +562,7 @@ func TestParseOutboundObservationEvent(t *testing.T) { }) t.Run("returns nil for data too short", func(t *testing.T) { - shortData := make([]byte, 96) // needs 97 + shortData := make([]byte, 119) // gas_used ends at 120 event := ParseEvent(wrapAsLog(shortData), signature, 12345, 0, EventTypeFinalizeUniversalTx, chainID, logger) assert.Nil(t, event) }) @@ -521,7 +588,7 @@ func TestParseOutboundObservationEvent(t *testing.T) { assert.Equal(t, "12345", outbound.GasFeeUsed) }) - t.Run("handles data longer than 97 bytes", func(t *testing.T) { + t.Run("handles data longer than the fixed fields", func(t *testing.T) { var txID, utxID [32]byte for i := range txID { txID[i] = 0xAA @@ -665,42 +732,186 @@ func TestDecodeUniversalTxEvent_PartialData(t *testing.T) { assert.Contains(t, err.Error(), "bridge_amount") }) - t.Run("returns partial result when no data field length", func(t *testing.T) { + // Everything through signature_data is required. Returning a partial result + // leaves tx_type at 0, which is GAS on the wire, so a truncated FUNDS + // transfer would be credited to the sender UEA instead of the recipient. + + t.Run("returns error when no data field length", func(t *testing.T) { // 8 + 32 + 20 + 32 + 8 = 100, no data_len data := make([]byte, 100) binary.LittleEndian.PutUint64(data[92:100], 777) - result, err := decodeUniversalTxEvent(data, logger) - require.NoError(t, err) - assert.Equal(t, "777", result.Amount) + _, err := decodeUniversalTxEvent(data, logger) + require.Error(t, err) + assert.Contains(t, err.Error(), "data field length") }) - t.Run("returns partial result when data field exceeds available bytes", func(t *testing.T) { + t.Run("returns error when data field exceeds available bytes", func(t *testing.T) { // 8 + 32 + 20 + 32 + 8 + 4 = 104 data := make([]byte, 104) binary.LittleEndian.PutUint64(data[92:100], 555) binary.LittleEndian.PutUint32(data[100:104], 999) // claims 999 bytes of payload - result, err := decodeUniversalTxEvent(data, logger) - require.NoError(t, err) - assert.Equal(t, "555", result.Amount) - assert.Empty(t, result.RawPayload) // not enough data, so payload is skipped + _, err := decodeUniversalTxEvent(data, logger) + require.Error(t, err) + assert.Contains(t, err.Error(), "claims 999 bytes") }) - t.Run("returns partial result when missing revert recipient", func(t *testing.T) { + t.Run("returns error when missing revert recipient", func(t *testing.T) { // 8 + 32 + 20 + 32 + 8 + 4(data_len=0) = 104 data := make([]byte, 104) binary.LittleEndian.PutUint32(data[100:104], 0) // 0 length payload - result, err := decodeUniversalTxEvent(data, logger) - require.NoError(t, err) - assert.Empty(t, result.RevertFundRecipient) + _, err := decodeUniversalTxEvent(data, logger) + require.Error(t, err) + assert.Contains(t, err.Error(), "revert recipient") }) - t.Run("returns partial result when missing tx_type", func(t *testing.T) { + t.Run("returns error when missing tx_type rather than defaulting to GAS", func(t *testing.T) { // 8 + 32 + 20 + 32 + 8 + 4(data_len=0) + 32(revert) = 136 data := make([]byte, 136) binary.LittleEndian.PutUint32(data[100:104], 0) + _, err := decodeUniversalTxEvent(data, logger) + require.Error(t, err) + assert.Contains(t, err.Error(), "tx_type") + }) + + t.Run("returns error when missing signature length", func(t *testing.T) { + // 136 + 1(tx_type) = 137, no signature length + data := make([]byte, 137) + binary.LittleEndian.PutUint32(data[100:104], 0) + data[136] = 2 // Funds + _, err := decodeUniversalTxEvent(data, logger) + require.Error(t, err) + assert.Contains(t, err.Error(), "signature length") + }) + + t.Run("returns error when signature data exceeds available bytes", func(t *testing.T) { + data := make([]byte, 141) + binary.LittleEndian.PutUint32(data[100:104], 0) + data[136] = 2 + binary.LittleEndian.PutUint32(data[137:141], 500) + _, err := decodeUniversalTxEvent(data, logger) + require.Error(t, err) + assert.Contains(t, err.Error(), "claims 500 bytes") + }) + + t.Run("from_cea stays optional and defaults to false", func(t *testing.T) { + // 141 bytes: complete through signature_data, no from_cea byte. + data := make([]byte, 141) + binary.LittleEndian.PutUint32(data[100:104], 0) + data[136] = 2 // Funds + binary.LittleEndian.PutUint32(data[137:141], 0) result, err := decodeUniversalTxEvent(data, logger) require.NoError(t, err) - // tx_type defaults to 0 when missing - assert.Equal(t, uint(0), result.TxType) + assert.Equal(t, uint(2), result.TxType) + assert.False(t, result.FromCEA) }) } + +// Real UniversalTx events captured from the deployed devnet gateway +// CFVSincHYbETh2k7w6u1ENEkjbSLtveRCEBupKidw2VS. They pin the decoder to what +// the chain actually emits rather than to a hand-built fixture. +// +// Layout, matching the gateway on pc20-3rd-iteration: +// +// disc 8, sender 32, recipient 20, token 32, amount u64, +// payload (u32 len + bytes), revert_recipient 32, tx_type 1, +// signature_data (u32 len + bytes), from_cea 1 = 142 bytes when both vecs are empty +var devnetUniversalTxEvents = []struct { + name string + hex string + wantAmount string + wantTxType uint + wantFromCEA bool + wantConfirmDep string +}{ + { + name: "3000000 lamports, Funds", + hex: "6c9ad829b5ea1d7c5824d1bda3f79e54416ae3d2ec8d8a7456ae9a3e8a85e2f43e3bd20f25a39e5107a26674effcfbef4ee6cc6e8a00dc54801d83d90000000000000000000000000000000000000000000000000000000000000000c0c62d0000000000000000005824d1bda3f79e54416ae3d2ec8d8a7456ae9a3e8a85e2f43e3bd20f25a39e51020000000001", + wantAmount: "3000000", + wantTxType: 2, + wantFromCEA: true, + wantConfirmDep: store.ConfirmationStandard, + }, + { + name: "8000 lamports, Funds", + hex: "6c9ad829b5ea1d7cdc84c8dd7c695f0ed78f3507fd867827812dcd9ccbba61ca9a16d899b6f5ac665c70c864cf1adfb04a0e107ffa248ba3600eab8dcbcae9e66452fe98abcf0fa51e557fc8d671c7fc6ce83c05f92992a3d2bf1932401f00000000000000000000dc84c8dd7c695f0ed78f3507fd867827812dcd9ccbba61ca9a16d899b6f5ac66020000000001", + wantAmount: "8000", + wantTxType: 2, + wantFromCEA: true, + wantConfirmDep: store.ConfirmationStandard, + }, + { + name: "5000000 lamports, Funds", + hex: "6c9ad829b5ea1d7cdc84c8dd7c695f0ed78f3507fd867827812dcd9ccbba61ca9a16d899b6f5ac665c70c864cf1adfb04a0e107ffa248ba3600eab8d0000000000000000000000000000000000000000000000000000000000000000404b4c000000000000000000dc84c8dd7c695f0ed78f3507fd867827812dcd9ccbba61ca9a16d899b6f5ac66020000000001", + wantAmount: "5000000", + wantTxType: 2, + wantFromCEA: true, + wantConfirmDep: store.ConfirmationStandard, + }, +} + +func TestDecodeUniversalTxEvent_RealDevnetEvents(t *testing.T) { + logger := nopLogger() + + for _, tc := range devnetUniversalTxEvents { + t.Run(tc.name, func(t *testing.T) { + data, err := hex.DecodeString(tc.hex) + require.NoError(t, err) + require.Len(t, data, 142, "captured event is not the deployed layout") + + got, err := decodeUniversalTxEvent(data, logger) + require.NoError(t, err) + + assert.Equal(t, tc.wantAmount, got.Amount) + assert.Equal(t, tc.wantTxType, got.TxType, "tx_type must survive decoding, not be defaulted") + assert.Equal(t, tc.wantFromCEA, got.FromCEA) + assert.NotEmpty(t, got.Sender) + assert.NotEmpty(t, got.Recipient) + assert.NotEmpty(t, got.RevertFundRecipient) + }) + } +} + +// FUNDS must take the slower confirmation path. The old default of 0 selected +// FAST as well as routing to GAS, so a high value transfer lost finality too. +func TestParseSendFundsEvent_RealDevnetEventConfirmation(t *testing.T) { + logger := nopLogger() + + for _, tc := range devnetUniversalTxEvents { + t.Run(tc.name, func(t *testing.T) { + data, err := hex.DecodeString(tc.hex) + require.NoError(t, err) + log := "Program data: " + base64.StdEncoding.EncodeToString(data) + + event := ParseEvent(log, "devnetSig", 1, 0, EventTypeSendFunds, "solana:devnet", logger) + require.NotNil(t, event) + require.NotNil(t, event.EventData) + + assert.Equal(t, store.EventTypeInbound, event.Type) + assert.Equal(t, tc.wantConfirmDep, event.ConfirmationType) + }) + } +} + +// Truncating a real event anywhere past bridge_amount must be rejected. Before +// the fix each of these decoded successfully with TxType left at 0. +func TestDecodeUniversalTxEvent_TruncatedRealEventIsRejected(t *testing.T) { + logger := nopLogger() + + full, err := hex.DecodeString(devnetUniversalTxEvents[0].hex) + require.NoError(t, err) + + // 100 is the end of bridge_amount; 142 is the whole event. from_cea is the + // only optional field, so 141 is the shortest valid length. + for n := 100; n < 141; n++ { + got, err := decodeUniversalTxEvent(full[:n], logger) + require.Error(t, err, "%d-byte truncation was accepted", n) + assert.Nil(t, got) + } + + // The two valid lengths still decode, and both carry the real tx_type. + for _, n := range []int{141, 142} { + got, err := decodeUniversalTxEvent(full[:n], logger) + require.NoError(t, err, "%d-byte event was rejected", n) + assert.Equal(t, uint(2), got.TxType) + } +} diff --git a/universalClient/externalchains/svm/rpc_client.go b/universalClient/externalchains/svm/rpc_client.go index ee1ac9a0d..7d30a2779 100644 --- a/universalClient/externalchains/svm/rpc_client.go +++ b/universalClient/externalchains/svm/rpc_client.go @@ -297,9 +297,12 @@ func calculateMedian(fees []uint64) uint64 { // otherwise it returns signatures strictly older than `before`, enabling // backward pagination. func (rc *RPCClient) GetSignaturesForAddress(ctx context.Context, address solana.PublicKey, before solana.Signature) ([]*rpc.TransactionSignature, error) { - var opts *rpc.GetSignaturesForAddressOpts + // Commitment is set explicitly rather than left to the server default, so + // discovery and the slot the confirmation depth is measured against are on + // the same footing. + opts := &rpc.GetSignaturesForAddressOpts{Commitment: rpc.CommitmentFinalized} if !before.IsZero() { - opts = &rpc.GetSignaturesForAddressOpts{Before: before} + opts.Before = before } var signatures []*rpc.TransactionSignature err := rc.executeWithFailover(ctx, "get_signatures_for_address", func(client *rpc.Client) error { @@ -321,6 +324,7 @@ func (rc *RPCClient) GetTransaction(ctx context.Context, signature solana.Signat signature, &rpc.GetTransactionOpts{ Encoding: solana.EncodingBase64, + Commitment: rpc.CommitmentFinalized, MaxSupportedTransactionVersion: &maxVersion, }, ) diff --git a/universalClient/externalchains/svm/tx_builder.go b/universalClient/externalchains/svm/tx_builder.go index e85888360..5bb7eb3a5 100644 --- a/universalClient/externalchains/svm/tx_builder.go +++ b/universalClient/externalchains/svm/tx_builder.go @@ -30,6 +30,7 @@ import ( "encoding/binary" "encoding/hex" "encoding/json" + "errors" "fmt" "math/big" "os" @@ -236,9 +237,8 @@ func (tb *TxBuilder) GetOutboundSigningRequest( } // Determine if this is native SOL or an SPL token transfer. - // Empty or zero address = native SOL. Otherwise it's the SPL token mint address. assetAddr := data.AssetAddr - isNative := assetAddr == "" || assetAddr == "0x0" || assetAddr == "0x0000000000000000000000000000000000000000" + isNative := isNativeAsset(assetAddr) txType, err := parseTxType(data.TxType) if err != nil { @@ -322,14 +322,11 @@ func (tb *TxBuilder) GetOutboundSigningRequest( // - Withdraw (id=1): the wallet that receives the funds (target = recipient) // - Execute (id=2): the target program to CPI into (target = destination_program) // - Revert (id=3,4): the wallet that gets the refund - var recipientPubkey solana.PublicKey - recipientPubkey, err = solana.PublicKeyFromBase58(data.Recipient) + // + // An empty recipient is the parking sentinel; see resolveRecipient. + recipientPubkey, recipientParked, err := tb.resolveRecipient(data.Recipient, sender) if err != nil { - hexBytes, hexErr := hex.DecodeString(removeHexPrefix(data.Recipient)) - if hexErr != nil || len(hexBytes) != 32 { - return nil, fmt.Errorf("invalid recipient address format (expected Solana Pubkey): %s", data.Recipient) - } - recipientPubkey = solana.PublicKeyFromBytes(hexBytes) + return nil, err } // --- Determine instruction ID and decode payload --- @@ -437,6 +434,10 @@ func (tb *TxBuilder) GetOutboundSigningRequest( } } + if err := checkParkedRecipientScope(recipientParked, instructionID); err != nil { + return nil, err + } + // --- Construct the TSS message and hash it --- // This message is what TSS validators sign. The gateway contract reconstructs // the same message on-chain and verifies the signature matches. @@ -523,9 +524,14 @@ func (tb *TxBuilder) VerifyBroadcastedTx(ctx context.Context, txHash string) (fo } tx, txErr := tb.rpcClient.GetTransaction(ctx, sig) - if txErr != nil { + // solana-go reports a genuinely absent tx as ErrNotFound, so that one is a verdict. + if errors.Is(txErr, rpc.ErrNotFound) { return false, 0, 0, 0, nil } + if txErr != nil { + // Reporting a not-found verdict here would let the resolver vote failure against a tx that already executed. + return false, 0, 0, 0, txErr + } if tx == nil { return false, 0, 0, 0, nil @@ -786,7 +792,7 @@ func (tb *TxBuilder) BuildOutboundTransaction( } assetAddr := data.AssetAddr - isNative := assetAddr == "" || assetAddr == "0x0" || assetAddr == "0x0000000000000000000000000000000000000000" + isNative := isNativeAsset(assetAddr) txType, err := parseTxType(data.TxType) if err != nil { @@ -845,13 +851,11 @@ func (tb *TxBuilder) BuildOutboundTransaction( gasFee, _ = strconv.ParseUint(data.GasFee, 10, 64) } - recipientPubkey, err := solana.PublicKeyFromBase58(data.Recipient) + // Must resolve identically to GetOutboundSigningRequest, or the accounts list + // would not match the pubkey already bound into the TSS message. + recipientPubkey, recipientParked, err := tb.resolveRecipient(data.Recipient, sender) if err != nil { - hexBytes, hexErr := hex.DecodeString(removeHexPrefix(data.Recipient)) - if hexErr != nil || len(hexBytes) != 32 { - return nil, 0, fmt.Errorf("invalid recipient address format: %s", data.Recipient) - } - recipientPubkey = solana.PublicKeyFromBytes(hexBytes) + return nil, 0, err } revertMsgBytes, err := hex.DecodeString(removeHexPrefix(data.RevertMsg)) @@ -897,6 +901,10 @@ func (tb *TxBuilder) BuildOutboundTransaction( } } + if err := checkParkedRecipientScope(recipientParked, instructionID); err != nil { + return nil, 0, err + } + // --- Derive PDAs --- configPDA, _, err := solana.FindProgramAddress([][]byte{configSeed}, tb.gatewayAddress) if err != nil { @@ -1022,10 +1030,6 @@ func (tb *TxBuilder) BuildOutboundTransaction( } // --- Assemble the Solana transaction --- - // Instructions in order: - // 1. SetComputeUnitLimit — tells the runtime how many compute units to allocate - // 2. (SPL only) CreateAssociatedTokenAccount — creates recipient ATA if it doesn't exist - // 3. The actual gateway instruction (withdraw/execute/revert) gatewayInstruction := solana.NewInstruction( tb.gatewayAddress, @@ -1038,20 +1042,9 @@ func (tb *TxBuilder) BuildOutboundTransaction( computeLimitIx := tb.buildSetComputeUnitLimitInstruction(defaultComputeUnitLimit) // Build the instruction list. - instructions := []solana.Instruction{computeLimitIx} - - // PC20 remint skips the pre-ix — the gateway creates the recipient ATA itself. - needsRecipientATA := ((instructionID == 1 && !isNative) || ((instructionID == 3 || instructionID == 4) && !isNative)) && pc20SourceAsset == nil - if needsRecipientATA { - createATAInstruction := tb.buildCreateATAIdempotentInstruction( - relayerKeypair.PublicKey(), - recipientPubkey, - mintPubkey, - ) - instructions = append(instructions, createATAInstruction) - } - - instructions = append(instructions, gatewayInstruction) + // The gateway creates the recipient ATA and meters the rent, so creating it + // here would leave that cost outside gas_used. + instructions := []solana.Instruction{computeLimitIx, gatewayInstruction} // Get a recent blockhash — Solana uses this instead of nonces for transaction expiry. // Transactions expire after ~60-90 seconds if not confirmed. @@ -1160,7 +1153,7 @@ func (tb *TxBuilder) BuildRefRouteTransactions( } assetAddr := data.AssetAddr - isNative := assetAddr == "" || assetAddr == "0x0" || assetAddr == "0x0000000000000000000000000000000000000000" + isNative := isNativeAsset(assetAddr) var txID [32]byte txIDBytes, err := hex.DecodeString(removeHexPrefix(data.TxID)) @@ -1212,13 +1205,9 @@ func (tb *TxBuilder) BuildRefRouteTransactions( gasFee, _ = strconv.ParseUint(data.GasFee, 10, 64) } - recipientPubkey, err := solana.PublicKeyFromBase58(data.Recipient) + recipientPubkey, recipientParked, err := tb.resolveRecipient(data.Recipient, sender) if err != nil { - hexBytes, hexErr := hex.DecodeString(removeHexPrefix(data.Recipient)) - if hexErr != nil || len(hexBytes) != 32 { - return nil, nil, solana.PublicKey{}, fmt.Errorf("invalid recipient address format: %s", data.Recipient) - } - recipientPubkey = solana.PublicKeyFromBytes(hexBytes) + return nil, nil, solana.PublicKey{}, err } // Decode payload — ref route is execute-only, so we require an instruction_id of 2. @@ -1241,6 +1230,9 @@ func (tb *TxBuilder) BuildRefRouteTransactions( if instructionID != 2 { return nil, nil, solana.PublicKey{}, fmt.Errorf("ref route only valid for execute mode (instruction_id=2), got %d", instructionID) } + if err := checkParkedRecipientScope(recipientParked, instructionID); err != nil { + return nil, nil, solana.PublicKey{}, err + } if len(ixData) == 0 { return nil, nil, solana.PublicKey{}, fmt.Errorf("ref route requires non-empty ix_data") } @@ -1345,14 +1337,7 @@ func (tb *TxBuilder) BuildRefRouteTransactions( refInstruction := solana.NewInstruction(tb.gatewayAddress, refAccounts, refInstructionData) computeLimitIx := tb.buildSetComputeUnitLimitInstruction(defaultComputeUnitLimit) - instructions := []solana.Instruction{computeLimitIx} - needsRecipientATA := !isNative && false // execute mode (id=2) doesn't create recipient ATA; gateway handles cea_ata internally - if needsRecipientATA { - instructions = append(instructions, tb.buildCreateATAIdempotentInstruction( - relayerKeypair.PublicKey(), recipientPubkey, mintPubkey, - )) - } - instructions = append(instructions, refInstruction) + instructions := []solana.Instruction{computeLimitIx, refInstruction} refOpts := []solana.TransactionOption{solana.TransactionPayer(relayerKeypair.PublicKey())} addressTables, altErr := tb.fetchAddressTables(ctx, mintPubkey, isNative) @@ -1390,6 +1375,83 @@ func removeHexPrefix(s string) string { return s } +// isNativeAsset reports whether addr denotes native SOL rather than an SPL mint. +// Both encodings of the zero address reach us. Core sends the EVM zero hex on +// withdrawals (registry token address), while reverts carry the base58 zero +// pubkey, SystemProgram 11111111111111111111111111111111, copied from the +// inbound. SPL mints are always base58 and parse as an ordinary non-zero pubkey. +func isNativeAsset(addr string) bool { + switch addr { + case "", "0x0", "0x0000000000000000000000000000000000000000": + return true + } + if pubkey, err := solana.PublicKeyFromBase58(addr); err == nil { + return pubkey.IsZero() + } + // The builder also accepts hex mints, so cover a hex-encoded zero pubkey. + // The length check is load bearing: PublicKeyFromBytes panics on anything + // other than 32 bytes, and a short hex string such as 0x1234 reaches here. + if raw, err := hex.DecodeString(removeHexPrefix(addr)); err == nil && len(raw) == 32 { + return solana.PublicKeyFromBytes(raw).IsZero() + } + return false +} + +// An empty recipient is the gateway's sentinel for parking funds in the caller's +// CEA rather than forwarding them to a wallet. Core hex-encodes the raw event +// bytes, so it arrives as "0x". The builder used to reject that pre-sign, which +// stranded the outbound PENDING with the PRC20 already burned. + +// isParkedRecipient reports whether recipient decodes to zero bytes. Tested that +// way rather than against the literal "0x" so "" and "0X" are covered too; +// anything decoding to a byte or more is a real address. +func isParkedRecipient(recipient string) bool { + s := strings.TrimSpace(recipient) + if len(s) >= 2 && (s[:2] == "0x" || s[:2] == "0X") { + s = s[2:] + } + decoded, err := hex.DecodeString(s) + return err == nil && len(decoded) == 0 +} + +// resolveRecipient converts the outbound recipient into a Solana pubkey, +// resolving the sentinel to the sender's CEA PDA. Callers must then run +// checkParkedRecipientScope once the instruction id is known. Shared by the +// signing and build paths so both derive the same pubkey. +func (tb *TxBuilder) resolveRecipient(recipient string, sender [20]byte) (solana.PublicKey, bool, error) { + if isParkedRecipient(recipient) { + ceaAuthorityPDA, _, err := solana.FindProgramAddress([][]byte{ceaAuthoritySeed, sender[:]}, tb.gatewayAddress) + if err != nil { + return solana.PublicKey{}, true, fmt.Errorf("failed to derive cea_authority PDA for parked recipient: %w", err) + } + return ceaAuthorityPDA, true, nil + } + + recipientPubkey, err := solana.PublicKeyFromBase58(recipient) + if err != nil { + hexBytes, hexErr := hex.DecodeString(removeHexPrefix(recipient)) + if hexErr != nil || len(hexBytes) != 32 { + return solana.PublicKey{}, false, fmt.Errorf("invalid recipient address format (expected Solana Pubkey): %s", recipient) + } + recipientPubkey = solana.PublicKeyFromBytes(hexBytes) + } + return recipientPubkey, false, nil +} + +// checkParkedRecipientScope confines the sentinel to withdraw (id=1), the only +// path where the recipient is a fund destination. On execute it is the CPI +// target, and on revert/rescue it is an observed source-chain address, so an +// empty value there means the outbound is malformed. +func checkParkedRecipientScope(parked bool, instructionID uint8) error { + if !parked || instructionID == 1 { + return nil + } + return fmt.Errorf( + "empty recipient parks funds in the sender CEA and is only valid for withdraw (instruction_id=1), got instruction_id=%d", + instructionID, + ) +} + // ============================================================================= // PDA Derivation & On-Chain Data // ============================================================================= @@ -2200,7 +2262,7 @@ func (tb *TxBuilder) buildRevertAccounts( if isNative { // SOL: optional SPL accounts are None (gateway program ID sentinel) - for i := 0; i < 4; i++ { + for i := 0; i < 6; i++ { accounts = append(accounts, &solana.AccountMeta{PublicKey: tb.gatewayAddress, IsWritable: false, IsSigner: false}) } } else { @@ -2218,6 +2280,9 @@ func (tb *TxBuilder) buildRevertAccounts( &solana.AccountMeta{PublicKey: recipientATA, IsWritable: true, IsSigner: false}, &solana.AccountMeta{PublicKey: mintPubkey, IsWritable: false, IsSigner: false}, &solana.AccountMeta{PublicKey: solana.TokenProgramID, IsWritable: false, IsSigner: false}, + // The gateway needs these to create the recipient ATA. + &solana.AccountMeta{PublicKey: solana.SPLAssociatedTokenAccountProgramID, IsWritable: false, IsSigner: false}, + &solana.AccountMeta{PublicKey: solana.SysVarRentPubkey, IsWritable: false, IsSigner: false}, ) } @@ -2416,33 +2481,6 @@ func (tb *TxBuilder) buildCloseStoredIxDataAccounts(caller, storedIxDataPDA, exe } } -// buildCreateATAIdempotentInstruction creates the recipient's ATA if absent -// (no-op if present). Required for SPL withdraw/revert flows because the -// gateway validates the recipient ATA exists but does NOT create it. Relayer -// pays the ~0.002 SOL rent, reimbursed via gas_fee. -func (tb *TxBuilder) buildCreateATAIdempotentInstruction( - payer solana.PublicKey, - owner solana.PublicKey, - mint solana.PublicKey, -) solana.Instruction { - ata, _, _ := solana.FindProgramAddress( - [][]byte{owner.Bytes(), solana.TokenProgramID.Bytes(), mint.Bytes()}, - solana.SPLAssociatedTokenAccountProgramID, - ) - - accounts := []*solana.AccountMeta{ - {PublicKey: payer, IsWritable: true, IsSigner: true}, - {PublicKey: ata, IsWritable: true, IsSigner: false}, - {PublicKey: owner, IsWritable: false, IsSigner: false}, - {PublicKey: mint, IsWritable: false, IsSigner: false}, - {PublicKey: solana.SystemProgramID, IsWritable: false, IsSigner: false}, - {PublicKey: solana.TokenProgramID, IsWritable: false, IsSigner: false}, - } - - // ATA program instruction discriminator: 0 = Create (fails if exists), 1 = CreateIdempotent. - return solana.NewInstruction(solana.SPLAssociatedTokenAccountProgramID, accounts, []byte{1}) -} - // ============================================================================= // Fund Migration (Unsupported on SVM) // SVM funds are held by the gateway program in PDA-controlled vaults, not by TSS diff --git a/universalClient/externalchains/svm/tx_builder_test.go b/universalClient/externalchains/svm/tx_builder_test.go index 6acf7a781..b22d531cc 100644 --- a/universalClient/externalchains/svm/tx_builder_test.go +++ b/universalClient/externalchains/svm/tx_builder_test.go @@ -5,9 +5,13 @@ import ( "crypto/ecdsa" crand "crypto/rand" "crypto/sha256" + "encoding/base64" "encoding/binary" "encoding/hex" "fmt" + "io" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -1223,6 +1227,72 @@ func TestBuildWithdrawAndExecuteAccounts(t *testing.T) { }) } +// The gateway can only create the recipient ATA if we hand it recipient_ata, +// rent and the ATA program. Dropping any of them fails only on chain. +func TestBuildWithdrawAndExecuteAccounts_SPLSlots(t *testing.T) { + builder := newTestBuilder(t) + + caller := solana.NewWallet().PublicKey() + config := solana.NewWallet().PublicKey() + vault := solana.NewWallet().PublicKey() + cea := solana.NewWallet().PublicKey() + tss := solana.NewWallet().PublicKey() + executed := solana.NewWallet().PublicKey() + recipient := solana.NewWallet().PublicKey() + mint := solana.NewWallet().PublicKey() + + accounts := builder.buildWithdrawAndExecuteAccounts( + caller, config, vault, cea, tss, executed, + solana.SystemProgramID, + false, 1, + recipient, mint, + nil, + solana.PublicKey{}, solana.PublicKey{}, + ) + require.Len(t, accounts, 20) + + wantVaultATA, _, err := solana.FindAssociatedTokenAddress(vault, mint) + require.NoError(t, err) + wantCeaATA, _, err := solana.FindAssociatedTokenAddress(cea, mint) + require.NoError(t, err) + wantRecipientATA, _, err := solana.FindAssociatedTokenAddress(recipient, mint) + require.NoError(t, err) + + for _, tc := range []struct { + slot int + name string + want solana.PublicKey + writable bool + }{ + {8, "recipient", recipient, true}, + {9, "vault_ata", wantVaultATA, true}, + {10, "cea_ata", wantCeaATA, true}, + {11, "mint", mint, false}, + {12, "token_program", solana.TokenProgramID, false}, + {13, "rent", solana.SysVarRentPubkey, false}, + {14, "associated_token_program", solana.SPLAssociatedTokenAccountProgramID, false}, + {15, "recipient_ata", wantRecipientATA, true}, + } { + assert.Equal(t, tc.want, accounts[tc.slot].PublicKey, "slot %d is %s", tc.slot, tc.name) + assert.Equal(t, tc.writable, accounts[tc.slot].IsWritable, "slot %d (%s) writability", tc.slot, tc.name) + assert.False(t, accounts[tc.slot].IsSigner, "slot %d (%s) must not sign", tc.slot, tc.name) + } + + t.Run("execute leaves recipient and recipient_ata unset", func(t *testing.T) { + exec := builder.buildWithdrawAndExecuteAccounts( + caller, config, vault, cea, tss, executed, + solana.NewWallet().PublicKey(), + false, 2, + recipient, mint, + nil, + solana.PublicKey{}, solana.PublicKey{}, + ) + assert.Equal(t, builder.gatewayAddress, exec[8].PublicKey, "recipient is None for execute") + assert.Equal(t, builder.gatewayAddress, exec[15].PublicKey, "recipient_ata is None for execute") + assert.Equal(t, wantCeaATA, exec[10].PublicKey, "cea_ata is still real for execute") + }) +} + func TestBuildRevertAccounts(t *testing.T) { builder := newTestBuilder(t) @@ -1235,10 +1305,10 @@ func TestBuildRevertAccounts(t *testing.T) { caller := solana.NewWallet().PublicKey() tokenMint := solana.NewWallet().PublicKey() - t.Run("SOL revert has 12 accounts (8 required + 4 None sentinels)", func(t *testing.T) { + t.Run("SOL revert has 14 accounts (8 required + 6 None sentinels)", func(t *testing.T) { accounts := builder.buildRevertAccounts(config, vault, feeVault, tss, recipient, executed, caller, true, solana.PublicKey{}) - assert.Len(t, accounts, 12) + assert.Len(t, accounts, 14) assert.Equal(t, config, accounts[0].PublicKey, "config") assert.False(t, accounts[0].IsWritable) assert.Equal(t, vault, accounts[1].PublicKey, "vault") @@ -1254,16 +1324,16 @@ func TestBuildRevertAccounts(t *testing.T) { assert.Equal(t, caller, accounts[6].PublicKey, "caller") assert.True(t, accounts[6].IsSigner) assert.Equal(t, solana.SystemProgramID, accounts[7].PublicKey, "system_program") - // SOL: 4 optional SPL accounts are gateway sentinel (None) - for i := 8; i < 12; i++ { + // SOL: 6 optional SPL accounts are gateway sentinel (None) + for i := 8; i < 14; i++ { assert.Equal(t, builder.gatewayAddress, accounts[i].PublicKey, "SOL sentinel account %d", i) } }) - t.Run("SPL revert has 12 accounts (8 required + 4 SPL accounts)", func(t *testing.T) { + t.Run("SPL revert has 14 accounts (8 required + 6 SPL accounts)", func(t *testing.T) { accounts := builder.buildRevertAccounts(config, vault, feeVault, tss, recipient, executed, caller, false, tokenMint) - assert.Len(t, accounts, 12) + assert.Len(t, accounts, 14) // First 8 same as SOL assert.Equal(t, config, accounts[0].PublicKey, "config") assert.Equal(t, vault, accounts[1].PublicKey, "vault") @@ -1273,11 +1343,18 @@ func TestBuildRevertAccounts(t *testing.T) { assert.Equal(t, executed, accounts[5].PublicKey, "executed_tx") assert.Equal(t, caller, accounts[6].PublicKey, "caller") assert.Equal(t, solana.SystemProgramID, accounts[7].PublicKey, "system_program") - // SPL: token_vault, recipient_token_account, token_mint, token_program + // token_vault, recipient_token_account, token_mint, token_program, + // associated_token_program, rent. assert.True(t, accounts[8].IsWritable, "token_vault should be writable") assert.True(t, accounts[9].IsWritable, "recipient_token_account should be writable") assert.Equal(t, tokenMint, accounts[10].PublicKey, "token_mint") assert.Equal(t, solana.TokenProgramID, accounts[11].PublicKey, "token_program") + assert.Equal(t, solana.SPLAssociatedTokenAccountProgramID, accounts[12].PublicKey, "associated_token_program") + assert.Equal(t, solana.SysVarRentPubkey, accounts[13].PublicKey, "rent") + + wantRecipientATA, _, err := solana.FindAssociatedTokenAddress(recipient, tokenMint) + require.NoError(t, err) + assert.Equal(t, wantRecipientATA, accounts[9].PublicKey, "recipient_token_account must be the canonical ATA") }) } @@ -1725,64 +1802,6 @@ func TestNewTxBuilder_ChainConfig(t *testing.T) { }) } -func TestBuildCreateATAIdempotentInstruction(t *testing.T) { - builder := newTestBuilder(t) - payer := solana.NewWallet().PublicKey() - owner := solana.NewWallet().PublicKey() - mint := solana.NewWallet().PublicKey() - - ix := builder.buildCreateATAIdempotentInstruction(payer, owner, mint) - - t.Run("program ID is ATA program", func(t *testing.T) { - expected := solana.MustPublicKeyFromBase58("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") - assert.Equal(t, expected, ix.ProgramID()) - }) - - t.Run("has 6 accounts in correct order", func(t *testing.T) { - accounts := ix.Accounts() - require.Len(t, accounts, 6) - - // payer (signer, writable) - assert.Equal(t, payer, accounts[0].PublicKey) - assert.True(t, accounts[0].IsSigner) - assert.True(t, accounts[0].IsWritable) - - // ATA (writable, derived deterministically) - ataProgramID := solana.MustPublicKeyFromBase58("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") - expectedATA, _, _ := solana.FindProgramAddress( - [][]byte{owner.Bytes(), solana.TokenProgramID.Bytes(), mint.Bytes()}, - ataProgramID, - ) - assert.Equal(t, expectedATA, accounts[1].PublicKey) - assert.True(t, accounts[1].IsWritable) - assert.False(t, accounts[1].IsSigner) - - // owner - assert.Equal(t, owner, accounts[2].PublicKey) - assert.False(t, accounts[2].IsWritable) - - // mint - assert.Equal(t, mint, accounts[3].PublicKey) - assert.False(t, accounts[3].IsWritable) - - // system program - assert.Equal(t, solana.SystemProgramID, accounts[4].PublicKey) - - // token program - assert.Equal(t, solana.TokenProgramID, accounts[5].PublicKey) - }) - - t.Run("instruction data is [1] for CreateIdempotent", func(t *testing.T) { - data, err := ix.Data() - require.NoError(t, err) - assert.Equal(t, []byte{1}, data) - }) -} - -// ============================================================================= -// Ref-Finalize Route Tests -// ============================================================================= - func TestDeriveStoredIxDataPDA(t *testing.T) { builder := newTestBuilder(t) subTxID := makeTxID(0xAB) @@ -2335,7 +2354,7 @@ func setupDevnetSimulation(t *testing.T) (*RPCClient, *TxBuilder) { // Skipped by default (CI never runs these). Run locally against devnet with // RUN_SVM_SIM=1 — the public RPC rate-limits, so use -p 1 or run tests singly. - if os.Getenv("RUN_SVM_SIM") == "" { + if os.Getenv("RUN_SVM_SIM") != "1" { t.Skip("skipping simulation tests; set RUN_SVM_SIM=1 to run against devnet") } t.Helper() @@ -2573,7 +2592,7 @@ func buildAndSimulateRescue(t *testing.T, rpcClient *RPCClient, builder *TxBuild require.NoError(t, err) copy(sender[:], senderBytes) - isNative := assetAddr == "" + isNative := isNativeAsset(assetAddr) var token [32]byte var mintPubkey solana.PublicKey if !isNative { @@ -2637,14 +2656,7 @@ func buildAndSimulateRescue(t *testing.T, rpcClient *RPCClient, builder *TxBuild gatewayIx := solana.NewInstruction(builder.gatewayAddress, accounts, instructionData) computeLimitIx := builder.buildSetComputeUnitLimitInstruction(400000) - instructions := []solana.Instruction{computeLimitIx} - if !isNative { - createATAIx := builder.buildCreateATAIdempotentInstruction( - relayerKeypair.PublicKey(), recipientPubkey, mintPubkey, - ) - instructions = append(instructions, createATAIx) - } - instructions = append(instructions, gatewayIx) + instructions := []solana.Instruction{computeLimitIx, gatewayIx} recentBlockhash, err := rpcClient.GetRecentBlockhash(ctx) require.NoError(t, err) @@ -2972,6 +2984,261 @@ func TestSimulate_RefRoute_Execute(t *testing.T) { requireSimulationSuccess(t, storeSim) } +// Both encodings of native SOL reach the builder, verified against donut: +// withdrawals carry the EVM zero hex from the registry token address, reverts +// carry the base58 SystemProgram marker copied from the inbound. Missing either +// builds an SPL transfer whose ATA-create reverts, since neither is a mint. +func TestIsNativeAsset(t *testing.T) { + t.Run("core withdrawal form is native", func(t *testing.T) { + // create_outbound.go copies the registry token address verbatim. + assert.True(t, isNativeAsset("0x0000000000000000000000000000000000000000")) + }) + + t.Run("core revert form is native", func(t *testing.T) { + // build_revert_outbound.go copies inbound.AssetAddr, which the SVM parser + // sets from the pubkey, so native SOL arrives base58 encoded. + assert.True(t, isNativeAsset("11111111111111111111111111111111")) + assert.True(t, isNativeAsset(solana.SystemProgramID.String())) + assert.True(t, isNativeAsset(solana.PublicKey{}.String())) + }) + + t.Run("other zero spellings are native", func(t *testing.T) { + assert.True(t, isNativeAsset("")) + assert.True(t, isNativeAsset("0x0")) + assert.True(t, isNativeAsset("0x"+strings.Repeat("0", 64)), "hex-encoded zero pubkey") + }) + + // SPL mints are base58 in both directions, so they parse normally and must + // keep taking the token path. + t.Run("real SPL mints are not native", func(t *testing.T) { + assert.False(t, isNativeAsset("EiXDnrAg9ea2Q6vEPV7E5TpTU1vh41jcuZqKjU5Dc4ZF"), "USDT.sol") + assert.False(t, isNativeAsset("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"), "USDC.sol") + assert.False(t, isNativeAsset(solana.TokenProgramID.String())) + }) + + t.Run("malformed addresses are not native", func(t *testing.T) { + assert.False(t, isNativeAsset("not-base58-0OlI")) + assert.False(t, isNativeAsset("0x1234")) + }) +} + +// Core sent the base58 marker before switching to the EVM zero, so both forms +// are live: withdrawals carry the zero hex and reverts still carry base58. Both +// must build the identical native account layout, with no recipient ATA. +func TestNativeMarkerFormsBuildIdenticalAccounts(t *testing.T) { + builder := newTestBuilder(t) + + caller := solana.NewWallet().PublicKey() + config := solana.NewWallet().PublicKey() + vault := solana.NewWallet().PublicKey() + cea := solana.NewWallet().PublicKey() + tss := solana.NewWallet().PublicKey() + executed := solana.NewWallet().PublicKey() + recipient := solana.NewWallet().PublicKey() + + build := func(t *testing.T, assetAddr string) []*solana.AccountMeta { + t.Helper() + isNative := isNativeAsset(assetAddr) + require.True(t, isNative, "asset %q must classify as native", assetAddr) + return builder.buildWithdrawAndExecuteAccounts( + caller, config, vault, cea, tss, executed, + solana.SystemProgramID, + isNative, 1, + recipient, solana.PublicKey{}, + nil, + solana.PublicKey{}, solana.PublicKey{}, + ) + } + + withdrawForm := build(t, "0x0000000000000000000000000000000000000000") + revertForm := build(t, "11111111111111111111111111111111") + + assert.Equal(t, withdrawForm, revertForm, + "revert-form native SOL must build the same accounts as withdraw-form") + + // The old bug took the SPL path and derived an ATA for a non-mint. + ata, _, err := solana.FindAssociatedTokenAddress(recipient, solana.SystemProgramID) + require.NoError(t, err) + for _, acc := range revertForm { + assert.NotEqual(t, ata, acc.PublicKey, "native layout must not include a recipient ATA") + } +} + +// --------------------------------------------------------------------------- +// VerifyBroadcastedTx +// --------------------------------------------------------------------------- + +const testVerifySignature = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW" + +// newVerifyRPCBuilder drives the real RPCClient against a local JSON-RPC server. +// getHealth must answer for NewRPCClient to keep the endpoint; the genesis hash +// check is skipped by passing an empty expected hash. +func newVerifyRPCBuilder(t *testing.T, respond func(method string, w http.ResponseWriter)) *TxBuilder { + t.Helper() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + w.Header().Set("Content-Type", "application/json") + switch { + case strings.Contains(string(body), `"getHealth"`): + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"ok"}`)) + case strings.Contains(string(body), `"getTransaction"`): + respond("getTransaction", w) + case strings.Contains(string(body), `"getSlot"`): + respond("getSlot", w) + default: + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":null}`)) + } + })) + t.Cleanup(server.Close) + + rpcClient, err := NewRPCClient([]string{server.URL}, "", zerolog.Nop()) + require.NoError(t, err) + + return &TxBuilder{rpcClient: rpcClient, chainID: "solana:test", logger: zerolog.Nop()} +} + +// solana-go collapses both cases onto the error return: a genuinely absent tx +// comes back as ErrNotFound, everything else is a real RPC failure. Only the +// first is a verdict; treating the second as one lets the resolver vote failure +// against a tx that already executed. +func TestVerifyBroadcastedTx_NotFoundVersusRPCFailure(t *testing.T) { + t.Run("absent tx is a verdict, not an error", func(t *testing.T) { + tb := newVerifyRPCBuilder(t, func(_ string, w http.ResponseWriter) { + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":null}`)) + }) + + found, _, _, _, err := tb.VerifyBroadcastedTx(context.Background(), testVerifySignature) + require.NoError(t, err, "an absent tx must resolve, not retry forever") + assert.False(t, found) + }) + + t.Run("rpc failure surfaces as an error", func(t *testing.T) { + tb := newVerifyRPCBuilder(t, func(_ string, w http.ResponseWriter) { + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"error":{"code":-32005,"message":"rate limited"}}`)) + }) + + found, _, _, _, err := tb.VerifyBroadcastedTx(context.Background(), testVerifySignature) + require.Error(t, err, "an unreachable chain must not be reported as a verdict") + assert.False(t, found) + }) + + t.Run("malformed signature is a verdict", func(t *testing.T) { + tb := newVerifyRPCBuilder(t, func(_ string, w http.ResponseWriter) { + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":null}`)) + }) + + found, _, _, _, err := tb.VerifyBroadcastedTx(context.Background(), "not-a-signature") + require.NoError(t, err) + assert.False(t, found) + }) +} + +// The gateway creates the recipient ATA and meters the rent. Creating it here +// makes the gateway see it already present, leaving the rent outside gas_used +// (F-2026-18815). +func TestBuildOutboundTransaction_NoClientSideATACreate(t *testing.T) { + ataProgram := solana.MustPublicKeyFromBase58("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") + + for _, tc := range []struct { + name string + txType string + spl bool + }{ + {"SPL withdraw", "FUNDS", true}, + {"SPL revert", "INBOUND_REVERT", true}, + {"SPL rescue", "RESCUE_FUNDS", true}, + {"native withdraw", "FUNDS", false}, + {"native revert", "INBOUND_REVERT", false}, + {"native rescue", "RESCUE_FUNDS", false}, + } { + t.Run(tc.name, func(t *testing.T) { + builder := newBlockhashOnlyBuilder(t) + recipient := solana.NewWallet().PublicKey() + mint := solana.NewWallet().PublicKey() + assetAddr := "" + if tc.spl { + assetAddr = mint.String() + } + data := &uetypes.OutboundCreatedEvent{ + TxID: "0x" + strings.Repeat("11", 32), + UniversalTxId: "0x" + strings.Repeat("22", 32), + DestinationChain: "solana:devnet", + Sender: "0x" + strings.Repeat("33", 20), + Recipient: recipient.String(), + Amount: "1000", + AssetAddr: assetAddr, + GasLimit: "400000", + GasFee: "3000000", + TxType: tc.txType, + SigningDeadline: time.Now().Unix() + 600, + } + req := &common.UnsignedSigningReq{SigningHash: make([]byte, 32), Nonce: 0} + + tx, instructionID, err := builder.BuildOutboundTransaction(context.Background(), req, data, make([]byte, 65)) + require.NoError(t, err) + require.NotNil(t, tx) + + require.Len(t, tx.Message.Instructions, 2, "compute limit and the gateway call only") + for i := range tx.Message.Instructions { + program, err := tx.Message.Program(tx.Message.Instructions[i].ProgramIDIndex) + require.NoError(t, err) + assert.NotEqual(t, ataProgram, program, "instruction %d creates an ATA", i) + } + + // Revert and rescue still hand the gateway what it needs to create it. + if tc.spl && (instructionID == 3 || instructionID == 4) { + gatewayIx := tx.Message.Instructions[len(tx.Message.Instructions)-1] + metas, err := gatewayIx.ResolveInstructionAccounts(&tx.Message) + require.NoError(t, err) + require.Len(t, metas, 14) + + wantATA, _, err := solana.FindAssociatedTokenAddress(recipient, mint) + require.NoError(t, err) + assert.Equal(t, wantATA, metas[9].PublicKey, "recipient_token_account") + assert.Equal(t, solana.SPLAssociatedTokenAccountProgramID, metas[12].PublicKey, "associated_token_program") + assert.Equal(t, solana.SysVarRentPubkey, metas[13].PublicKey, "rent") + } + }) + } +} + +// newBlockhashOnlyBuilder answers the single RPC BuildOutboundTransaction makes. +// No ALTs are configured, so address-table lookup short-circuits offline. +func newBlockhashOnlyBuilder(t *testing.T) *TxBuilder { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + w.Header().Set("Content-Type", "application/json") + switch { + case strings.Contains(string(body), `"getHealth"`): + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"ok"}`)) + case strings.Contains(string(body), `"getLatestBlockhash"`): + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{"context":{"slot":1},` + + `"value":{"blockhash":"9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM","lastValidBlockHeight":100}}}`)) + case strings.Contains(string(body), `"getAccountInfo"`): + // Absent account: the PC20 probe reads this as "not a PC20 asset". + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{"context":{"slot":1},"value":null}}`)) + default: + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":null}`)) + } + })) + t.Cleanup(server.Close) + + rpcClient, err := NewRPCClient([]string{server.URL}, "", zerolog.Nop()) + require.NoError(t, err) + t.Cleanup(func() { rpcClient.Close() }) + + tmpDir := t.TempDir() + relayerDir := filepath.Join(tmpDir, "relayer") + require.NoError(t, os.MkdirAll(relayerDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(relayerDir, "solana.json"), []byte(testSolanaKeypairJSON), 0o600)) + + builder, err := NewTxBuilder(rpcClient, "solana:devnet", testGatewayAddress, tmpDir, zerolog.Nop(), nil) + require.NoError(t, err) + return builder +} + // ============================================================================= // Frozen vectors against the devnet dummy gateway program // (contracts/svm-gateway/app/gateway-test.ts §17 — devnetGatewayAddress) @@ -2986,6 +3253,452 @@ func newDummyGatewayBuilder(t *testing.T) *TxBuilder { return builder } +// --------------------------------------------------------------------------- +// Empty-recipient parking sentinel (F-2026-18184) +// +// The PC gateway documents bytes("") as "park funds in the caller's CEA". +// Core hex-encodes the raw event bytes unconditionally, so that reaches the +// builder as the string "0x". The builder used to reject it pre-sign, which +// stranded the outbound PENDING forever with the PRC20 already burned. +// --------------------------------------------------------------------------- + +// parkedCEA returns the CEA PDA the gateway derives for sender — the address a +// parked recipient must resolve to. Derived independently of resolveRecipient +// so the test pins the seed rather than the implementation. +func parkedCEA(t *testing.T, gateway solana.PublicKey, sender [20]byte) solana.PublicKey { + t.Helper() + pda, _, err := solana.FindProgramAddress([][]byte{[]byte("push_identity"), sender[:]}, gateway) + require.NoError(t, err) + return pda +} + +func TestIsParkedRecipient(t *testing.T) { + tests := []struct { + name string + recipient string + want bool + }{ + {"core's encoding of bytes(\"\")", "0x", true}, + {"bare empty string", "", true}, + {"uppercase prefix", "0X", true}, + {"surrounding whitespace", " 0x ", true}, + {"one zero byte is a real value, not the sentinel", "0x00", false}, + {"32 zero bytes is a real pubkey, not the sentinel", "0x" + strings.Repeat("00", 32), false}, + {"base58 pubkey", testGatewayAddress, false}, + {"32-byte hex pubkey", "0x" + strings.Repeat("ab", 32), false}, + {"garbage", "not-an-address", false}, + {"lone 0", "0", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, isParkedRecipient(tt.recipient)) + }) + } +} + +func TestResolveRecipient(t *testing.T) { + builder := newTestBuilder(t) + sender := makeSender(0xCC) + expectedCEA := parkedCEA(t, builder.gatewayAddress, sender) + + t.Run("sentinel resolves to the sender's CEA PDA", func(t *testing.T) { + pubkey, parked, err := builder.resolveRecipient("0x", sender) + require.NoError(t, err) + assert.True(t, parked) + assert.Equal(t, expectedCEA, pubkey) + }) + + t.Run("CEA is per-sender", func(t *testing.T) { + other, _, err := builder.resolveRecipient("0x", makeSender(0xDD)) + require.NoError(t, err) + assert.NotEqual(t, expectedCEA, other) + }) + + t.Run("base58 recipient parses unchanged", func(t *testing.T) { + wallet := solana.NewWallet().PublicKey() + pubkey, parked, err := builder.resolveRecipient(wallet.String(), sender) + require.NoError(t, err) + assert.False(t, parked) + assert.Equal(t, wallet, pubkey) + }) + + t.Run("32-byte hex recipient parses unchanged", func(t *testing.T) { + wallet := solana.NewWallet().PublicKey() + pubkey, parked, err := builder.resolveRecipient("0x"+hex.EncodeToString(wallet.Bytes()), sender) + require.NoError(t, err) + assert.False(t, parked) + assert.Equal(t, wallet, pubkey) + }) + + t.Run("malformed recipient still errors", func(t *testing.T) { + _, parked, err := builder.resolveRecipient("not-an-address", sender) + assert.False(t, parked, "a real parse failure must not be mistaken for parking") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid recipient address format") + }) + + t.Run("short hex is not 32 bytes and still errors", func(t *testing.T) { + _, _, err := builder.resolveRecipient("0xdeadbeef", sender) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid recipient address format") + }) +} + +func TestCheckParkedRecipientScope(t *testing.T) { + t.Run("parking is accepted on withdraw only", func(t *testing.T) { + require.NoError(t, checkParkedRecipientScope(true, 1)) + for _, id := range []uint8{0, 2, 3, 4} { + err := checkParkedRecipientScope(true, id) + require.Error(t, err, "instruction_id=%d", id) + assert.Contains(t, err.Error(), "only valid for withdraw") + } + }) + + t.Run("a real recipient is never scoped", func(t *testing.T) { + for _, id := range []uint8{0, 1, 2, 3, 4} { + require.NoError(t, checkParkedRecipientScope(false, id), "instruction_id=%d", id) + } + }) +} + +// newParkingRPCBuilder drives the real RPCClient against a local JSON-RPC +// server answering the two calls the outbound path makes: getAccountInfo (the +// TSS PDA, for the chain id bound into the signed message) and +// getLatestBlockhash (transaction assembly). The relayer keypair is written to +// disk so BuildOutboundTransaction can sign. +func newParkingRPCBuilder(t *testing.T, tssChainID string) *TxBuilder { + t.Helper() + + tssData := buildMockTSSPDAData([20]byte{}, tssChainID, 255) + accountInfoResp := fmt.Sprintf( + `{"jsonrpc":"2.0","id":1,"result":{"context":{"slot":1},"value":{"data":["%s","base64"],"executable":false,"lamports":1,"owner":"11111111111111111111111111111111","rentEpoch":0,"space":%d}}}`, + base64.StdEncoding.EncodeToString(tssData), len(tssData), + ) + blockhash := makeTxID(0x42) + blockhashResp := fmt.Sprintf( + `{"jsonrpc":"2.0","id":1,"result":{"context":{"slot":1},"value":{"blockhash":"%s","lastValidBlockHeight":100}}}`, + solana.Hash(blockhash).String(), + ) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + w.Header().Set("Content-Type", "application/json") + switch { + case strings.Contains(string(body), `"getHealth"`): + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"ok"}`)) + case strings.Contains(string(body), `"getAccountInfo"`): + _, _ = w.Write([]byte(accountInfoResp)) + case strings.Contains(string(body), `"getLatestBlockhash"`): + _, _ = w.Write([]byte(blockhashResp)) + default: + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":null}`)) + } + })) + t.Cleanup(server.Close) + + rpcClient, err := NewRPCClient([]string{server.URL}, "", zerolog.Nop()) + require.NoError(t, err) + + tmpDir := t.TempDir() + relayerDir := filepath.Join(tmpDir, "relayer") + require.NoError(t, os.MkdirAll(relayerDir, 0o755)) + require.NoError(t, os.WriteFile( + filepath.Join(relayerDir, "solana.json"), + []byte(testSolanaKeypairJSON), + 0o600, + )) + + builder, err := NewTxBuilder(rpcClient, "solana:devnet", testGatewayAddress, tmpDir, zerolog.Nop(), nil) + require.NoError(t, err) + return builder +} + +// parkingTestSender is the 20-byte EVM sender used by the outbound fixtures. +var parkingTestSender = makeSender(0x7E) + +// newWithdrawEvent is the happy withdraw template: native SOL, positive amount, +// empty payload — the exact shape a PRC20 withdraw to Solana produces +// (_fetchTxType: no payload + funds => TX_TYPE.FUNDS => instruction_id 1). +func newWithdrawEvent(recipient string) *uetypes.OutboundCreatedEvent { + txID := makeTxID(0xA1) + utxID := makeTxID(0xB2) + return &uetypes.OutboundCreatedEvent{ + TxID: "0x" + hex.EncodeToString(txID[:]), + UniversalTxId: "0x" + hex.EncodeToString(utxID[:]), + DestinationChain: "solana:devnet", + Sender: "0x" + hex.EncodeToString(parkingTestSender[:]), + Recipient: recipient, + Amount: "1000000", + AssetAddr: "0x0000000000000000000000000000000000000000", + GasFee: "5000", + SigningDeadline: 1735689600, + TxType: "FUNDS", + } +} + +// expectedWithdrawHash is the TSS message hash for the withdraw template with +// target as the bound target_program. For withdraw with an empty payload the +// builder copies the recipient into target_program, so this pins which pubkey +// the signature commits to. +func expectedWithdrawHash(t *testing.T, tb *TxBuilder, tssChainID string, target solana.PublicKey) []byte { + t.Helper() + txID := makeTxID(0xA1) + utxID := makeTxID(0xB2) + var targetProgram [32]byte + copy(targetProgram[:], target.Bytes()) + + hash, err := tb.constructTSSMessage( + 1, tssChainID, 1735689600, 1000000, + txID, utxID, parkingTestSender, [32]byte{}, 5000, + targetProgram, nil, nil, + [32]byte{}, [32]byte{}, nil, + ) + require.NoError(t, err) + return hash +} + +func TestGetOutboundSigningRequest_ParkedRecipient(t *testing.T) { + const tssChainID = "devnet" + builder := newParkingRPCBuilder(t, tssChainID) + ctx := context.Background() + cea := parkedCEA(t, builder.gatewayAddress, parkingTestSender) + + t.Run("sentinel signs against the sender's CEA", func(t *testing.T) { + req, err := builder.GetOutboundSigningRequest(ctx, newWithdrawEvent("0x"), 0) + require.NoError(t, err, "a parked recipient must produce a signing request, not strand the outbound") + require.NotNil(t, req) + assert.Equal(t, + expectedWithdrawHash(t, builder, tssChainID, cea), + req.SigningHash, + "signed target_program must be the CEA PDA for [\"push_identity\", sender]", + ) + }) + + t.Run("base58 recipient is unaffected", func(t *testing.T) { + wallet := solana.NewWallet().PublicKey() + req, err := builder.GetOutboundSigningRequest(ctx, newWithdrawEvent(wallet.String()), 0) + require.NoError(t, err) + assert.Equal(t, expectedWithdrawHash(t, builder, tssChainID, wallet), req.SigningHash) + assert.NotEqual(t, expectedWithdrawHash(t, builder, tssChainID, cea), req.SigningHash) + }) + + t.Run("32-byte hex recipient is unaffected", func(t *testing.T) { + wallet := solana.NewWallet().PublicKey() + ev := newWithdrawEvent("0x" + hex.EncodeToString(wallet.Bytes())) + req, err := builder.GetOutboundSigningRequest(ctx, ev, 0) + require.NoError(t, err) + assert.Equal(t, expectedWithdrawHash(t, builder, tssChainID, wallet), req.SigningHash) + }) + + t.Run("malformed recipient still errors", func(t *testing.T) { + _, err := builder.GetOutboundSigningRequest(ctx, newWithdrawEvent("not-an-address"), 0) + require.Error(t, err, "the sentinel must not become a catch-all that swallows real parse failures") + assert.Contains(t, err.Error(), "invalid recipient address format") + }) +} + +// Parking is a withdraw-only convention. On execute the recipient is the +// program to CPI into, and on revert/rescue it is an observed source-chain +// address that can never be the sentinel — so an empty recipient there is a +// malformed outbound and must stay an error. +func TestGetOutboundSigningRequest_ParkedRecipientScopedToWithdraw(t *testing.T) { + const tssChainID = "devnet" + builder := newParkingRPCBuilder(t, tssChainID) + ctx := context.Background() + + t.Run("execute (id=2) rejects the sentinel", func(t *testing.T) { + ev := newWithdrawEvent("0x") + ev.TxType = "FUNDS_AND_PAYLOAD" + ev.Payload = buildExecutePayloadForTest(t, []GatewayAccountMeta{}, []byte{0xDE, 0xAD}, 2, makeTxID(0x99)) + _, err := builder.GetOutboundSigningRequest(ctx, ev, 0) + require.Error(t, err) + assert.Contains(t, err.Error(), "only valid for withdraw") + }) + + t.Run("revert (id=3) rejects the sentinel", func(t *testing.T) { + ev := newWithdrawEvent("0x") + ev.TxType = "INBOUND_REVERT" + _, err := builder.GetOutboundSigningRequest(ctx, ev, 0) + require.Error(t, err) + assert.Contains(t, err.Error(), "only valid for withdraw") + }) + + t.Run("rescue (id=4) rejects the sentinel", func(t *testing.T) { + ev := newWithdrawEvent("0x") + ev.TxType = "RESCUE_FUNDS" + _, err := builder.GetOutboundSigningRequest(ctx, ev, 0) + require.Error(t, err) + assert.Contains(t, err.Error(), "only valid for withdraw") + }) + + t.Run("withdraw payload (id=1) accepts the sentinel", func(t *testing.T) { + ev := newWithdrawEvent("0x") + ev.Payload = buildExecutePayloadForTest(t, []GatewayAccountMeta{}, nil, 1, [32]byte{}) + _, err := builder.GetOutboundSigningRequest(ctx, ev, 0) + require.NoError(t, err) + }) + + t.Run("ref route rejects the sentinel — execute-only", func(t *testing.T) { + ev := newWithdrawEvent("0x") + ev.TxType = "FUNDS_AND_PAYLOAD" + ev.Payload = buildExecutePayloadForTest(t, []GatewayAccountMeta{}, []byte{0xDE, 0xAD}, 2, makeTxID(0x99)) + _, _, _, err := builder.BuildRefRouteTransactions( + ctx, + &common.UnsignedSigningReq{SigningHash: make([]byte, 32)}, + ev, + make([]byte, 65), + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "only valid for withdraw") + }) +} + +// BuildOutboundTransaction re-parses the event independently of +// GetOutboundSigningRequest. If the two disagreed the accounts list would not +// match the pubkey already bound into the TSS signature, so the built tx must +// carry the same CEA. +func TestBuildOutboundTransaction_ParkedRecipient(t *testing.T) { + const tssChainID = "devnet" + builder := newParkingRPCBuilder(t, tssChainID) + ctx := context.Background() + cea := parkedCEA(t, builder.gatewayAddress, parkingTestSender) + + req, err := builder.GetOutboundSigningRequest(ctx, newWithdrawEvent("0x"), 0) + require.NoError(t, err) + + tx, instructionID, err := builder.BuildOutboundTransaction(ctx, req, newWithdrawEvent("0x"), make([]byte, 65)) + require.NoError(t, err, "a parked recipient must build a broadcastable tx") + require.NotNil(t, tx) + assert.Equal(t, uint8(1), instructionID) + + // Account #4 is cea_authority and #9 is the withdraw recipient (native SOL + // layout). Parking makes them the same PDA — that is what tells the gateway + // to leave the funds where finalize already staged them. + gatewayIx := tx.Message.Instructions[len(tx.Message.Instructions)-1] + metas, err := gatewayIx.ResolveInstructionAccounts(&tx.Message) + require.NoError(t, err) + require.GreaterOrEqual(t, len(metas), 9) + assert.Equal(t, cea, metas[3].PublicKey, "cea_authority slot") + assert.Equal(t, cea, metas[8].PublicKey, "recipient slot must be the CEA when parked") +} + +// The parked path must be structurally valid against the deployed gateway, not +// just against our own mocks. The devnet TSS PDA holds a real signer, so a test +// signature can never pass tss.rs. That still pins what we need: Anchor +// validates every account constraint before the handler runs, so reaching +// TssAuthFailed proves the program accepted our cea_authority derivation +// against its own `seeds = [CEA_SEED, push_account]`. A wrong PDA would fail +// earlier, with ConstraintSeeds, and never reach the signature check. +func TestSimulate_Withdraw_ParkedRecipient(t *testing.T) { + rpcClient, builder := setupDevnetSimulation(t) + defer rpcClient.Close() + + withdrawPayload := "0x" + hex.EncodeToString(buildMockWithdrawPayload()) + + stageOf := func(recipient string) string { + data, evmKey := newDevnetOutbound(t, "1000000", "", withdrawPayload, "", "FUNDS") + if recipient != "" { + data.Recipient = recipient + } + result, err := buildAndSimulate(t, rpcClient, builder, data, evmKey) + require.NoError(t, err, "the parked recipient must build and reach the cluster") + require.NotNil(t, result) + return fmt.Sprintf("%v", result.Err) + } + + parked := stageOf("0x") + normal := stageOf("") + t.Logf("parked on-chain result: %s", parked) + t.Logf("normal on-chain result: %s", normal) + + assert.Equal(t, normal, parked, + "a parked withdraw must reach the same on-chain stage as an ordinary one") + assert.NotContains(t, parked, "2006", "ConstraintSeeds means the CEA derivation disagrees with the gateway") +} + +// legacyResolveRecipient is the parsing that ran at both call sites before the +// sentinel was introduced, kept verbatim as an oracle. Every recipient that +// works today must resolve through the new path to the same pubkey. +func legacyResolveRecipient(recipient string) (solana.PublicKey, error) { + pk, err := solana.PublicKeyFromBase58(recipient) + if err != nil { + hexBytes, hexErr := hex.DecodeString(removeHexPrefix(recipient)) + if hexErr != nil || len(hexBytes) != 32 { + return solana.PublicKey{}, fmt.Errorf("invalid recipient address format (expected Solana Pubkey): %s", recipient) + } + pk = solana.PublicKeyFromBytes(hexBytes) + } + return pk, nil +} + +func TestResolveRecipient_MatchesPreSentinelBehaviour(t *testing.T) { + builder := newParkingRPCBuilder(t, "devnet") + wallet := solana.NewWallet().PublicKey() + + cases := []string{ + wallet.String(), + "AdWDRaQfvWJqW4TaxTrXP5WogCWJMJBrtBfGjjHUDADM", + "11111111111111111111111111111111", + solana.SystemProgramID.String(), + "0x" + hex.EncodeToString(wallet.Bytes()), + hex.EncodeToString(wallet.Bytes()), + "0x" + hex.EncodeToString(make([]byte, 32)), + "not-an-address", + "0xdeadbeef", + "0x" + hex.EncodeToString(make([]byte, 20)), + } + + for _, recipient := range cases { + t.Run(recipient, func(t *testing.T) { + want, wantErr := legacyResolveRecipient(recipient) + got, parked, gotErr := builder.resolveRecipient(recipient, parkingTestSender) + + assert.False(t, parked, "a non-empty recipient must never be treated as the sentinel") + if wantErr != nil { + require.Error(t, gotErr, "an input rejected before must still be rejected") + assert.Equal(t, wantErr.Error(), gotErr.Error()) + return + } + require.NoError(t, gotErr, "an input accepted before must still be accepted") + assert.Equal(t, want, got, "resolved pubkey drifted from pre-sentinel behaviour") + }) + } +} + +// Frozen hashes. The signing hash is what the TSS signs and what the gateway +// re-derives, so a change here is a consensus break, not a test update. +func TestGetOutboundSigningRequest_GoldenHashes(t *testing.T) { + builder := newParkingRPCBuilder(t, "devnet") + ctx := context.Background() + + t.Run("ordinary base58 recipient", func(t *testing.T) { + req, err := builder.GetOutboundSigningRequest(ctx, newWithdrawEvent("AdWDRaQfvWJqW4TaxTrXP5WogCWJMJBrtBfGjjHUDADM"), 0) + require.NoError(t, err) + assert.Equal(t, + "ba5378d8db7d82a64e3b5770845cc06356974cc020618ae2ab65cd2d027b5f5c", + hex.EncodeToString(req.SigningHash), + ) + }) + + t.Run("parked recipient", func(t *testing.T) { + req, err := builder.GetOutboundSigningRequest(ctx, newWithdrawEvent("0x"), 0) + require.NoError(t, err) + assert.Equal(t, + "c2ef605c5e3ce32a8c6b7f3db4898741c0fbeef464660970a2487191f55e4403", + hex.EncodeToString(req.SigningHash), + ) + }) +} + +// The scope check runs on every instruction id, so it has to be invisible to +// outbounds carrying a real recipient. +func TestCheckParkedRecipientScope_AllowsEveryInstructionWhenNotParked(t *testing.T) { + for id := uint8(0); id <= 5; id++ { + require.NoError(t, checkParkedRecipientScope(false, id), "instruction_id=%d", id) + } +} + func TestPC20DummyGateway_PDAGoldens(t *testing.T) { tb := newDummyGatewayBuilder(t) sourceAsset := makeSender(0x44) diff --git a/universalClient/pushcore/pushCore.go b/universalClient/pushcore/pushCore.go index 600c48365..0115fbf7f 100644 --- a/universalClient/pushcore/pushCore.go +++ b/universalClient/pushcore/pushCore.go @@ -23,8 +23,10 @@ import ( uvalidatortypes "github.com/pushchain/push-chain-node/x/uvalidator/types" "github.com/rs/zerolog" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" ) // Client is a fan-out client that connects to multiple Push Chain gRPC endpoints. @@ -143,19 +145,45 @@ func retryWithRoundRobin[T any]( // GetAllChainConfigs retrieves all chain configurations from Push Chain. func (c *Client) GetAllChainConfigs(ctx context.Context) ([]*uregistrytypes.ChainConfig, error) { - return retryWithRoundRobin( - len(c.eps), - &c.rr, - func(idx int) ([]*uregistrytypes.ChainConfig, error) { - resp, err := c.eps[idx].AllChainConfigs(ctx, &uregistrytypes.QueryAllChainConfigsRequest{}) - if err != nil { - return nil, err - } - return resp.Configs, nil - }, - "GetAllChainConfigs", - c.logger, + // Paged rather than a single request: the server paginates this collection, + // and an omitted PageRequest silently caps the response at the SDK default of + // 100. A chain missing from this list is simply never watched, so truncation + // must not be possible. + var ( + configs []*uregistrytypes.ChainConfig + nextKey []byte ) + for page := 0; page < chainConfigMaxPages; page++ { + key := nextKey + resp, err := retryWithRoundRobin( + len(c.eps), + &c.rr, + func(idx int) (*uregistrytypes.QueryAllChainConfigsResponse, error) { + return c.eps[idx].AllChainConfigs(ctx, &uregistrytypes.QueryAllChainConfigsRequest{ + Pagination: &query.PageRequest{Key: key, Limit: chainConfigPageSize}, + }) + }, + "GetAllChainConfigs", + c.logger, + ) + if err != nil { + return nil, err + } + + configs = append(configs, resp.Configs...) + + if resp.Pagination == nil || len(resp.Pagination.NextKey) == 0 { + return configs, nil + } + nextKey = resp.Pagination.NextKey + } + + // Unreachable with any plausible number of chains; loud rather than silent. + c.logger.Error(). + Int("max_pages", chainConfigMaxPages). + Int("fetched", len(configs)). + Msg("chain config page cap reached; some chains will not be watched") + return configs, nil } // GetLatestBlock retrieves the latest block from Push Chain. @@ -215,6 +243,27 @@ func (c *Client) GetCurrentKey(ctx context.Context) (*utsstypes.TssKey, error) { ) } +// GetKeyByID retrieves a single TSS key from the on-chain key history. +// Returns an error if the key ID is not in the history. +func (c *Client) GetKeyByID(ctx context.Context, keyID string) (*utsstypes.TssKey, error) { + return retryWithRoundRobin( + len(c.utssClients), + &c.rr, + func(idx int) (*utsstypes.TssKey, error) { + resp, err := c.utssClients[idx].KeyById(ctx, &utsstypes.QueryKeyByIdRequest{KeyId: keyID}) + if err != nil { + return nil, err + } + if resp == nil || resp.Key == nil { + return nil, fmt.Errorf("pushcore: TSS key %s not found", keyID) + } + return resp.Key, nil + }, + "GetKeyByID", + c.logger, + ) +} + // GetGasPrice retrieves the median gas price for a specific chain from the on-chain oracle. func (c *Client) GetGasPrice(ctx context.Context, chainID string) (*big.Int, error) { if chainID == "" { @@ -351,24 +400,109 @@ func (c *Client) GetPendingFundMigrations(ctx context.Context) ([]*utsstypes.Fun ) } -// GetAllPendingOutbounds retrieves up to the first 1000 pending outbound transactions from Push Chain. -// Sorted by created_at (block height) ascending — oldest first. +// Page size and page cap for the pending-outbound walk. The cap bounds a single +// poll; anything beyond it is picked up on the next tick. +const ( + // A row costs roughly a kilobyte on the wire, so a page stays well inside + // gRPC's 4 MiB default. Asking for the whole set in one request would fail + // the call outright once the set grew, taking the poll down entirely. + pendingOutboundPageSize = 1000 + + // pendingOutboundMaxRows caps one poll. The remainder is read on the next + // tick. Counted in rows so a page that had to shrink costs extra requests + // rather than fewer rows. + pendingOutboundMaxRows = 5000 + + chainConfigPageSize = 200 + chainConfigMaxPages = 20 + + // maxPushCoreRecvMsgSize bounds a single gRPC response from a Push-core + // endpoint. grpc-go otherwise applies an implicit 4 MiB default that nobody + // chose and that is not tied to anything this client asks for; pinning it + // here makes the bound deliberate and keeps it from drifting with the + // library default. + // + // Sized off the largest poll: GetAllPendingOutbounds asks for + // pendingOutboundPageSize entries and gets the matching outbounds back, so + // 2 x 1000 rows in one response. A row costs roughly a kilobyte today, so a + // 4 KiB per-row budget leaves 4x headroom and lands on 8 MiB — above the + // 4 MiB the client has been running on, so no response that works today + // starts failing, and low enough that a hostile or broken endpoint cannot + // stream an unbounded body into the validator. + maxPushCoreRecvMsgSize = 8 * 1024 * 1024 +) + +// GetAllPendingOutbounds retrieves pending outbound transactions from Push Chain, +// oldest first, so older work is signed before newer. +// +// Walked by offset rather than read as a single page. An outbound leaves the +// pending set only when a quorum vote terminalizes it, so rows that cannot reach +// one accumulate at the head of the list; without the walk they would hide every +// newer outbound behind them, on every chain, since this query is not chain +// scoped. +// +// The walk stops at the first short page, so the ordinary case where the whole +// set fits in one page costs exactly one request. func (c *Client) GetAllPendingOutbounds(ctx context.Context) ([]*uexecutortypes.PendingOutboundEntry, []*uexecutortypes.OutboundTx, error) { - resp, err := retryWithRoundRobin( - len(c.uexecutorClients), - &c.rr, - func(idx int) (*uexecutortypes.QueryAllPendingOutboundsResponse, error) { - return c.uexecutorClients[idx].AllPendingOutbounds(ctx, &uexecutortypes.QueryAllPendingOutboundsRequest{ - Pagination: &query.PageRequest{Limit: 1000}, - }) - }, - "GetAllPendingOutbounds", - c.logger, + var ( + entries []*uexecutortypes.PendingOutboundEntry + outbounds []*uexecutortypes.OutboundTx ) - if err != nil { - return nil, nil, err + + // Halving on ResourceExhausted is what keeps one large row from blinding the + // whole poll. It terminates because core caps an outbound payload + // (MaxOutboundPayloadBytes) well below maxPushCoreRecvMsgSize, so a page of + // one always fits and no row is ever unfetchable. + var offset uint64 + limit := uint64(pendingOutboundPageSize) + + // The walk is bounded without counting requests: a served page adds at least + // one row or is short and ends the walk, so there are at most + // pendingOutboundMaxRows of them, and halving bottoms out at a page of one. + for uint64(len(entries)) < pendingOutboundMaxRows { + pageLimit := limit + if remaining := pendingOutboundMaxRows - uint64(len(entries)); pageLimit > remaining { + pageLimit = remaining + } + resp, err := retryWithRoundRobin( + len(c.uexecutorClients), + &c.rr, + func(idx int) (*uexecutortypes.QueryAllPendingOutboundsResponse, error) { + return c.uexecutorClients[idx].AllPendingOutbounds(ctx, &uexecutortypes.QueryAllPendingOutboundsRequest{ + Pagination: &query.PageRequest{Offset: offset, Limit: pageLimit}, + }) + }, + "GetAllPendingOutbounds", + c.logger, + ) + if err != nil { + if status.Code(err) != codes.ResourceExhausted || limit == 1 { + return nil, nil, err + } + limit /= 2 + c.logger.Warn(). + Uint64("offset", offset). + Uint64("was", pageLimit). + Uint64("now", limit). + Msg("pending outbound page exceeded the receive limit; halving the page") + continue + } + + entries = append(entries, resp.Entries...) + outbounds = append(outbounds, resp.Outbounds...) + + if uint64(len(resp.Entries)) < pageLimit { + return entries, outbounds, nil + } + offset += uint64(len(resp.Entries)) } - return resp.Entries, resp.Outbounds, nil + + c.logger.Warn(). + Int("max_rows", pendingOutboundMaxRows). + Int("fetched", len(entries)). + Msg("pending outbound row budget reached; the remainder is read on the next poll") + + return entries, outbounds, nil } // GetAllPendingReadRequests retrieves up to the first 1000 pending external read @@ -426,7 +560,9 @@ func createGRPCConnection(endpoint string) (*grpc.ClientConn, error) { } } - var opts []grpc.DialOption + opts := []grpc.DialOption{ + grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxPushCoreRecvMsgSize)), + } if useTLS { opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(nil))) } else { diff --git a/universalClient/pushcore/pushCore_test.go b/universalClient/pushcore/pushCore_test.go index 964540f75..5f5ebcebc 100644 --- a/universalClient/pushcore/pushCore_test.go +++ b/universalClient/pushcore/pushCore_test.go @@ -1,12 +1,17 @@ package pushcore import ( + "bytes" "context" + "errors" + "fmt" "math/big" "testing" + "time" cmtservice "github.com/cosmos/cosmos-sdk/client/grpc/cmtservice" sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" "github.com/cosmos/cosmos-sdk/types/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/cosmos/cosmos-sdk/x/authz" @@ -19,6 +24,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) func TestNew(t *testing.T) { @@ -984,9 +991,24 @@ type mockRegistryQueryClient struct { uregistrytypes.QueryClient allChainConfigsResp *uregistrytypes.QueryAllChainConfigsResponse err error + + chainConfigPages []*uregistrytypes.QueryAllChainConfigsResponse + chainConfigKeys [][]byte } func (m *mockRegistryQueryClient) AllChainConfigs(ctx context.Context, req *uregistrytypes.QueryAllChainConfigsRequest, opts ...grpc.CallOption) (*uregistrytypes.QueryAllChainConfigsResponse, error) { + if m.chainConfigPages != nil { + var key []byte + if req.Pagination != nil { + key = req.Pagination.Key + } + m.chainConfigKeys = append(m.chainConfigKeys, key) + idx := len(m.chainConfigKeys) - 1 + if idx >= len(m.chainConfigPages) { + return nil, assert.AnError + } + return m.chainConfigPages[idx], nil + } if m.err != nil { return nil, m.err } @@ -1034,6 +1056,7 @@ func (m *mockUValidatorQueryClient) UniversalValidator(ctx context.Context, req type mockUTSSQueryClient struct { utsstypes.QueryClient currentKeyResp *utsstypes.QueryCurrentKeyResponse + keyByIdResp *utsstypes.QueryKeyByIdResponse pendingTssEventsResp *utsstypes.QueryAllPendingTssEventsResponse pendingFundMigrationsResp *utsstypes.QueryPendingFundMigrationsResponse err error @@ -1061,7 +1084,10 @@ func (m *mockUTSSQueryClient) PendingFundMigrations(ctx context.Context, req *ut } func (m *mockUTSSQueryClient) KeyById(ctx context.Context, req *utsstypes.QueryKeyByIdRequest, opts ...grpc.CallOption) (*utsstypes.QueryKeyByIdResponse, error) { - return nil, nil + if m.err != nil { + return nil, m.err + } + return m.keyByIdResp, nil } type mockTxServiceClient struct { @@ -1091,6 +1117,14 @@ type mockUExecutorQueryClient struct { gasPriceResp *uexecutortypes.QueryGasPriceResponse allPendingOutboundsResp *uexecutortypes.QueryAllPendingOutboundsResponse err error + + // lastPendingReq records the request so tests can assert the limit sent. + lastPendingReq *uexecutortypes.QueryAllPendingOutboundsRequest + + // pendingReqs records every page request of a walk. + pendingReqs []*uexecutortypes.QueryAllPendingOutboundsRequest + // pendingTotal, when set, makes the mock serve that many rows by offset. + pendingTotal int } func (m *mockUExecutorQueryClient) GasPrice(ctx context.Context, req *uexecutortypes.QueryGasPriceRequest, opts ...grpc.CallOption) (*uexecutortypes.QueryGasPriceResponse, error) { @@ -1117,9 +1151,27 @@ func (m *mockUExecutorQueryClient) AllUniversalTx(ctx context.Context, req *uexe } func (m *mockUExecutorQueryClient) AllPendingOutbounds(ctx context.Context, req *uexecutortypes.QueryAllPendingOutboundsRequest, opts ...grpc.CallOption) (*uexecutortypes.QueryAllPendingOutboundsResponse, error) { + m.lastPendingReq = req + m.pendingReqs = append(m.pendingReqs, req) if m.err != nil { return nil, m.err } + if m.pendingTotal > 0 { + offset := int(req.Pagination.GetOffset()) + end := offset + int(req.Pagination.GetLimit()) + if end > m.pendingTotal { + end = m.pendingTotal + } + resp := &uexecutortypes.QueryAllPendingOutboundsResponse{ + Pagination: &query.PageResponse{Total: uint64(m.pendingTotal)}, + } + for i := offset; i < end; i++ { + id := fmt.Sprintf("ob-%d", i) + resp.Entries = append(resp.Entries, &uexecutortypes.PendingOutboundEntry{OutboundId: id}) + resp.Outbounds = append(resp.Outbounds, &uexecutortypes.OutboundTx{Id: id}) + } + return resp, nil + } return m.allPendingOutboundsResp, nil } @@ -1153,6 +1205,382 @@ func (m *mockAuthAccountQueryClient) Account(ctx context.Context, req *authtypes return m.accountResp, nil } +func TestClient_GetKeyByID(t *testing.T) { + logger := zerolog.Nop() + + t.Run("no endpoints configured", func(t *testing.T) { + client := &Client{logger: logger, utssClients: []utsstypes.QueryClient{}} + + key, err := client.GetKeyByID(context.Background(), "key-123") + require.Error(t, err) + assert.Contains(t, err.Error(), "no endpoints configured") + assert.Nil(t, key) + }) + + t.Run("successful query returns key", func(t *testing.T) { + mockClient := &mockUTSSQueryClient{ + keyByIdResp: &utsstypes.QueryKeyByIdResponse{ + Key: &utsstypes.TssKey{KeyId: "key-123", TssPubkey: "0xpub"}, + }, + } + client := &Client{logger: logger, utssClients: []utsstypes.QueryClient{mockClient}} + + key, err := client.GetKeyByID(context.Background(), "key-123") + require.NoError(t, err) + require.NotNil(t, key) + assert.Equal(t, "key-123", key.KeyId) + assert.Equal(t, "0xpub", key.TssPubkey) + }) + + t.Run("unknown key id errors", func(t *testing.T) { + mockClient := &mockUTSSQueryClient{ + keyByIdResp: &utsstypes.QueryKeyByIdResponse{Key: nil}, + } + client := &Client{logger: logger, utssClients: []utsstypes.QueryClient{mockClient}} + + key, err := client.GetKeyByID(context.Background(), "missing") + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") + assert.Nil(t, key) + }) + + // A nil response with a nil error must not panic. + t.Run("nil response errors", func(t *testing.T) { + mockClient := &mockUTSSQueryClient{keyByIdResp: nil} + client := &Client{logger: logger, utssClients: []utsstypes.QueryClient{mockClient}} + + key, err := client.GetKeyByID(context.Background(), "key-123") + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") + assert.Nil(t, key) + }) + + t.Run("query error propagates", func(t *testing.T) { + mockClient := &mockUTSSQueryClient{err: errors.New("rpc down")} + client := &Client{logger: logger, utssClients: []utsstypes.QueryClient{mockClient}} + + key, err := client.GetKeyByID(context.Background(), "key-123") + require.Error(t, err) + assert.Nil(t, key) + }) +} + +// An outbound leaves the pending set only on a quorum vote, so rows that cannot +// reach one accumulate at the head of an oldest-first list. The walk is what +// stops them hiding everything newer. +func TestClient_GetAllPendingOutbounds_WalksOldestFirst(t *testing.T) { + ctx := context.Background() + + newClient := func(total int) (*Client, *mockUExecutorQueryClient) { + m := &mockUExecutorQueryClient{pendingTotal: total} + return &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{m}}, m + } + + t.Run("oldest first, never reversed", func(t *testing.T) { + client, m := newClient(9) + _, _, err := client.GetAllPendingOutbounds(ctx) + require.NoError(t, err) + + p := m.pendingReqs[0].Pagination + require.NotNil(t, p) + assert.False(t, p.Reverse, "older outbounds must be read first") + assert.Zero(t, p.Offset) + assert.Equal(t, uint64(pendingOutboundPageSize), p.Limit) + }) + + // The ordinary case is a set that fits, and it must not cost extra requests. + t.Run("a set that fits costs one request", func(t *testing.T) { + client, m := newClient(9) + entries, _, err := client.GetAllPendingOutbounds(ctx) + require.NoError(t, err) + assert.Len(t, m.pendingReqs, 1) + assert.Len(t, entries, 9) + }) + + // A stuck prefix must not hide what is behind it. + t.Run("walks past a full first page", func(t *testing.T) { + client, m := newClient(pendingOutboundPageSize + 250) + entries, outbounds, err := client.GetAllPendingOutbounds(ctx) + require.NoError(t, err) + + require.Len(t, m.pendingReqs, 2) + assert.Equal(t, uint64(0), m.pendingReqs[0].Pagination.GetOffset()) + assert.Equal(t, uint64(pendingOutboundPageSize), m.pendingReqs[1].Pagination.GetOffset()) + + require.Len(t, entries, pendingOutboundPageSize+250) + require.Len(t, outbounds, pendingOutboundPageSize+250) + assert.Equal(t, "ob-0", entries[0].OutboundId, "oldest first") + assert.Equal(t, fmt.Sprintf("ob-%d", pendingOutboundPageSize+249), entries[len(entries)-1].OutboundId) + }) + + // The cap bounds one poll; the rest is read on the next tick. + t.Run("stops at the row budget and says so", func(t *testing.T) { + var logBuf bytes.Buffer + m := &mockUExecutorQueryClient{pendingTotal: pendingOutboundMaxRows + 2*pendingOutboundPageSize} + client := &Client{logger: zerolog.New(&logBuf), uexecutorClients: []uexecutortypes.QueryClient{m}} + + entries, _, err := client.GetAllPendingOutbounds(ctx) + require.NoError(t, err) + assert.Len(t, m.pendingReqs, pendingOutboundMaxRows/pendingOutboundPageSize) + assert.Len(t, entries, pendingOutboundMaxRows) + assert.Contains(t, logBuf.String(), "row budget reached") + }) + + t.Run("quiet when the set fits", func(t *testing.T) { + var logBuf bytes.Buffer + m := &mockUExecutorQueryClient{pendingTotal: 9} + client := &Client{logger: zerolog.New(&logBuf), uexecutorClients: []uexecutortypes.QueryClient{m}} + + _, _, err := client.GetAllPendingOutbounds(ctx) + require.NoError(t, err) + assert.NotContains(t, logBuf.String(), "page cap reached") + }) +} + +// oversizedPageClient serves rows by offset like the mock above, but rejects any +// page larger than maxServable with ResourceExhausted, the way grpc-go does when +// a response exceeds the receive limit. +type oversizedPageClient struct { + uexecutortypes.QueryClient + total int + maxServable uint64 + reqs []*query.PageRequest +} + +func (m *oversizedPageClient) AllPendingOutbounds(ctx context.Context, req *uexecutortypes.QueryAllPendingOutboundsRequest, opts ...grpc.CallOption) (*uexecutortypes.QueryAllPendingOutboundsResponse, error) { + m.reqs = append(m.reqs, req.Pagination) + if req.Pagination.GetLimit() > m.maxServable { + return nil, status.Error(codes.ResourceExhausted, + "grpc: received message larger than max") + } + offset := int(req.Pagination.GetOffset()) + end := offset + int(req.Pagination.GetLimit()) + if end > m.total { + end = m.total + } + resp := &uexecutortypes.QueryAllPendingOutboundsResponse{ + Pagination: &query.PageResponse{Total: uint64(m.total)}, + } + for i := offset; i < end; i++ { + id := fmt.Sprintf("ob-%d", i) + resp.Entries = append(resp.Entries, &uexecutortypes.PendingOutboundEntry{OutboundId: id}) + resp.Outbounds = append(resp.Outbounds, &uexecutortypes.OutboundTx{Id: id}) + } + return resp, nil +} + +// A page that will not fit must not blind the poll. The client halves until the +// response fits, which terminates because core caps an outbound payload well +// below the receive limit, so a page of one always fits. +func TestGetAllPendingOutbounds_HalvesOnResourceExhausted(t *testing.T) { + ctx := context.Background() + + t.Run("degrades and still returns every row oldest first", func(t *testing.T) { + m := &oversizedPageClient{total: 300, maxServable: 250} + client := &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{m}} + + entries, outbounds, err := client.GetAllPendingOutbounds(ctx) + require.NoError(t, err, "an oversized page must not fail the whole poll") + require.Len(t, entries, 300) + require.Len(t, outbounds, 300) + + assert.Equal(t, "ob-0", entries[0].OutboundId, "oldest first") + assert.Equal(t, "ob-299", entries[299].OutboundId) + + // 1000 rejected, then 500 rejected, then 250 serves. + require.GreaterOrEqual(t, len(m.reqs), 3) + assert.Equal(t, uint64(1000), m.reqs[0].Limit) + assert.Equal(t, uint64(500), m.reqs[1].Limit) + assert.Equal(t, uint64(250), m.reqs[2].Limit) + }) + + t.Run("offsets follow the rows actually returned", func(t *testing.T) { + m := &oversizedPageClient{total: 300, maxServable: 250} + client := &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{m}} + + _, _, err := client.GetAllPendingOutbounds(ctx) + require.NoError(t, err) + + // The second served page must start where the first ended, not at a + // multiple of the original page size. + var served []*query.PageRequest + for _, r := range m.reqs { + if r.Limit <= m.maxServable { + served = append(served, r) + } + } + require.GreaterOrEqual(t, len(served), 2) + assert.Equal(t, uint64(0), served[0].Offset) + assert.Equal(t, uint64(250), served[1].Offset, "no row may be skipped or read twice") + }) + + t.Run("a page of one that still fails is a real error", func(t *testing.T) { + m := &oversizedPageClient{total: 10, maxServable: 0} + client := &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{m}} + + // Bounded, because the failure mode here is a walk that never ends: if + // the floor guard let the page reach zero, every request would return no + // rows and the offset would stop moving. That must fail, not hang. + done := make(chan error, 1) + go func() { + _, _, err := client.GetAllPendingOutbounds(ctx) + done <- err + }() + + select { + case err := <-done: + require.Error(t, err, "nothing left to halve, so the caller must see it") + assert.Equal(t, codes.ResourceExhausted, status.Code(err)) + case <-time.After(5 * time.Second): + t.Fatal("the walk did not terminate; the page size floor is gone") + } + }) + + t.Run("an unrelated error is not retried smaller", func(t *testing.T) { + m := &mockUExecutorQueryClient{err: status.Error(codes.Unavailable, "endpoint down")} + client := &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{m}} + + _, _, err := client.GetAllPendingOutbounds(ctx) + require.Error(t, err) + assert.Len(t, m.pendingReqs, 1, "halving is only for a response that did not fit") + }) +} + +// A page that had to shrink must cost extra requests, not rows. Bounding the +// walk by iterations instead would quietly cut a degraded poll from 5000 rows to +// five times whatever the page shrank to. +func TestGetAllPendingOutbounds_RowBudgetSurvivesDegradedPages(t *testing.T) { + ctx := context.Background() + + full := &oversizedPageClient{total: 20000, maxServable: pendingOutboundPageSize} + fullClient := &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{full}} + fullEntries, _, err := fullClient.GetAllPendingOutbounds(ctx) + require.NoError(t, err) + + degraded := &oversizedPageClient{total: 20000, maxServable: 250} + degradedClient := &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{degraded}} + degradedEntries, _, err := degradedClient.GetAllPendingOutbounds(ctx) + require.NoError(t, err) + + assert.Len(t, fullEntries, pendingOutboundMaxRows) + assert.Len(t, degradedEntries, pendingOutboundMaxRows, + "a shrunken page must not shrink the poll") + assert.Greater(t, len(degraded.reqs), len(full.reqs), + "the cost of degrading is requests, not rows") + + // Same rows, same order, whatever the page size was. + require.Equal(t, fullEntries[0].OutboundId, degradedEntries[0].OutboundId) + require.Equal(t, fullEntries[len(fullEntries)-1].OutboundId, degradedEntries[len(degradedEntries)-1].OutboundId) +} + +// Every payload sitting at the cap is the worst page the client can meet: 8 MiB +// over a 128 KiB row is 64 rows, so the page settles at 62 and the poll costs +// more than 80 requests. It must still read the full budget rather than stop at +// some request count. +func TestGetAllPendingOutbounds_WorstCaseRowSizeStillReadsTheBudget(t *testing.T) { + m := &oversizedPageClient{total: 20000, maxServable: 64} + client := &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{m}} + + entries, _, err := client.GetAllPendingOutbounds(context.Background()) + require.NoError(t, err) + require.Len(t, entries, pendingOutboundMaxRows, "a small page must not shrink the poll") + assert.Greater(t, len(m.reqs), 64, "this case genuinely needs more than 64 requests") + + for i, e := range entries { + require.Equal(t, fmt.Sprintf("ob-%d", i), e.OutboundId, "row %d out of order", i) + } +} + +// The cap is only useful if the rows under it are the right ones. Asserting the +// count alone would pass on a walk that skipped a page and re-read another, so +// this checks the whole sequence, and that entries and outbounds stay aligned. +func TestGetAllPendingOutbounds_ReadsTheCappedSetExactlyOnce(t *testing.T) { + ctx := context.Background() + + assertContiguous := func(t *testing.T, entries []*uexecutortypes.PendingOutboundEntry, outbounds []*uexecutortypes.OutboundTx) { + t.Helper() + require.Len(t, entries, pendingOutboundMaxRows) + require.Len(t, outbounds, pendingOutboundMaxRows, "an outbound per entry") + + seen := make(map[string]int, len(entries)) + for i, e := range entries { + assert.Equal(t, fmt.Sprintf("ob-%d", i), e.OutboundId, "row %d out of order", i) + assert.Equal(t, e.OutboundId, outbounds[i].Id, "entry and outbound diverged at %d", i) + seen[e.OutboundId]++ + } + require.Len(t, seen, pendingOutboundMaxRows, "a row was skipped or read twice") + for id, n := range seen { + require.Equal(t, 1, n, "%s appeared %d times", id, n) + } + } + + t.Run("full pages", func(t *testing.T) { + m := &oversizedPageClient{total: 20000, maxServable: pendingOutboundPageSize} + client := &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{m}} + + entries, outbounds, err := client.GetAllPendingOutbounds(ctx) + require.NoError(t, err) + assertContiguous(t, entries, outbounds) + }) + + t.Run("pages that had to shrink", func(t *testing.T) { + m := &oversizedPageClient{total: 20000, maxServable: 250} + client := &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{m}} + + entries, outbounds, err := client.GetAllPendingOutbounds(ctx) + require.NoError(t, err) + assertContiguous(t, entries, outbounds) + }) + + t.Run("exactly the cap available stops without a second empty request", func(t *testing.T) { + m := &oversizedPageClient{total: pendingOutboundMaxRows, maxServable: pendingOutboundPageSize} + client := &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{m}} + + entries, outbounds, err := client.GetAllPendingOutbounds(ctx) + require.NoError(t, err) + assertContiguous(t, entries, outbounds) + assert.Len(t, m.reqs, pendingOutboundMaxRows/pendingOutboundPageSize, + "hitting the cap exactly must not cost an extra round trip") + }) + + t.Run("under the cap returns everything and stops early", func(t *testing.T) { + m := &oversizedPageClient{total: 2500, maxServable: pendingOutboundPageSize} + client := &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{m}} + + entries, _, err := client.GetAllPendingOutbounds(ctx) + require.NoError(t, err) + require.Len(t, entries, 2500) + assert.Equal(t, "ob-0", entries[0].OutboundId) + assert.Equal(t, "ob-2499", entries[2499].OutboundId) + }) +} + +// The last page is trimmed to what is left of the budget. It matters whenever +// the page the walk settled on does not divide the budget: at 62 rows the walk +// reaches 4960 and the final request must ask for 40, not another 62. +func TestGetAllPendingOutbounds_LastPageIsTrimmedToTheBudget(t *testing.T) { + m := &oversizedPageClient{total: 20000, maxServable: 64} + client := &Client{logger: zerolog.Nop(), uexecutorClients: []uexecutortypes.QueryClient{m}} + + entries, outbounds, err := client.GetAllPendingOutbounds(context.Background()) + require.NoError(t, err) + + require.Len(t, entries, pendingOutboundMaxRows, "the budget is a ceiling, not a rounding") + require.Len(t, outbounds, pendingOutboundMaxRows) + + last := m.reqs[len(m.reqs)-1] + assert.Equal(t, uint64(40), last.Limit, "the final page asks only for what is left") + assert.Equal(t, uint64(pendingOutboundMaxRows-40), last.Offset) + + // No request may reach past the budget. + for i, r := range m.reqs { + if r.Limit <= m.maxServable { + assert.LessOrEqual(t, r.Offset+r.Limit, uint64(pendingOutboundMaxRows), + "request %d would read past the budget", i) + } + } +} + type mockUCallbackQueryClient struct { ucallbacktypes.QueryClient allPendingReadsResp *ucallbacktypes.QueryAllPendingReadRequestsResponse diff --git a/universalClient/pushcore/recv_size_test.go b/universalClient/pushcore/recv_size_test.go new file mode 100644 index 000000000..c4e4a0a17 --- /dev/null +++ b/universalClient/pushcore/recv_size_test.go @@ -0,0 +1,97 @@ +package pushcore + +import ( + "context" + "net" + "strings" + "testing" + "time" + + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// grpcDefaultMaxRecvMsgSize is the receive cap grpc-go applies when a client +// sets none. The whole point of maxPushCoreRecvMsgSize is that the bound is a +// choice made here rather than this inherited default. +const grpcDefaultMaxRecvMsgSize = 4 * 1024 * 1024 + +// serveResponseOfSize starts a gRPC server on a loopback port that answers any +// request with a pending-outbounds response carrying a payload of payloadBytes, +// and returns its address. +func serveResponseOfSize(t *testing.T, payloadBytes int) string { + t.Helper() + + lis, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + resp := &uexecutortypes.QueryAllPendingOutboundsResponse{ + Outbounds: []*uexecutortypes.OutboundTx{{ + Id: "oversized", + Payload: strings.Repeat("a", payloadBytes), + }}, + } + + srv := grpc.NewServer(grpc.UnknownServiceHandler(func(_ interface{}, stream grpc.ServerStream) error { + var req uexecutortypes.QueryAllPendingOutboundsRequest + if err := stream.RecvMsg(&req); err != nil { + return err + } + return stream.SendMsg(resp) + })) + + go func() { _ = srv.Serve(lis) }() + t.Cleanup(srv.Stop) + + return lis.Addr().String() +} + +func queryPendingOutbounds(t *testing.T, endpoint string) (*uexecutortypes.QueryAllPendingOutboundsResponse, error) { + t.Helper() + + conn, err := createGRPCConnection(endpoint) + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + return uexecutortypes.NewQueryClient(conn). + AllPendingOutbounds(ctx, &uexecutortypes.QueryAllPendingOutboundsRequest{}) +} + +func TestPushCoreRecvMsgSizeIsExplicit(t *testing.T) { + // A bound equal to the library default would be indistinguishable from + // setting nothing at all. + require.Greater(t, maxPushCoreRecvMsgSize, grpcDefaultMaxRecvMsgSize, + "the receive bound must be a deliberate value, not grpc-go's implicit default") + + t.Run("a response inside the bound is accepted", func(t *testing.T) { + // Above grpc-go's default, below ours: this only succeeds because the + // dial options carry an explicit MaxCallRecvMsgSize. + const size = 5 * 1024 * 1024 + require.Greater(t, size, grpcDefaultMaxRecvMsgSize) + require.Less(t, size, maxPushCoreRecvMsgSize) + + resp, err := queryPendingOutbounds(t, serveResponseOfSize(t, size)) + require.NoError(t, err) + require.Len(t, resp.Outbounds, 1) + require.Len(t, resp.Outbounds[0].Payload, size) + }) + + t.Run("a response past the bound fails as ResourceExhausted", func(t *testing.T) { + resp, err := queryPendingOutbounds(t, serveResponseOfSize(t, maxPushCoreRecvMsgSize+1)) + + require.Nil(t, resp, "no partial response may be handed to the caller") + require.Error(t, err) + + st, ok := status.FromError(err) + require.True(t, ok, "the failure must be a gRPC status, not an opaque error: %v", err) + require.Equal(t, codes.ResourceExhausted, st.Code()) + require.Contains(t, st.Message(), "8388608", + "the error must name the bound that was applied") + }) +} diff --git a/universalClient/pushsigner/vote.go b/universalClient/pushsigner/vote.go index 593375c55..a235382fe 100644 --- a/universalClient/pushsigner/vote.go +++ b/universalClient/pushsigner/vote.go @@ -14,7 +14,7 @@ import ( ) const ( - defaultGasLimit = uint64(500000000) + defaultGasLimit = uint64(100000000) defaultFeeAmount = "500000000000000upc" defaultVoteTimeout = 30 * time.Second txPollInterval = 500 * time.Millisecond diff --git a/universalClient/pushsigner/vote_test.go b/universalClient/pushsigner/vote_test.go index 19682c1b7..fe5949f65 100644 --- a/universalClient/pushsigner/vote_test.go +++ b/universalClient/pushsigner/vote_test.go @@ -17,7 +17,11 @@ import ( func TestVoteConstants(t *testing.T) { t.Run("default gas limit", func(t *testing.T) { - assert.Equal(t, uint64(500000000), defaultGasLimit) + assert.Equal(t, uint64(100000000), defaultGasLimit) + // Votes are gasless, so the chain caps what they may declare. Declaring + // more than the cap gets every vote rejected in the ante handler. + assert.LessOrEqual(t, defaultGasLimit, uexecutortypes.DefaultMaxGaslessTxGas, + "the universal validator must declare no more gas than the chain's gasless cap") }) t.Run("default fee amount is valid", func(t *testing.T) { diff --git a/universalClient/pushwatcher/event_parser.go b/universalClient/pushwatcher/event_parser.go index 476f7090a..264915a8f 100644 --- a/universalClient/pushwatcher/event_parser.go +++ b/universalClient/pushwatcher/event_parser.go @@ -77,6 +77,7 @@ func convertFundMigrationEvent(migration *utsstypes.FundMigration) (*store.Event GasPrice: migration.GasPrice, GasLimit: migration.GasLimit, L1GasFee: migration.L1GasFee, + TransferAmount: migration.TransferAmount, }) if err != nil { return nil, fmt.Errorf("failed to marshal fund migration event data: %w", err) diff --git a/universalClient/pushwatcher/event_parser_test.go b/universalClient/pushwatcher/event_parser_test.go index fae4c123e..771b0c608 100644 --- a/universalClient/pushwatcher/event_parser_test.go +++ b/universalClient/pushwatcher/event_parser_test.go @@ -359,6 +359,7 @@ func TestConvertFundMigrationEvent(t *testing.T) { GasPrice: "1000000000", GasLimit: 21100, L1GasFee: "42", + TransferAmount: "999999999999999999", } result, err := convertFundMigrationEvent(migration) @@ -384,6 +385,44 @@ func TestConvertFundMigrationEvent(t *testing.T) { assert.Equal(t, "1000000000", data.GasPrice) assert.Equal(t, uint64(21100), data.GasLimit) assert.Equal(t, "42", data.L1GasFee, "L1 gas fee must be forwarded to downstream consumers") + assert.Equal(t, "999999999999999999", data.TransferAmount, + "the chain-pinned sweep amount is what every validator signs; dropping it here leaves nothing deterministic to sign") + }) + + // A field on the record but not copied here reaches signers empty. + t.Run("fields the signing path depends on are all carried", func(t *testing.T) { + migration := &utsstypes.FundMigration{ + Id: 7, + OldKeyId: "old", + OldTssPubkey: "0x02aa", + CurrentKeyId: "new", + CurrentTssPubkey: "0x03bb", + Chain: "eip155:84532", + InitiatedBlock: 9, + GasPrice: "1", + GasLimit: 2, + L1GasFee: "3", + TransferAmount: "4", + } + + result, err := convertFundMigrationEvent(migration) + require.NoError(t, err) + + var data utsstypes.FundMigrationInitiatedEventData + require.NoError(t, json.Unmarshal(result.EventData, &data)) + + for name, got := range map[string]string{ + "old_tss_pubkey": data.OldTssPubkey, + "current_tss_pubkey": data.CurrentTssPubkey, + "chain": data.Chain, + "gas_price": data.GasPrice, + "l1_gas_fee": data.L1GasFee, + "transfer_amount": data.TransferAmount, + "old_key_id": data.OldKeyID, + } { + assert.NotEmpty(t, got, "%s was dropped in conversion", name) + } + assert.NotZero(t, data.GasLimit, "gas_limit was dropped in conversion") }) t.Run("event ID is hash of type and migration ID", func(t *testing.T) { diff --git a/universalClient/tss/coordinator/coordinator.go b/universalClient/tss/coordinator/coordinator.go index 4e7276886..d19ae5b8b 100644 --- a/universalClient/tss/coordinator/coordinator.go +++ b/universalClient/tss/coordinator/coordinator.go @@ -33,6 +33,7 @@ import ( type PushCoreClient interface { GetLatestBlock(ctx context.Context) (uint64, error) GetCurrentKey(ctx context.Context) (*utsstypes.TssKey, error) + GetKeyByID(ctx context.Context, keyID string) (*utsstypes.TssKey, error) GetAllUniversalValidators(ctx context.Context) ([]*types.UniversalValidator, error) } @@ -179,6 +180,28 @@ func (c *Coordinator) GetPeerIDFromPartyID(_ context.Context, partyID string) (s return "", fmt.Errorf("partyID %s not found in validators", partyID) } +// IsKnownPeer reports whether peerID belongs to a Universal Validator that can +// participate in some TSS protocol (Active, Pending Join, or Pending Leave). +// Fails closed when the cache is empty or stale. +func (c *Coordinator) IsKnownPeer(peerID string) bool { + for _, v := range c.validatorsSnapshot() { + if v.NetworkInfo == nil || v.NetworkInfo.PeerId != peerID { + continue + } + if v.LifecycleInfo == nil { + return false + } + switch v.LifecycleInfo.CurrentStatus { + case types.UVStatus_UV_STATUS_ACTIVE, + types.UVStatus_UV_STATUS_PENDING_JOIN, + types.UVStatus_UV_STATUS_PENDING_LEAVE: + return true + } + return false + } + return false +} + // GetMultiAddrsFromPeerID gets the multiaddrs for a given peerID. func (c *Coordinator) GetMultiAddrsFromPeerID(_ context.Context, peerID string) ([]string, error) { for _, v := range c.validatorsSnapshot() { @@ -450,11 +473,13 @@ func (c *Coordinator) processConfirmedEvents(ctx context.Context) error { // For SIGN/FUND_MIGRATE: pick a random threshold subset (>2/3 of eligible) rather than all eligible. // A threshold subset suffices for signing and is more resilient when some nodes are offline. // For all other protocols (keygen, keyrefresh, quorum_change), all eligible must participate. - var participants []*types.UniversalValidator - if event.Type == store.EventTypeSignOutbound || event.Type == store.EventTypeSignFundMigrate { - participants = getSignParticipants(allValidators) - } else { - participants = getEligibleForProtocol(event.Type, allValidators) + participants, err := c.SelectParticipants(ctx, event, allValidators) + if err != nil { + c.logger.Error().Err(err). + Str("event_id", event.EventID). + Str("type", event.Type). + Msg("cannot select participants for event") + continue } if participants == nil { c.logger.Debug().Str("event_id", event.EventID).Str("type", event.Type).Msg("unknown protocol type") @@ -591,7 +616,7 @@ func (c *Coordinator) createFundMigrationSignSetup(ctx context.Context, eventDat } keyIDBytes := deriveKeyIDBytes(migrationData.OldKeyID) - signingReq, err := c.buildFundMigrationTransaction(ctx, eventData, assignedNonce, nil /* query chain for balance */) + signingReq, err := c.buildFundMigrationTransaction(ctx, eventData, assignedNonce) if err != nil { return nil, nil, fmt.Errorf("failed to build fund migration transaction: %w", err) } @@ -612,12 +637,9 @@ func (c *Coordinator) createFundMigrationSignSetup(ctx context.Context, eventDat return setupData, signingReq, nil } -// buildFundMigrationTransaction parses event data and returns the signing -// request for sweeping old-TSS funds to the current TSS. If claimedAmount is -// non-nil, the balance is reconstructed as amount + gas + L1 instead of -// queried from chain — used by the ACK verify path to rebuild the hash -// deterministically without racing a successful sweep. -func (c *Coordinator) buildFundMigrationTransaction(ctx context.Context, eventData []byte, assignedNonce *uint64, claimedAmount *big.Int) (*common.UnsignedSigningReq, error) { +// buildFundMigrationTransaction returns the signing request for sweeping +// old-TSS funds to the current TSS, using the amount pinned on the event. +func (c *Coordinator) buildFundMigrationTransaction(ctx context.Context, eventData []byte, assignedNonce *uint64) (*common.UnsignedSigningReq, error) { if assignedNonce == nil { return nil, fmt.Errorf("assigned nonce is required for fund migration transaction") } @@ -650,23 +672,18 @@ func (c *Coordinator) buildFundMigrationTransaction(ctx context.Context, eventDa l1GasFee := new(big.Int) l1GasFee.SetString(migrationData.L1GasFee, 10) - var balance *big.Int - if claimedAmount != nil { - // balance = amount + gas + L1; inverse of computeFundMigrationTransfer - balance = new(big.Int).Set(claimedAmount) - balance.Add(balance, new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(migrationData.GasLimit))) - if l1GasFee.Sign() > 0 { - balance.Add(balance, l1GasFee) - } + transferAmount, ok := new(big.Int).SetString(migrationData.TransferAmount, 10) + if !ok || transferAmount.Sign() <= 0 { + return nil, fmt.Errorf("migration event carries no usable transfer amount: %q", migrationData.TransferAmount) } return builder.GetFundMigrationSigningRequest(ctx, &common.FundMigrationData{ - From: oldTSSAddr, - To: currentTSSAddr, - GasPrice: gasPrice, - GasLimit: migrationData.GasLimit, - L1GasFee: l1GasFee, - Balance: balance, + From: oldTSSAddr, + To: currentTSSAddr, + GasPrice: gasPrice, + GasLimit: migrationData.GasLimit, + L1GasFee: l1GasFee, + TransferAmount: transferAmount, }, *assignedNonce) } @@ -954,7 +971,7 @@ func getSignParticipants(allValidators []*types.UniversalValidator) []*types.Uni eligible := getSignEligible(allValidators) // Use utils function to select random threshold subset - return selectRandomThreshold(eligible) + return selectRandomThreshold(eligible, CalculateThreshold(len(eligible))) } // getInFlightSignCountPerChain returns per-chain in-flight SIGN count. @@ -1134,3 +1151,112 @@ func (c *Coordinator) assignFundMigrateNonce(ctx context.Context, event store.Ev return builder.GetNextNonce(ctx, oldTSSAddr, true) } + +// SelectParticipants picks who takes part in an event. +// +// For SIGN a random threshold subset (>2/3 of eligible) suffices and is more +// resilient when some nodes are offline. For all other protocols (keygen, +// keyrefresh, quorum change) every eligible validator must participate. +func (c *Coordinator) SelectParticipants( + ctx context.Context, + event store.Event, + allValidators []*types.UniversalValidator, +) ([]*types.UniversalValidator, error) { + switch event.Type { + case store.EventTypeSignOutbound: + return getSignParticipants(allValidators), nil + case store.EventTypeSignFundMigrate: + // Signed with the old key's shares, so the signers must be drawn from + // the validators that hold them rather than from whoever is eligible + // now. A newcomer selected here has no such share and never ACKs, so + // the session stalls waiting for a party that cannot take part. + return c.fundMigrateParticipants(ctx, event, allValidators) + default: + return getEligibleForProtocol(event.Type, allValidators), nil + } +} + +// FundMigrateEligible returns the validators that may sign a fund migration, +// and how many of them are required. +// +// Used by the coordinator to select signers and by every participant to +// validate the selection it receives. Both derive the answer from the same +// chain state, so a set the coordinator can legitimately pick is a set the +// participants accept. +func (c *Coordinator) FundMigrateEligible( + ctx context.Context, + event store.Event, +) ([]*types.UniversalValidator, int, error) { + return c.fundMigrateEligible(ctx, event, c.validatorsSnapshot()) +} + +// fundMigrateParticipants selects signers for a fund migration from the +// validators that hold the old key's shares. +func (c *Coordinator) fundMigrateParticipants( + ctx context.Context, + event store.Event, + allValidators []*types.UniversalValidator, +) ([]*types.UniversalValidator, error) { + holders, required, err := c.fundMigrateEligible(ctx, event, allValidators) + if err != nil { + return nil, err + } + return selectRandomThreshold(holders, required), nil +} + +// fundMigrateEligible resolves the eligible signers and the required count for +// a fund migration. +// +// The signature is produced with the old keyshare, so eligibility is decided by +// the historical shareholder set recorded on chain, not by who is a validator +// today. The required count is the old key's threshold for the same reason: it +// is the quorum that key was created under. +// +// Fails rather than returning a set that is already too small. Too few +// surviving shareholders means no subset can sign, and proceeding anyway would +// stall the session on an ACK that is never coming instead of reporting why. +// +// Nothing here can rebuild a lost quorum: an old key of N tolerates only +// N-threshold(N) departures, so migration must follow keygen promptly. +func (c *Coordinator) fundMigrateEligible( + ctx context.Context, + event store.Event, + allValidators []*types.UniversalValidator, +) ([]*types.UniversalValidator, int, error) { + var migrationData utsstypes.FundMigrationInitiatedEventData + if err := json.Unmarshal(event.EventData, &migrationData); err != nil { + return nil, 0, fmt.Errorf("parse fund migration data: %w", err) + } + if migrationData.OldKeyID == "" { + return nil, 0, fmt.Errorf("fund migration event carries no old key id") + } + + oldKey, err := c.pushCore.GetKeyByID(ctx, migrationData.OldKeyID) + if err != nil { + return nil, 0, fmt.Errorf("fetch old key %s: %w", migrationData.OldKeyID, err) + } + if oldKey == nil || len(oldKey.Participants) == 0 { + return nil, 0, fmt.Errorf("old key %s records no participants", migrationData.OldKeyID) + } + + shareholders := make(map[string]bool, len(oldKey.Participants)) + for _, p := range oldKey.Participants { + shareholders[p] = true + } + + var holders []*types.UniversalValidator + for _, v := range getSignEligible(allValidators) { + if v.IdentifyInfo != nil && shareholders[v.IdentifyInfo.CoreValidatorAddress] { + holders = append(holders, v) + } + } + + required := CalculateThreshold(len(oldKey.Participants)) + if len(holders) < required { + return nil, 0, fmt.Errorf( + "key %s needs %d of its %d shareholders to sign, only %d are still eligible", + migrationData.OldKeyID, required, len(oldKey.Participants), len(holders)) + } + + return holders, required, nil +} diff --git a/universalClient/tss/coordinator/coordinator_test.go b/universalClient/tss/coordinator/coordinator_test.go index b4ea3cd4c..f2fbe2cf1 100644 --- a/universalClient/tss/coordinator/coordinator_test.go +++ b/universalClient/tss/coordinator/coordinator_test.go @@ -382,21 +382,21 @@ func TestSelectRandomThreshold(t *testing.T) { t.Run("returns exactly threshold count", func(t *testing.T) { // threshold(5) = 4 - assert.Len(t, selectRandomThreshold(makeN(5)), 4) + assert.Len(t, selectRandomThreshold(makeN(5), CalculateThreshold(5)), 4) }) t.Run("returns all when count equals threshold", func(t *testing.T) { // threshold(2) = 2 → returns all 2 - assert.Len(t, selectRandomThreshold(makeN(2)), 2) + assert.Len(t, selectRandomThreshold(makeN(2), CalculateThreshold(2)), 2) }) t.Run("returns all when count is below threshold", func(t *testing.T) { // threshold(1) = 1 → returns all 1 - assert.Len(t, selectRandomThreshold(makeN(1)), 1) + assert.Len(t, selectRandomThreshold(makeN(1), CalculateThreshold(1)), 1) }) t.Run("returns nil for empty list", func(t *testing.T) { - assert.Nil(t, selectRandomThreshold(nil)) + assert.Nil(t, selectRandomThreshold(nil, 3)) }) } @@ -1126,6 +1126,10 @@ type stalenessMockPushCore struct { block uint64 validators []*types.UniversalValidator failGetAll bool + + // Old key history, consulted when selecting fund migration signers. + keysByID map[string]*utsstypes.TssKey + keyErr error } func (m *stalenessMockPushCore) GetLatestBlock(_ context.Context) (uint64, error) { @@ -1136,6 +1140,13 @@ func (m *stalenessMockPushCore) GetCurrentKey(_ context.Context) (*utsstypes.Tss return &utsstypes.TssKey{KeyId: "test-key"}, nil } +func (m *stalenessMockPushCore) GetKeyByID(_ context.Context, keyID string) (*utsstypes.TssKey, error) { + if m.keyErr != nil { + return nil, m.keyErr + } + return m.keysByID[keyID], nil +} + func (m *stalenessMockPushCore) GetAllUniversalValidators(_ context.Context) ([]*types.UniversalValidator, error) { if m.failGetAll { return nil, fmt.Errorf("simulated GetAllUniversalValidators RPC failure") @@ -1280,3 +1291,84 @@ func TestValidatorsSnapshot(t *testing.T) { assert.NotNil(t, coord.validatorsSnapshot()) }) } + +func TestIsKnownPeer(t *testing.T) { + uv := func(peerID string, status types.UVStatus) *types.UniversalValidator { + return &types.UniversalValidator{ + IdentifyInfo: &types.IdentityInfo{CoreValidatorAddress: "addr-" + peerID}, + NetworkInfo: &types.NetworkInfo{PeerId: peerID, MultiAddrs: []string{"/ip4/127.0.0.1/tcp/9001"}}, + LifecycleInfo: &types.LifecycleInfo{CurrentStatus: status}, + } + } + + setValidators := func(coord *Coordinator, vs []*types.UniversalValidator) { + coord.mu.Lock() + coord.allValidators = vs + coord.lastValidatorsRefreshAt = time.Now() + coord.mu.Unlock() + } + + coord, _, _ := setupTestCoordinator(t) + + t.Run("eligible statuses admitted", func(t *testing.T) { + setValidators(coord, []*types.UniversalValidator{ + uv("active", types.UVStatus_UV_STATUS_ACTIVE), + uv("joining", types.UVStatus_UV_STATUS_PENDING_JOIN), + uv("leaving", types.UVStatus_UV_STATUS_PENDING_LEAVE), + }) + assert.True(t, coord.IsKnownPeer("active")) + assert.True(t, coord.IsKnownPeer("joining")) + assert.True(t, coord.IsKnownPeer("leaving")) + }) + + t.Run("inactive and unspecified rejected", func(t *testing.T) { + setValidators(coord, []*types.UniversalValidator{ + uv("active", types.UVStatus_UV_STATUS_ACTIVE), + uv("inactive", types.UVStatus_UV_STATUS_INACTIVE), + uv("unspecified", types.UVStatus_UV_STATUS_UNSPECIFIED), + }) + assert.False(t, coord.IsKnownPeer("inactive")) + assert.False(t, coord.IsKnownPeer("unspecified")) + }) + + t.Run("unknown peer rejected", func(t *testing.T) { + setValidators(coord, []*types.UniversalValidator{ + uv("active", types.UVStatus_UV_STATUS_ACTIVE), + }) + assert.False(t, coord.IsKnownPeer("stranger")) + }) + + t.Run("nil lifecycle info rejected", func(t *testing.T) { + noLifecycle := uv("ghost", types.UVStatus_UV_STATUS_ACTIVE) + noLifecycle.LifecycleInfo = nil + setValidators(coord, []*types.UniversalValidator{ + uv("active", types.UVStatus_UV_STATUS_ACTIVE), + noLifecycle, + }) + assert.False(t, coord.IsKnownPeer("ghost")) + }) + + t.Run("bootstrap keygen peers admitted without any active validator", func(t *testing.T) { + // Fresh network: everyone is Pending Join. Strict filter must still + // admit them so keygen can start; Inactive stays rejected even here. + setValidators(coord, []*types.UniversalValidator{ + uv("joining", types.UVStatus_UV_STATUS_PENDING_JOIN), + uv("joining2", types.UVStatus_UV_STATUS_PENDING_JOIN), + uv("inactive", types.UVStatus_UV_STATUS_INACTIVE), + }) + assert.True(t, coord.IsKnownPeer("joining")) + assert.True(t, coord.IsKnownPeer("joining2")) + assert.False(t, coord.IsKnownPeer("inactive")) + assert.False(t, coord.IsKnownPeer("stranger")) + }) + + t.Run("stale cache fails closed", func(t *testing.T) { + setValidators(coord, []*types.UniversalValidator{ + uv("active", types.UVStatus_UV_STATUS_ACTIVE), + }) + coord.mu.Lock() + coord.lastValidatorsRefreshAt = time.Now().Add(-time.Hour) + coord.mu.Unlock() + assert.False(t, coord.IsKnownPeer("active")) + }) +} diff --git a/universalClient/tss/coordinator/fund_migrate_participants_test.go b/universalClient/tss/coordinator/fund_migrate_participants_test.go new file mode 100644 index 000000000..8bd919616 --- /dev/null +++ b/universalClient/tss/coordinator/fund_migrate_participants_test.go @@ -0,0 +1,276 @@ +package coordinator + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/universalClient/store" + utsstypes "github.com/pushchain/push-chain-node/x/utss/types" + "github.com/pushchain/push-chain-node/x/uvalidator/types" +) + +func activeValidator(addr string) *types.UniversalValidator { + return &types.UniversalValidator{ + IdentifyInfo: &types.IdentityInfo{CoreValidatorAddress: addr}, + LifecycleInfo: &types.LifecycleInfo{CurrentStatus: types.UVStatus_UV_STATUS_ACTIVE}, + } +} + +func validatorWithStatus(addr string, status types.UVStatus) *types.UniversalValidator { + return &types.UniversalValidator{ + IdentifyInfo: &types.IdentityInfo{CoreValidatorAddress: addr}, + LifecycleInfo: &types.LifecycleInfo{CurrentStatus: status}, + } +} + +func validatorSet(addrs ...string) []*types.UniversalValidator { + set := make([]*types.UniversalValidator, 0, len(addrs)) + for _, a := range addrs { + set = append(set, activeValidator(a)) + } + return set +} + +func addressesOf(vs []*types.UniversalValidator) []string { + addrs := make([]string, 0, len(vs)) + for _, v := range vs { + addrs = append(addrs, v.IdentifyInfo.CoreValidatorAddress) + } + return addrs +} + +func fundMigrateEvent(t *testing.T, oldKeyID string) store.Event { + t.Helper() + data, err := json.Marshal(utsstypes.FundMigrationInitiatedEventData{OldKeyID: oldKeyID}) + require.NoError(t, err) + return store.Event{ + EventID: "fm-1", + Type: store.EventTypeSignFundMigrate, + EventData: data, + } +} + +func coordinatorWithKeys(keys map[string]*utsstypes.TssKey) *Coordinator { + return &Coordinator{ + pushCore: &stalenessMockPushCore{keysByID: keys}, + logger: zerolog.Nop(), + } +} + +// The finding's scenario: the old key has three shareholders, the validator set +// has since grown to ten. Selecting from the current set draws newcomers who +// hold no share of that key. +func TestFundMigrateParticipants_DrawsOnlyFromOldKeyShareholders(t *testing.T) { + keys := map[string]*utsstypes.TssKey{ + "old-key": {KeyId: "old-key", Participants: []string{"v1", "v2", "v3"}}, + } + c := coordinatorWithKeys(keys) + + all := validatorSet("v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10") + + // Selection is randomised, so repeat to catch a newcomer slipping in. + for i := 0; i < 200; i++ { + got, err := c.fundMigrateParticipants(context.Background(), fundMigrateEvent(t, "old-key"), all) + require.NoError(t, err) + + // Old key threshold is 3 of 3, not 7 of 10. + require.Len(t, got, 3) + assert.ElementsMatch(t, []string{"v1", "v2", "v3"}, addressesOf(got)) + } +} + +// A subset of shareholders large enough to sign, alongside a much larger +// current set. Every signer must still be a shareholder. +func TestFundMigrateParticipants_UsesOldKeyThreshold(t *testing.T) { + keys := map[string]*utsstypes.TssKey{ + "old-key": {KeyId: "old-key", Participants: []string{"v1", "v2", "v3", "v4", "v5", "v6"}}, + } + c := coordinatorWithKeys(keys) + + all := validatorSet("v1", "v2", "v3", "v4", "v5", "v6", "n1", "n2", "n3", "n4", "n5") + + shareholders := map[string]bool{"v1": true, "v2": true, "v3": true, "v4": true, "v5": true, "v6": true} + for i := 0; i < 200; i++ { + got, err := c.fundMigrateParticipants(context.Background(), fundMigrateEvent(t, "old-key"), all) + require.NoError(t, err) + + // CalculateThreshold(6) is 5, and it is the old key's size that decides. + require.Len(t, got, CalculateThreshold(6)) + for _, addr := range addressesOf(got) { + assert.True(t, shareholders[addr], "selected %s which holds no share of the old key", addr) + } + } +} + +// Fail closed rather than hand back a set that cannot reach the old key's +// threshold. A short set would stall the session on an ACK that never arrives. +func TestFundMigrateParticipants_FailsWhenTooFewShareholdersRemain(t *testing.T) { + keys := map[string]*utsstypes.TssKey{ + "old-key": {KeyId: "old-key", Participants: []string{"v1", "v2", "v3", "v4", "v5", "v6"}}, + } + c := coordinatorWithKeys(keys) + + // Only 4 of the 6 shareholders remain, one short of the threshold of 5, + // while the current set is comfortably large. + all := validatorSet("v1", "v2", "v3", "v4", "n1", "n2", "n3", "n4", "n5", "n6") + + got, err := c.fundMigrateParticipants(context.Background(), fundMigrateEvent(t, "old-key"), all) + require.Error(t, err) + assert.Nil(t, got) + assert.Contains(t, err.Error(), "only 4 are still eligible") +} + +// Pending leave keeps signing; anything else is not a usable signer even when +// it holds a share. +func TestFundMigrateParticipants_ExcludesIneligibleShareholders(t *testing.T) { + keys := map[string]*utsstypes.TssKey{ + "old-key": {KeyId: "old-key", Participants: []string{"v1", "v2", "v3"}}, + } + c := coordinatorWithKeys(keys) + + all := []*types.UniversalValidator{ + validatorWithStatus("v1", types.UVStatus_UV_STATUS_ACTIVE), + validatorWithStatus("v2", types.UVStatus_UV_STATUS_PENDING_LEAVE), + validatorWithStatus("v3", types.UVStatus_UV_STATUS_ACTIVE), + } + + got, err := c.fundMigrateParticipants(context.Background(), fundMigrateEvent(t, "old-key"), all) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"v1", "v2", "v3"}, addressesOf(got)) + + // The same set with one shareholder no longer signing is one short. + all[1] = validatorWithStatus("v2", types.UVStatus_UV_STATUS_INACTIVE) + got, err = c.fundMigrateParticipants(context.Background(), fundMigrateEvent(t, "old-key"), all) + require.Error(t, err) + assert.Nil(t, got) +} + +func TestFundMigrateParticipants_RejectsUnusableEventData(t *testing.T) { + c := coordinatorWithKeys(map[string]*utsstypes.TssKey{ + "old-key": {KeyId: "old-key", Participants: []string{"v1", "v2", "v3"}}, + }) + all := validatorSet("v1", "v2", "v3") + + t.Run("malformed event data", func(t *testing.T) { + event := store.Event{EventID: "fm-1", Type: store.EventTypeSignFundMigrate, EventData: []byte("not json")} + _, err := c.fundMigrateParticipants(context.Background(), event, all) + require.Error(t, err) + assert.Contains(t, err.Error(), "parse fund migration data") + }) + + t.Run("no old key id", func(t *testing.T) { + _, err := c.fundMigrateParticipants(context.Background(), fundMigrateEvent(t, ""), all) + require.Error(t, err) + assert.Contains(t, err.Error(), "no old key id") + }) + + t.Run("unknown old key", func(t *testing.T) { + _, err := c.fundMigrateParticipants(context.Background(), fundMigrateEvent(t, "missing-key"), all) + require.Error(t, err) + assert.Contains(t, err.Error(), "records no participants") + }) + + t.Run("key with empty participants", func(t *testing.T) { + c := coordinatorWithKeys(map[string]*utsstypes.TssKey{"old-key": {KeyId: "old-key"}}) + _, err := c.fundMigrateParticipants(context.Background(), fundMigrateEvent(t, "old-key"), all) + require.Error(t, err) + assert.Contains(t, err.Error(), "records no participants") + }) + + t.Run("lookup failure", func(t *testing.T) { + c := &Coordinator{ + pushCore: &stalenessMockPushCore{keyErr: fmt.Errorf("rpc down")}, + logger: zerolog.Nop(), + } + _, err := c.fundMigrateParticipants(context.Background(), fundMigrateEvent(t, "old-key"), all) + require.Error(t, err) + assert.Contains(t, err.Error(), "fetch old key") + }) +} + +// A shareholder that has since dropped its identity record must not be counted +// towards the threshold, since it cannot be addressed as a party. +func TestFundMigrateParticipants_SkipsValidatorWithoutIdentity(t *testing.T) { + c := coordinatorWithKeys(map[string]*utsstypes.TssKey{ + "old-key": {KeyId: "old-key", Participants: []string{"v1", "v2", "v3"}}, + }) + + all := []*types.UniversalValidator{ + activeValidator("v1"), + {LifecycleInfo: &types.LifecycleInfo{CurrentStatus: types.UVStatus_UV_STATUS_ACTIVE}}, + activeValidator("v3"), + } + + _, err := c.fundMigrateParticipants(context.Background(), fundMigrateEvent(t, "old-key"), all) + require.Error(t, err) + assert.Contains(t, err.Error(), "only 2 are still eligible") +} + +// The routing itself: a fund migration must not be selected the way an +// outbound is, which is the defect this change fixes. +func TestSelectParticipants_RoutesFundMigrateToShareholders(t *testing.T) { + c := coordinatorWithKeys(map[string]*utsstypes.TssKey{ + "old-key": {KeyId: "old-key", Participants: []string{"v1", "v2", "v3"}}, + }) + + all := validatorSet("v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10") + + t.Run("fund migrate is confined to the old key", func(t *testing.T) { + for i := 0; i < 100; i++ { + got, err := c.SelectParticipants(context.Background(), fundMigrateEvent(t, "old-key"), all) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"v1", "v2", "v3"}, addressesOf(got)) + } + }) + + t.Run("outbound still uses the current set", func(t *testing.T) { + event := store.Event{EventID: "ob-1", Type: store.EventTypeSignOutbound} + got, err := c.SelectParticipants(context.Background(), event, all) + require.NoError(t, err) + assert.Len(t, got, CalculateThreshold(len(all))) + }) + + t.Run("fund migrate reports rather than returning a short set", func(t *testing.T) { + _, err := c.SelectParticipants(context.Background(), fundMigrateEvent(t, "gone"), all) + require.Error(t, err) + }) + + t.Run("other protocols take every eligible validator", func(t *testing.T) { + event := store.Event{EventID: "kg-1", Type: store.EventTypeKeygen} + got, err := c.SelectParticipants(context.Background(), event, all) + require.NoError(t, err) + assert.Len(t, got, len(all)) + }) +} + +// The caller-supplied threshold is what keeps the count tied to the old key +// rather than to the surviving holders. +func TestSelectRandomThreshold_ExplicitCount(t *testing.T) { + all := validatorSet("v1", "v2", "v3", "v4", "v5") + + assert.Nil(t, selectRandomThreshold(nil, 3)) + assert.Nil(t, selectRandomThreshold(all, 0)) + assert.Nil(t, selectRandomThreshold(all, -1)) + assert.Len(t, selectRandomThreshold(all, 5), 5) + assert.Len(t, selectRandomThreshold(all, 9), 5) + + // Picks vary across calls and never repeat a validator within one pick. + seen := map[string]bool{} + for i := 0; i < 200; i++ { + got := selectRandomThreshold(all, 3) + require.Len(t, got, 3) + unique := map[string]bool{} + for _, addr := range addressesOf(got) { + assert.False(t, unique[addr], "duplicate %s in one selection", addr) + unique[addr] = true + seen[addr] = true + } + } + assert.Len(t, seen, 5, "selection never reached some validators") +} diff --git a/universalClient/tss/coordinator/msg_handler.go b/universalClient/tss/coordinator/msg_handler.go index 736a0e485..d2f3816d1 100644 --- a/universalClient/tss/coordinator/msg_handler.go +++ b/universalClient/tss/coordinator/msg_handler.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "math/big" "github.com/pushchain/push-chain-node/universalClient/store" utsstypes "github.com/pushchain/push-chain-node/x/utss/types" @@ -199,7 +198,6 @@ func (c *Coordinator) handleSignedAck(ctx context.Context, senderPeerID, eventID signedData.Signature, signedData.SigningHash, signedData.Nonce, - signedData.TSSFundMigrationAmount, ) if err != nil { return fmt.Errorf("event %s: persist verified signature: %w", eventID, err) @@ -234,7 +232,7 @@ func (c *Coordinator) VerifySignedData(ctx context.Context, event *store.Event, if len(signedData.Signature) != 64 && len(signedData.Signature) != 65 { return fmt.Errorf("signature must be 64 or 65 bytes, got %d", len(signedData.Signature)) } - expectedHash, err := c.rebuildSigningHash(ctx, event, signedData.Nonce, signedData.TSSFundMigrationAmount) + expectedHash, err := c.rebuildSigningHash(ctx, event, signedData.Nonce) if err != nil { return fmt.Errorf("rebuild signing hash: %w", err) } @@ -279,7 +277,7 @@ func (c *Coordinator) verifyingPubkey(ctx context.Context, event *store.Event) ( } } -func (c *Coordinator) rebuildSigningHash(ctx context.Context, event *store.Event, nonce uint64, claimedAmount *big.Int) ([]byte, error) { +func (c *Coordinator) rebuildSigningHash(ctx context.Context, event *store.Event, nonce uint64) ([]byte, error) { switch event.Type { case store.EventTypeSignOutbound: req, err := c.buildSignTransaction(ctx, event.EventData, &nonce) @@ -288,10 +286,7 @@ func (c *Coordinator) rebuildSigningHash(ctx context.Context, event *store.Event } return req.SigningHash, nil case store.EventTypeSignFundMigrate: - if claimedAmount == nil || claimedAmount.Sign() <= 0 { - return nil, fmt.Errorf("fund migration verification requires positive claimed amount") - } - req, err := c.buildFundMigrationTransaction(ctx, event.EventData, &nonce, claimedAmount) + req, err := c.buildFundMigrationTransaction(ctx, event.EventData, &nonce) if err != nil { return nil, err } diff --git a/universalClient/tss/coordinator/msg_handler_test.go b/universalClient/tss/coordinator/msg_handler_test.go index f271ce34e..f61a2f4a5 100644 --- a/universalClient/tss/coordinator/msg_handler_test.go +++ b/universalClient/tss/coordinator/msg_handler_test.go @@ -202,7 +202,7 @@ func TestHandleSignedAck_FailurePaths(t *testing.T) { assert.Contains(t, err.Error(), "has no signature to verify") }) - t.Run("fund migration without claimed amount rejected", func(t *testing.T) { + t.Run("fund migration with unusable event data rejected", func(t *testing.T) { require.NoError(t, db.Create(&store.Event{ EventID: "fm-evt", BlockHeight: 1, @@ -216,7 +216,7 @@ func TestHandleSignedAck_FailurePaths(t *testing.T) { SigningHash: make([]byte, 32), }) require.Error(t, err) - assert.Contains(t, err.Error(), "requires positive claimed amount") + assert.Contains(t, err.Error(), "rebuild signing hash") }) t.Run("verification failure does not touch ackTracking", func(t *testing.T) { diff --git a/universalClient/tss/coordinator/types.go b/universalClient/tss/coordinator/types.go index 1e291aaf6..fbfec7785 100644 --- a/universalClient/tss/coordinator/types.go +++ b/universalClient/tss/coordinator/types.go @@ -2,7 +2,6 @@ package coordinator import ( "context" - "math/big" "github.com/pushchain/push-chain-node/universalClient/externalchains/common" ) @@ -25,10 +24,9 @@ const ( // when the participant already holds a valid signature for this event, // letting the coordinator skip a fresh DKLS run. type SignedDataPayload struct { - Signature []byte `json:"signature"` // ECDSA (r || s [|| v]) - SigningHash []byte `json:"signing_hash"` // 32-byte message hash - Nonce uint64 `json:"nonce"` // EVM nonce; ignored by SVM - TSSFundMigrationAmount *big.Int `json:"tss_fund_migration_amount,omitempty"` + Signature []byte `json:"signature"` // ECDSA (r || s [|| v]) + SigningHash []byte `json:"signing_hash"` // 32-byte message hash + Nonce uint64 `json:"nonce"` // EVM nonce; ignored by SVM } // Message is the wire format for all TSS coordination messages. diff --git a/universalClient/tss/coordinator/utils.go b/universalClient/tss/coordinator/utils.go index b64371ba8..562138afe 100644 --- a/universalClient/tss/coordinator/utils.go +++ b/universalClient/tss/coordinator/utils.go @@ -65,28 +65,25 @@ func deriveKeyIDBytes(keyID string) []byte { return sum[:] } -// selectRandomThreshold selects a random subset of at least threshold count from eligible validators. -// Returns a shuffled copy of at least threshold validators (or all if fewer than threshold). -func selectRandomThreshold(eligible []*types.UniversalValidator) []*types.UniversalValidator { - if len(eligible) == 0 { +// selectRandomThreshold selects a random threshold count of eligible validators. +// Returns a shuffled copy of threshold validators (or all if fewer than threshold). +// The caller supplies the threshold: for fund migration it belongs to the old key, +// not to the set of validators still holding its shares. +func selectRandomThreshold(eligible []*types.UniversalValidator, threshold int) []*types.UniversalValidator { + if len(eligible) == 0 || threshold <= 0 { return nil } - // Calculate minimum required: >2/3 (same as threshold calculation) - minRequired := CalculateThreshold(len(eligible)) - - // If we have fewer than minRequired, return all - if len(eligible) <= minRequired { + // If we have fewer than threshold, return all + if len(eligible) <= threshold { return eligible } - // Randomly select at least minRequired participants - // Shuffle and take first minRequired shuffled := make([]*types.UniversalValidator, len(eligible)) copy(shuffled, eligible) rand.Shuffle(len(shuffled), func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] }) - return shuffled[:minRequired] + return shuffled[:threshold] } diff --git a/universalClient/tss/dkls/utils.go b/universalClient/tss/dkls/utils.go index c77c62415..ae2fecc98 100644 --- a/universalClient/tss/dkls/utils.go +++ b/universalClient/tss/dkls/utils.go @@ -2,6 +2,10 @@ package dkls import ( "crypto/sha256" + "encoding/binary" + "fmt" + + session "go-wrapper/go-dkls/sessions" ) // deriveKeyID derives a key ID bytes from a string key ID. @@ -22,3 +26,90 @@ func encodeParticipantIDs(participants []string) []byte { } return ids } + +// --- Setup decoding ------------------------------------------------------- +// The coordinator supplies the setup blob and the values a follower validates +// separately. DKLS runs on the blob, so these expose what it actually contains +// and let callers bind the two. Both return an error on a malformed blob. + +// SetupMessageHash returns the message hash embedded in a sign setup blob. +// DklsSignSessionFromSetup signs over the setup, not over any hash passed +// alongside it, so callers must confirm the two agree. +func SetupMessageHash(setupData []byte) ([]byte, error) { + if len(setupData) == 0 { + return nil, fmt.Errorf("setupData is required") + } + return session.DklsDecodeMessage(setupData) +} + +// SetupParticipants returns the participant list embedded in a DKLS setup blob, +// in index order. The setup is what actually drives the session, so callers must +// confirm it matches the participants they validated. Otherwise a coordinator +// can present one list for validation and run the session over another. +// +// Party names decode by index and come back empty past the end, which is how the +// list terminates. +func SetupParticipants(setupData []byte) ([]string, error) { + if len(setupData) == 0 { + return nil, fmt.Errorf("setupData is required") + } + var participants []string + for i := 0; ; i++ { + name, err := session.DklsDecodePartyName(setupData, i) + if err != nil { + return nil, fmt.Errorf("failed to decode party name at index %d: %w", i, err) + } + if len(name) == 0 { + break + } + participants = append(participants, string(name)) + } + return participants, nil +} + +// Setup blobs are a tag-length-value list after a fixed header. The wrapper +// exposes decoders for the key ID, message and party names but not the +// threshold, so that one is read here. Values are laid out as: +// +// tag uint16 little endian +// length uint16 little endian, stored as length-1 +// value length bytes +// +// The header is MESSAGE_ID_SIZE(32) + 2 + 2. This mirrors the library's internal +// encoding, so TestSetupThreshold pins it: if the format changes, that test +// fails rather than this silently reading the wrong byte. +const ( + setupHeaderSize = 36 + setupTagThreshold = 1 +) + +// SetupThreshold returns the threshold embedded in a keygen, keyrefresh or +// quorumchange setup blob. Sign setups carry no threshold and return an error. +func SetupThreshold(setupData []byte) (int, error) { + if len(setupData) < setupHeaderSize { + return 0, fmt.Errorf("setup message too short to contain a threshold") + } + for offset := setupHeaderSize; offset+4 <= len(setupData); { + tag := binary.LittleEndian.Uint16(setupData[offset : offset+2]) + length := int(binary.LittleEndian.Uint16(setupData[offset+2:offset+4])) + 1 + valueStart := offset + 4 + if valueStart+length > len(setupData) { + return 0, fmt.Errorf("setup message is malformed: tag %d claims %d bytes past the end", tag, length) + } + if tag == setupTagThreshold { + // keygen and quorumchange store the threshold as a u8; the weighted + // keygen variant uses a u16 under the same tag. Accept either so a + // library upgrade widening it does not reject every setup. + switch length { + case 1: + return int(setupData[valueStart]), nil + case 2: + return int(binary.LittleEndian.Uint16(setupData[valueStart : valueStart+2])), nil + default: + return 0, fmt.Errorf("threshold tag has unexpected length %d", length) + } + } + offset = valueStart + length + } + return 0, fmt.Errorf("setup message carries no threshold") +} diff --git a/universalClient/tss/dkls/utils_test.go b/universalClient/tss/dkls/utils_test.go index df57610e8..c3985b7f3 100644 --- a/universalClient/tss/dkls/utils_test.go +++ b/universalClient/tss/dkls/utils_test.go @@ -1,8 +1,11 @@ package dkls import ( + "bytes" "crypto/sha256" "testing" + + session "go-wrapper/go-dkls/sessions" ) func TestDeriveKeyID(t *testing.T) { @@ -58,3 +61,265 @@ func TestEncodeParticipantIDs(t *testing.T) { }) } } + +// The setup blob is what DKLS actually runs on, so these decoders are what let a +// follower bind it to the values it validated separately. Both must report what +// the blob really contains, and must error rather than guess on a malformed one. + +func TestSetupMessageHash(t *testing.T) { + participantIDs := encodeParticipantIDs([]string{"party1", "party2"}) + keyID := make([]byte, 32) + + legitHash := make([]byte, 32) + copy(legitHash, "legitimate-outbound-hash-32bytes") + attackerHash := make([]byte, 32) + copy(attackerHash, "attacker-chosen-vault-call-digest") + + t.Run("returns the hash embedded in the setup", func(t *testing.T) { + setup, err := session.DklsSignSetupMsgNew(keyID, nil, legitHash, participantIDs) + if err != nil { + t.Fatalf("failed to build sign setup: %v", err) + } + got, err := SetupMessageHash(setup) + if err != nil { + t.Fatalf("SetupMessageHash() error = %v", err) + } + if !bytes.Equal(got, legitHash) { + t.Errorf("SetupMessageHash() = %x, want %x", got, legitHash) + } + }) + + // A substituted setup must report the hash it really signs, which is what + // makes the mismatch detectable. + t.Run("substituted setup reports the attacker hash", func(t *testing.T) { + setup, err := session.DklsSignSetupMsgNew(keyID, nil, attackerHash, participantIDs) + if err != nil { + t.Fatalf("failed to build sign setup: %v", err) + } + got, err := SetupMessageHash(setup) + if err != nil { + t.Fatalf("SetupMessageHash() error = %v", err) + } + if bytes.Equal(got, legitHash) { + t.Fatal("substituted setup must not report the legitimate hash") + } + if !bytes.Equal(got, attackerHash) { + t.Errorf("SetupMessageHash() = %x, want %x", got, attackerHash) + } + }) + + t.Run("errors on empty and malformed setup", func(t *testing.T) { + if _, err := SetupMessageHash(nil); err == nil { + t.Error("SetupMessageHash(nil) should error") + } + if _, err := SetupMessageHash([]byte("not-a-dkls-setup")); err == nil { + t.Error("SetupMessageHash(malformed) should error") + } + }) +} + +func TestSetupParticipants(t *testing.T) { + t.Run("returns the participants in index order", func(t *testing.T) { + want := []string{"alice", "bob", "carol"} + setup, err := session.DklsKeygenSetupMsgNew(2, nil, encodeParticipantIDs(want)) + if err != nil { + t.Fatalf("failed to build keygen setup: %v", err) + } + got, err := SetupParticipants(setup) + if err != nil { + t.Fatalf("SetupParticipants() error = %v", err) + } + if len(got) != len(want) { + t.Fatalf("SetupParticipants() = %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("participant %d = %q, want %q", i, got[i], want[i]) + } + } + }) + + // Enumeration terminates on the first empty name rather than an error, which + // is the contract this relies on to find the end of the list. + t.Run("terminates at the end of a two party list", func(t *testing.T) { + want := []string{"first", "second"} + setup, err := session.DklsKeygenSetupMsgNew(2, nil, encodeParticipantIDs(want)) + if err != nil { + t.Fatalf("failed to build keygen setup: %v", err) + } + got, err := SetupParticipants(setup) + if err != nil { + t.Fatalf("SetupParticipants() error = %v", err) + } + if len(got) != 2 || got[0] != "first" || got[1] != "second" { + t.Errorf("SetupParticipants() = %v, want %v", got, want) + } + }) + + t.Run("errors on empty and malformed setup", func(t *testing.T) { + if _, err := SetupParticipants(nil); err == nil { + t.Error("SetupParticipants(nil) should error") + } + if _, err := SetupParticipants([]byte("not-a-dkls-setup")); err == nil { + t.Error("SetupParticipants(malformed) should error") + } + }) +} + +// Pins the setup TLV layout this package parses directly. If the library +// changes its encoding, this fails loudly instead of SetupThreshold silently +// reading the wrong byte. +func TestSetupThreshold(t *testing.T) { + participants := []string{"alice", "bob", "carol"} + + t.Run("reads the embedded keygen threshold", func(t *testing.T) { + for _, want := range []int{2, 3} { + setup, err := session.DklsKeygenSetupMsgNew(want, nil, encodeParticipantIDs(participants)) + if err != nil { + t.Fatalf("failed to build keygen setup with threshold %d: %v", want, err) + } + got, err := SetupThreshold(setup) + if err != nil { + t.Fatalf("SetupThreshold() error = %v", err) + } + if got != want { + t.Errorf("SetupThreshold() = %d, want %d", got, want) + } + } + }) + + // A downgraded setup must report the weaker threshold it really carries, + // which is what makes the mismatch detectable. + t.Run("downgraded setup reports the weaker threshold", func(t *testing.T) { + setup, err := session.DklsKeygenSetupMsgNew(2, nil, encodeParticipantIDs(participants)) + if err != nil { + t.Fatalf("failed to build keygen setup: %v", err) + } + got, err := SetupThreshold(setup) + if err != nil { + t.Fatalf("SetupThreshold() error = %v", err) + } + if got == 3 { + t.Fatal("downgraded setup must not report the expected threshold") + } + if got != 2 { + t.Errorf("SetupThreshold() = %d, want 2", got) + } + }) + + t.Run("errors on short, malformed and thresholdless setups", func(t *testing.T) { + if _, err := SetupThreshold(nil); err == nil { + t.Error("SetupThreshold(nil) should error") + } + if _, err := SetupThreshold([]byte("too-short")); err == nil { + t.Error("SetupThreshold(short) should error") + } + // Header present but no tags at all. + if _, err := SetupThreshold(make([]byte, setupHeaderSize)); err == nil { + t.Error("SetupThreshold(no tags) should error") + } + }) +} + +// Cross-protocol matrix over the three setup shapes the coordinator builds: +// keygen (shared with keyrefresh), quorumchange, and sign (shared with fund +// migration). Pins what each decoder returns for each shape, so a rebuilt DKLS +// library that changes the encoding fails here rather than in production. +func TestSetupDecoders_AllProtocols(t *testing.T) { + participants := []string{"party1", "party2", "party3"} + ids := encodeParticipantIDs(participants) + const threshold = 2 + + messageHash := make([]byte, 32) + copy(messageHash, "outbound-signing-hash-32-bytes!!") + + // keygen, also used verbatim for keyrefresh + keygenSetup, err := session.DklsKeygenSetupMsgNew(threshold, nil, ids) + if err != nil { + t.Fatalf("failed to build keygen setup: %v", err) + } + + // sign, also used for fund migration + signSetup, err := session.DklsSignSetupMsgNew(make([]byte, 32), nil, messageHash, ids) + if err != nil { + t.Fatalf("failed to build sign setup: %v", err) + } + + // quorumchange needs a real keyshare, so run a keygen to completion first + sessions := map[string]Session{} + for _, p := range participants { + sess, err := NewKeygenSession(keygenSetup, "matrix", p, participants, threshold) + if err != nil { + t.Fatalf("failed to create keygen session for %s: %v", p, err) + } + sessions[p] = sess + } + keyshare := runToCompletion(t, sessions)["party1"].Keyshare + handle, err := session.DklsKeyshareFromBytes(keyshare) + if err != nil { + t.Fatalf("failed to load keyshare: %v", err) + } + defer session.DklsKeyshareFree(handle) + + qcSetup, err := session.DklsQcSetupMsgNew(handle, threshold, participants, []int{0, 1, 2}, []int{0, 1, 2}) + if err != nil { + t.Fatalf("failed to build QC setup: %v", err) + } + + shapes := []struct { + name string + setup []byte + wantHash []byte // nil means the shape carries no message + hasThreshold bool + }{ + {"keygen and keyrefresh", keygenSetup, nil, true}, + {"quorumchange", qcSetup, nil, true}, + {"sign and fund migration", signSetup, messageHash, false}, + } + + for _, sh := range shapes { + t.Run(sh.name, func(t *testing.T) { + // Participants are bound for every protocol, so every shape must decode them. + got, err := SetupParticipants(sh.setup) + if err != nil { + t.Fatalf("SetupParticipants() error = %v", err) + } + if len(got) != len(participants) { + t.Fatalf("SetupParticipants() = %v, want %v", got, participants) + } + for i := range participants { + if got[i] != participants[i] { + t.Errorf("participant %d = %q, want %q", i, got[i], participants[i]) + } + } + + gotThreshold, thresholdErr := SetupThreshold(sh.setup) + if sh.hasThreshold { + if thresholdErr != nil { + t.Fatalf("SetupThreshold() error = %v", thresholdErr) + } + if gotThreshold != threshold { + t.Errorf("SetupThreshold() = %d, want %d", gotThreshold, threshold) + } + } else if thresholdErr == nil { + // Sign setups carry no threshold. Erroring is what stops a bogus + // value being read out of unrelated bytes. + t.Errorf("SetupThreshold() on a sign setup returned %d, want an error", gotThreshold) + } + + gotHash, hashErr := SetupMessageHash(sh.setup) + if hashErr != nil { + t.Fatalf("SetupMessageHash() error = %v", hashErr) + } + if sh.wantHash == nil { + // Shapes without a message report an empty hash rather than an + // error, so a non-sign setup can never satisfy the hash binding. + if len(gotHash) != 0 { + t.Errorf("SetupMessageHash() = %x, want empty", gotHash) + } + } else if !bytes.Equal(gotHash, sh.wantHash) { + t.Errorf("SetupMessageHash() = %x, want %x", gotHash, sh.wantHash) + } + }) + } +} diff --git a/universalClient/tss/eventstore/store.go b/universalClient/tss/eventstore/store.go index d7bb7ede9..f8cb4b50a 100644 --- a/universalClient/tss/eventstore/store.go +++ b/universalClient/tss/eventstore/store.go @@ -4,7 +4,6 @@ import ( "encoding/hex" "encoding/json" "fmt" - "math/big" "time" "github.com/rs/zerolog" @@ -74,16 +73,12 @@ func (s *Store) PersistSignature( signature []byte, signingHash []byte, nonce uint64, - fundMigrationAmount *big.Int, ) (bool, error) { signingData := map[string]any{ "signature": hex.EncodeToString(signature), "signing_hash": hex.EncodeToString(signingHash), "nonce": nonce, } - if fundMigrationAmount != nil && fundMigrationAmount.Sign() > 0 { - signingData["tss_fund_migration_amount"] = fundMigrationAmount - } var raw map[string]any if err := json.Unmarshal(eventData, &raw); err != nil { diff --git a/universalClient/tss/eventstore/store_test.go b/universalClient/tss/eventstore/store_test.go index 0ee1f8566..8d62f3c22 100644 --- a/universalClient/tss/eventstore/store_test.go +++ b/universalClient/tss/eventstore/store_test.go @@ -715,7 +715,7 @@ func TestPersistSignature(t *testing.T) { t.Fatalf("seed event: %v", err) } - persisted, err := s.PersistSignature("ev-1", baseEventData, sig, hash, 42, nil) + persisted, err := s.PersistSignature("ev-1", baseEventData, sig, hash, 42) if err != nil { t.Fatalf("PersistSignature: %v", err) } @@ -758,7 +758,7 @@ func TestPersistSignature(t *testing.T) { t.Fatalf("seed event: %v", err) } - persisted, err := s.PersistSignature("ev-2", baseEventData, sig, hash, 7, nil) + persisted, err := s.PersistSignature("ev-2", baseEventData, sig, hash, 7) if err != nil { t.Fatalf("PersistSignature: %v", err) } @@ -783,7 +783,7 @@ func TestPersistSignature(t *testing.T) { t.Fatalf("seed event: %v", err) } - persisted, err := s.PersistSignature("ev-3", baseEventData, sig, hash, 1, nil) + persisted, err := s.PersistSignature("ev-3", baseEventData, sig, hash, 1) if err != nil { t.Fatalf("PersistSignature: %v", err) } @@ -811,7 +811,7 @@ func TestPersistSignature(t *testing.T) { t.Fatalf("seed event: %v", err) } - persisted, err := s.PersistSignature("ev-4", baseEventData, sig, hash, 1, nil) + persisted, err := s.PersistSignature("ev-4", baseEventData, sig, hash, 1) if err != nil { t.Fatalf("PersistSignature: %v", err) } @@ -829,7 +829,7 @@ func TestPersistSignature(t *testing.T) { t.Run("invalid event data JSON returns error", func(t *testing.T) { s := setupTestStore(t) - _, err := s.PersistSignature("ev-5", []byte("not json"), sig, hash, 1, nil) + _, err := s.PersistSignature("ev-5", []byte("not json"), sig, hash, 1) if err == nil { t.Fatal("expected error on invalid JSON") } diff --git a/universalClient/tss/keyshare/manager.go b/universalClient/tss/keyshare/manager.go index 0e5574261..0e599428e 100644 --- a/universalClient/tss/keyshare/manager.go +++ b/universalClient/tss/keyshare/manager.go @@ -144,6 +144,59 @@ func (m *Manager) Exists(id string) (bool, error) { return true, nil } +// List returns the IDs of all stored keyshares. +func (m *Manager) List() ([]string, error) { + entries, err := os.ReadDir(m.keysharesDir) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, fmt.Errorf("failed to read keyshares directory: %w", err) + } + + ids := make([]string, 0, len(entries)) + for _, e := range entries { + if !e.IsDir() { + ids = append(ids, e.Name()) + } + } + return ids, nil +} + +// Delete removes a stored keyshare. It overwrites the file with random bytes +// before unlinking; on SSD/COW filesystems that is best-effort, so the real +// protection remains the at-rest encryption. Deleting a missing ID is a no-op. +func (m *Manager) Delete(id string) error { + if id == "" { + return ErrInvalidID + } + + if strings.Contains(id, "/") || strings.Contains(id, "\\") || strings.Contains(id, "..") { + return fmt.Errorf("%w: id contains invalid characters", ErrInvalidID) + } + + filePath := filepath.Join(m.keysharesDir, id) + info, err := os.Stat(filePath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return fmt.Errorf("failed to stat keyshare file: %w", err) + } + + if info.Mode().IsRegular() && info.Size() > 0 { + scratch := make([]byte, info.Size()) + if _, rerr := rand.Read(scratch); rerr == nil { + _ = os.WriteFile(filePath, scratch, filePerms) + } + } + + if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove keyshare file: %w", err) + } + return nil +} + // encrypt encrypts keyshare data using AES-256-GCM with a password-derived key. // Returns encrypted data in format: [salt(32) || nonce(12) || ciphertext || tag(16)] func (m *Manager) encrypt(keyshareData []byte) ([]byte, error) { diff --git a/universalClient/tss/keyshare/manager_test.go b/universalClient/tss/keyshare/manager_test.go index 348d0d3d7..f7c9c37af 100644 --- a/universalClient/tss/keyshare/manager_test.go +++ b/universalClient/tss/keyshare/manager_test.go @@ -516,3 +516,133 @@ func TestManager_EncryptDecrypt(t *testing.T) { } }) } + +func TestList(t *testing.T) { + t.Run("empty directory", func(t *testing.T) { + mgr, err := NewManager(t.TempDir(), "pw") + if err != nil { + t.Fatalf("NewManager() error = %v", err) + } + ids, err := mgr.List() + if err != nil { + t.Fatalf("List() error = %v", err) + } + if len(ids) != 0 { + t.Errorf("List() = %v, want empty", ids) + } + }) + + t.Run("returns stored ids", func(t *testing.T) { + mgr, err := NewManager(t.TempDir(), "pw") + if err != nil { + t.Fatalf("NewManager() error = %v", err) + } + for _, id := range []string{"key-a", "key-b"} { + if err := mgr.Store([]byte("share-"+id), id); err != nil { + t.Fatalf("Store(%s) error = %v", id, err) + } + } + ids, err := mgr.List() + if err != nil { + t.Fatalf("List() error = %v", err) + } + if len(ids) != 2 { + t.Fatalf("List() returned %d ids, want 2", len(ids)) + } + found := map[string]bool{} + for _, id := range ids { + found[id] = true + } + if !found["key-a"] || !found["key-b"] { + t.Errorf("List() = %v, want key-a and key-b", ids) + } + }) + + t.Run("ignores subdirectories", func(t *testing.T) { + tmpDir := t.TempDir() + mgr, err := NewManager(tmpDir, "pw") + if err != nil { + t.Fatalf("NewManager() error = %v", err) + } + if err := os.MkdirAll(filepath.Join(mgr.keysharesDir, "nested"), dirPerms); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + ids, err := mgr.List() + if err != nil { + t.Fatalf("List() error = %v", err) + } + if len(ids) != 0 { + t.Errorf("List() = %v, want empty (dirs ignored)", ids) + } + }) +} + +func TestDelete(t *testing.T) { + t.Run("removes stored keyshare", func(t *testing.T) { + mgr, err := NewManager(t.TempDir(), "pw") + if err != nil { + t.Fatalf("NewManager() error = %v", err) + } + if err := mgr.Store([]byte("secret-share"), "key-1"); err != nil { + t.Fatalf("Store() error = %v", err) + } + if err := mgr.Delete("key-1"); err != nil { + t.Fatalf("Delete() error = %v", err) + } + if _, err := mgr.Get("key-1"); !errors.Is(err, ErrKeyshareNotFound) { + t.Errorf("Get() after Delete error = %v, want ErrKeyshareNotFound", err) + } + exists, err := mgr.Exists("key-1") + if err != nil { + t.Fatalf("Exists() error = %v", err) + } + if exists { + t.Error("Exists() = true after Delete, want false") + } + }) + + t.Run("missing id is a no-op", func(t *testing.T) { + mgr, err := NewManager(t.TempDir(), "pw") + if err != nil { + t.Fatalf("NewManager() error = %v", err) + } + if err := mgr.Delete("never-stored"); err != nil { + t.Errorf("Delete() on missing id error = %v, want nil", err) + } + }) + + t.Run("rejects invalid ids", func(t *testing.T) { + mgr, err := NewManager(t.TempDir(), "pw") + if err != nil { + t.Fatalf("NewManager() error = %v", err) + } + for _, id := range []string{"", "../escape", "sub/dir", "back\\slash"} { + if err := mgr.Delete(id); !errors.Is(err, ErrInvalidID) { + t.Errorf("Delete(%q) error = %v, want ErrInvalidID", id, err) + } + } + }) + + t.Run("leaves other keyshares intact", func(t *testing.T) { + mgr, err := NewManager(t.TempDir(), "pw") + if err != nil { + t.Fatalf("NewManager() error = %v", err) + } + if err := mgr.Store([]byte("share-a"), "key-a"); err != nil { + t.Fatalf("Store() error = %v", err) + } + if err := mgr.Store([]byte("share-b"), "key-b"); err != nil { + t.Fatalf("Store() error = %v", err) + } + if err := mgr.Delete("key-a"); err != nil { + t.Fatalf("Delete() error = %v", err) + } + got, err := mgr.Get("key-b") + if err != nil { + t.Fatalf("Get(key-b) error = %v", err) + } + if string(got) != "share-b" { + t.Errorf("Get(key-b) = %q, want %q", got, "share-b") + } + }) +} diff --git a/universalClient/tss/keyshare/sweeper.go b/universalClient/tss/keyshare/sweeper.go new file mode 100644 index 000000000..06e4c2e91 --- /dev/null +++ b/universalClient/tss/keyshare/sweeper.go @@ -0,0 +1,157 @@ +package keyshare + +import ( + "context" + "sync" + "time" + + "github.com/rs/zerolog" + + utsstypes "github.com/pushchain/push-chain-node/x/utss/types" +) + +// Quorum change and key refresh are rare, and a retained share is only a +// concern over the long run, so sweeping daily is ample. +const defaultCheckInterval = 24 * time.Hour + +// PushCoreClient is the subset of pushcore.Client the sweeper depends on. +// Defined as an interface so tests can inject a mock. *pushcore.Client satisfies it. +type PushCoreClient interface { + GetCurrentKey(ctx context.Context) (*utsstypes.TssKey, error) + GetKeyByID(ctx context.Context, keyID string) (*utsstypes.TssKey, error) +} + +// KeyshareStore is the subset of keyshare.Manager the sweeper depends on. +type KeyshareStore interface { + List() ([]string, error) + Delete(id string) error +} + +// Config holds configuration for the keyshare sweeper. +type Config struct { + Keyshares KeyshareStore + PushCore PushCoreClient + CheckInterval time.Duration + Logger zerolog.Logger +} + +// Sweeper deletes local keyshares that chain state proves are redundant. +// +// A keyshare is deleted only when every one of these holds: +// - it is not the current key ID; +// - its TSS pubkey equals the current key's pubkey, i.e. a quorum change or +// key refresh superseded it while preserving the vault key. +// +// Those two conditions are sufficient. The current key only changes when a key +// process finalizes, so while one is in flight the predecessor is still current +// and therefore never a deletion candidate. Fund migrations only exist across a +// pubkey rotation, so they can only reference a key this sweeper already keeps. +// +// Shares whose pubkey differs from the current one are kept: they belong to a +// rotated-away key that fund migration still needs to sweep its vault. Retiring +// those is an explicit operator action, since a chain that was never migrated is +// indistinguishable from one with nothing to migrate. +// +// Every chain-state lookup fails closed: on error the sweep is skipped and +// retried next tick rather than deleting on incomplete information. Pubkeys are +// resolved per held share rather than from the full key history, which grows +// unbounded and would need paging. +type Sweeper struct { + keyshares KeyshareStore + pushCore PushCoreClient + checkInterval time.Duration + logger zerolog.Logger + startOnce sync.Once +} + +// NewSweeper creates a new keyshare sweeper. +func NewSweeper(cfg Config) *Sweeper { + interval := cfg.CheckInterval + if interval == 0 { + interval = defaultCheckInterval + } + return &Sweeper{ + keyshares: cfg.Keyshares, + pushCore: cfg.PushCore, + checkInterval: interval, + logger: cfg.Logger.With().Str("component", "keyshare_sweeper").Logger(), + } +} + +// Start begins the background sweep loop. Repeat calls are no-ops, so a +// restarted node cannot end up with two sweepers deleting concurrently. +func (s *Sweeper) Start(ctx context.Context) { + s.startOnce.Do(func() { + go s.run(ctx) + }) +} + +func (s *Sweeper) run(ctx context.Context) { + ticker := time.NewTicker(s.checkInterval) + defer ticker.Stop() + + // Sweep on start: with a long interval, a node restarted more often than + // that would otherwise never sweep. + s.sweep(ctx) + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + s.sweep(ctx) + } + } +} + +func (s *Sweeper) sweep(ctx context.Context) { + if s.keyshares == nil || s.pushCore == nil { + return + } + + localIDs, err := s.keyshares.List() + if err != nil { + s.logger.Warn().Err(err).Msg("failed to list keyshares, skipping sweep") + return + } + if len(localIDs) <= 1 { + return + } + + current, err := s.pushCore.GetCurrentKey(ctx) + if err != nil { + s.logger.Debug().Err(err).Msg("failed to get current TSS key, skipping sweep") + return + } + if current == nil || current.KeyId == "" || current.TssPubkey == "" { + return + } + + deleted := 0 + for _, id := range localIDs { + if id == current.KeyId { + continue + } + // Look up only the shares we hold; the on-chain key history is unbounded. + // Any lookup failure (unknown ID or transport error) keeps the share. + key, err := s.pushCore.GetKeyByID(ctx, id) + if err != nil || key == nil { + s.logger.Debug().Err(err).Str("key_id", id).Msg("cannot resolve keyshare on chain, keeping") + continue + } + if key.TssPubkey != current.TssPubkey { + continue + } + if err := s.keyshares.Delete(id); err != nil { + s.logger.Warn().Err(err).Str("key_id", id).Msg("failed to delete superseded keyshare") + continue + } + deleted++ + s.logger.Info().Str("key_id", id).Str("current_key_id", current.KeyId). + Msg("deleted superseded keyshare") + } + + if deleted > 0 { + s.logger.Info().Int("deleted", deleted).Msg("keyshare sweep complete") + } +} diff --git a/universalClient/tss/keyshare/sweeper_test.go b/universalClient/tss/keyshare/sweeper_test.go new file mode 100644 index 000000000..d537812dc --- /dev/null +++ b/universalClient/tss/keyshare/sweeper_test.go @@ -0,0 +1,213 @@ +package keyshare + +import ( + "context" + "errors" + "sync" + "testing" + "time" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + utsstypes "github.com/pushchain/push-chain-node/x/utss/types" +) + +const ( + pubkeyA = "0xAAA" + pubkeyB = "0xBBB" +) + +type mockStore struct { + mu sync.Mutex + ids []string + deleted []string + listErr error + delErr error +} + +func (m *mockStore) List() ([]string, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.listErr != nil { + return nil, m.listErr + } + return append([]string(nil), m.ids...), nil +} + +// Delete drops the id so a repeat sweep cannot delete it twice, matching the +// real Manager. +func (m *mockStore) Delete(id string) error { + m.mu.Lock() + defer m.mu.Unlock() + if m.delErr != nil { + return m.delErr + } + remaining := m.ids[:0] + for _, existing := range m.ids { + if existing != id { + remaining = append(remaining, existing) + } + } + m.ids = remaining + m.deleted = append(m.deleted, id) + return nil +} + +func (m *mockStore) deletedIDs() []string { + m.mu.Lock() + defer m.mu.Unlock() + return append([]string(nil), m.deleted...) +} + +type mockCore struct { + current *utsstypes.TssKey + keys map[string]*utsstypes.TssKey + + currentErr, keysErr error +} + +func (m *mockCore) GetCurrentKey(context.Context) (*utsstypes.TssKey, error) { + return m.current, m.currentErr +} +func (m *mockCore) GetKeyByID(_ context.Context, keyID string) (*utsstypes.TssKey, error) { + if m.keysErr != nil { + return nil, m.keysErr + } + k, ok := m.keys[keyID] + if !ok { + return nil, errors.New("key not found") + } + return k, nil +} +func key(id, pubkey string) *utsstypes.TssKey { + return &utsstypes.TssKey{KeyId: id, TssPubkey: pubkey} +} + +// baseCore: K1 and K2 share pubkeyA (quorum change / refresh); K0 is a rotated +// away key on pubkeyB. K2 is current. +func baseCore() *mockCore { + return &mockCore{ + current: key("K2", pubkeyA), + keys: map[string]*utsstypes.TssKey{ + "K0": key("K0", pubkeyB), + "K1": key("K1", pubkeyA), + "K2": key("K2", pubkeyA), + }, + } +} + +func sweepWith(t *testing.T, store *mockStore, core *mockCore) *mockStore { + t.Helper() + NewSweeper(Config{Keyshares: store, PushCore: core, Logger: zerolog.Nop()}).sweep(context.Background()) + return store +} + +func TestSweep_DeletesSupersededSamePubkeyShare(t *testing.T) { + store := sweepWith(t, &mockStore{ids: []string{"K1", "K2"}}, baseCore()) + assert.Equal(t, []string{"K1"}, store.deleted) +} + +func TestSweep_KeepsCurrentKey(t *testing.T) { + store := sweepWith(t, &mockStore{ids: []string{"K1", "K2"}}, baseCore()) + assert.NotContains(t, store.deleted, "K2") +} + +// A rotated-away key (different pubkey) may still be needed to sign fund +// migration out of the retired vault, so it must survive. +func TestSweep_KeepsRotatedAwayPubkeyShare(t *testing.T) { + store := sweepWith(t, &mockStore{ids: []string{"K0", "K1", "K2"}}, baseCore()) + assert.NotContains(t, store.deleted, "K0") + assert.Equal(t, []string{"K1"}, store.deleted) +} + +// A share the chain doesn't know about is never deleted. +func TestSweep_KeepsUnknownKeyID(t *testing.T) { + store := sweepWith(t, &mockStore{ids: []string{"mystery", "K2"}}, baseCore()) + assert.Empty(t, store.deleted) +} + +func TestSweep_FailsClosedOnRPCError(t *testing.T) { + cases := map[string]func(*mockCore){ + "current key": func(c *mockCore) { c.currentErr = errors.New("boom") }, + "key lookup": func(c *mockCore) { c.keysErr = errors.New("boom") }, + } + for name, breakIt := range cases { + t.Run(name, func(t *testing.T) { + core := baseCore() + breakIt(core) + store := sweepWith(t, &mockStore{ids: []string{"K1", "K2"}}, core) + assert.Empty(t, store.deleted, "must not delete on incomplete chain state") + }) + } +} + +func TestSweep_NoopWhenSingleOrNoShare(t *testing.T) { + core := baseCore() + core.currentErr = errors.New("should not be called") + store := sweepWith(t, &mockStore{ids: []string{"K2"}}, core) + assert.Empty(t, store.deleted) +} + +func TestSweep_ContinuesAfterDeleteError(t *testing.T) { + store := &mockStore{ids: []string{"K1", "K2"}, delErr: errors.New("disk error")} + NewSweeper(Config{Keyshares: store, PushCore: baseCore(), Logger: zerolog.Nop()}). + sweep(context.Background()) + assert.Empty(t, store.deleted) +} + +func TestNewSweeper_DefaultInterval(t *testing.T) { + s := NewSweeper(Config{Keyshares: &mockStore{}, PushCore: baseCore(), Logger: zerolog.Nop()}) + require.Equal(t, defaultCheckInterval, s.checkInterval) +} + +// One unresolvable share must not block collection of the others; only the +// shares we hold are looked up, so the unbounded key history is never paged. +func TestSweep_StrayShareDoesNotBlockOthers(t *testing.T) { + store := sweepWith(t, &mockStore{ids: []string{"stray", "K1", "K2"}}, baseCore()) + assert.Equal(t, []string{"K1"}, store.deletedIDs()) +} + +// Start must be idempotent: a second call cannot spawn a concurrent sweeper. +func TestSweeper_StartIsIdempotent(t *testing.T) { + store := &mockStore{ids: []string{"K1", "K2"}} + s := NewSweeper(Config{ + Keyshares: store, + PushCore: baseCore(), + CheckInterval: 10 * time.Millisecond, + Logger: zerolog.Nop(), + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + for range 5 { + s.Start(ctx) + } + + // One loop deletes K1 exactly once; duplicates would retry the deleted id. + time.Sleep(60 * time.Millisecond) + cancel() + assert.Equal(t, []string{"K1"}, store.deletedIDs()) +} + +// The interval is long, so the first sweep must happen at start rather than +// after a full period — otherwise a frequently restarted node never sweeps. +func TestSweeper_SweepsOnStart(t *testing.T) { + store := &mockStore{ids: []string{"K1", "K2"}} + s := NewSweeper(Config{ + Keyshares: store, + PushCore: baseCore(), + CheckInterval: time.Hour, // far longer than the test waits + Logger: zerolog.Nop(), + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + s.Start(ctx) + + assert.Eventually(t, func() bool { + return len(store.deletedIDs()) == 1 + }, 2*time.Second, 10*time.Millisecond, "expected a sweep at start") + assert.Equal(t, []string{"K1"}, store.deletedIDs()) +} diff --git a/universalClient/tss/networking/libp2p/config.go b/universalClient/tss/networking/libp2p/config.go index 2acfa6328..e45e11d98 100644 --- a/universalClient/tss/networking/libp2p/config.go +++ b/universalClient/tss/networking/libp2p/config.go @@ -15,6 +15,11 @@ type Config struct { DialTimeout time.Duration // IOTimeout bounds stream read/write operations. IOTimeout time.Duration + // Authorizer reports whether a remote peer ID is allowed to connect and + // open TSS streams. When set, inbound connections from unauthorized peers + // are rejected at secured-connection admission and any stream that slips + // through is reset before reading. Nil disables gating (tests only). + Authorizer func(peerID string) bool } // setDefaults sets default values for unset fields. diff --git a/universalClient/tss/networking/libp2p/gater.go b/universalClient/tss/networking/libp2p/gater.go new file mode 100644 index 000000000..ac773e9ef --- /dev/null +++ b/universalClient/tss/networking/libp2p/gater.go @@ -0,0 +1,30 @@ +package libp2p + +import ( + "github.com/libp2p/go-libp2p/core/control" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + ma "github.com/multiformats/go-multiaddr" +) + +// validatorGater rejects inbound connections whose authenticated peer ID is +// not accepted by the authorizer. Outbound dials are not gated: this node only +// dials peers resolved from the validator set. +type validatorGater struct { + authorizer func(peerID string) bool +} + +func (g *validatorGater) InterceptPeerDial(peer.ID) bool { return true } +func (g *validatorGater) InterceptAddrDial(peer.ID, ma.Multiaddr) bool { return true } +func (g *validatorGater) InterceptAccept(network.ConnMultiaddrs) bool { return true } + +func (g *validatorGater) InterceptSecured(dir network.Direction, p peer.ID, _ network.ConnMultiaddrs) bool { + if dir == network.DirOutbound { + return true + } + return g.authorizer(p.String()) +} + +func (g *validatorGater) InterceptUpgraded(network.Conn) (bool, control.DisconnectReason) { + return true, 0 +} diff --git a/universalClient/tss/networking/libp2p/network.go b/universalClient/tss/networking/libp2p/network.go index 8710940c6..dec383032 100644 --- a/universalClient/tss/networking/libp2p/network.go +++ b/universalClient/tss/networking/libp2p/network.go @@ -31,6 +31,10 @@ import ( // observed DKLS Step() + coordinator.Message wrapping for our committee sizes. const MaxFrameSize = 1 * 1024 * 1024 // 1 MiB +// maxConcurrentReads bounds in-flight framed reads across all inbound TSS +// streams so slow peers cannot pin unbounded goroutines on blocking reads. +const maxConcurrentReads = 64 + // Network implements networking.Network using libp2p. type Network struct { cfg Config @@ -43,6 +47,8 @@ type Network struct { peerMu sync.RWMutex peers map[string]peer.AddrInfo + readSem chan struct{} + logger zerolog.Logger } @@ -58,10 +64,15 @@ func New(ctx context.Context, cfg Config, logger zerolog.Logger) (*Network, erro return nil, err } - host, err := libp2p.New( + opts := []libp2p.Option{ libp2p.Identity(priv), libp2p.ListenAddrStrings(cfg.ListenAddrs...), - ) + } + if cfg.Authorizer != nil { + opts = append(opts, libp2p.ConnectionGater(&validatorGater{authorizer: cfg.Authorizer})) + } + + host, err := libp2p.New(opts...) if err != nil { return nil, err } @@ -71,6 +82,7 @@ func New(ctx context.Context, cfg Config, logger zerolog.Logger) (*Network, erro host: host, protocolID: protocol.ID(cfg.ProtocolID), peers: make(map[string]peer.AddrInfo), + readSem: make(chan struct{}, maxConcurrentReads), logger: logger.With().Str("component", "networking_libp2p").Logger(), } @@ -194,6 +206,25 @@ func (n *Network) lookupPeer(peerID string) (peer.AddrInfo, error) { } func (n *Network) handleStream(stream network.Stream) { + remotePeer := stream.Conn().RemotePeer().String() + // Recheck authorization per stream: the gater only runs at connection + // admission, so this covers peers removed from the validator set while a + // connection is still open. + if n.cfg.Authorizer != nil && !n.cfg.Authorizer(remotePeer) { + n.logger.Warn().Str("peer_id", remotePeer).Msg("resetting stream from unauthorized peer") + _ = stream.Reset() + return + } + + select { + case n.readSem <- struct{}{}: + default: + n.logger.Warn().Str("peer_id", remotePeer).Msg("concurrent read limit reached, resetting stream") + _ = stream.Reset() + return + } + defer func() { <-n.readSem }() + defer stream.Close() if deadline := time.Now().Add(n.cfg.IOTimeout); true { @@ -214,7 +245,7 @@ func (n *Network) handleStream(stream network.Stream) { } // Call handler in a goroutine to avoid blocking - go handler(stream.Conn().RemotePeer().String(), data) + go handler(remotePeer, data) } func loadIdentity(base64Key string) (crypto.PrivKey, error) { diff --git a/universalClient/tss/networking/libp2p/network_test.go b/universalClient/tss/networking/libp2p/network_test.go index 8c9846684..f09aa1e7c 100644 --- a/universalClient/tss/networking/libp2p/network_test.go +++ b/universalClient/tss/networking/libp2p/network_test.go @@ -2,10 +2,15 @@ package libp2p import ( "bytes" + "context" "encoding/binary" + "fmt" "io" + "sync" "testing" + "time" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -78,3 +83,143 @@ func TestWriteFramed_AcceptsAtMaxFrameSize(t *testing.T) { assert.Equal(t, payload[len(payload)-1], got[len(got)-1]) } +// allowlist is a mutable peer-ID allowlist used as a test Authorizer. +type allowlist struct { + mu sync.RWMutex + peers map[string]bool +} + +func newAllowlist() *allowlist { + return &allowlist{peers: make(map[string]bool)} +} + +func (a *allowlist) allow(peerID string) { + a.mu.Lock() + a.peers[peerID] = true + a.mu.Unlock() +} + +func (a *allowlist) revoke(peerID string) { + a.mu.Lock() + delete(a.peers, peerID) + a.mu.Unlock() +} + +func (a *allowlist) authorized(peerID string) bool { + a.mu.RLock() + defer a.mu.RUnlock() + return a.peers[peerID] +} + +func newTestNetwork(t *testing.T, authorizer func(string) bool) *Network { + t.Helper() + n, err := New(context.Background(), Config{ + ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, + DialTimeout: 5 * time.Second, + IOTimeout: 5 * time.Second, + Authorizer: authorizer, + }, zerolog.New(io.Discard)) + require.NoError(t, err) + t.Cleanup(func() { _ = n.Close() }) + return n +} + +func connectPeer(t *testing.T, from *Network, to *Network) { + t.Helper() + require.NoError(t, from.EnsurePeer(to.ID(), to.ListenAddrs())) +} + +func collectMessages(t *testing.T, n *Network) <-chan string { + t.Helper() + msgs := make(chan string, 64) + require.NoError(t, n.RegisterHandler(func(peerID string, data []byte) { + msgs <- peerID + ":" + string(data) + })) + return msgs +} + +func TestNetwork_RejectsUnknownPeer(t *testing.T) { + acl := newAllowlist() + receiver := newTestNetwork(t, acl.authorized) + rogue := newTestNetwork(t, nil) + msgs := collectMessages(t, receiver) + + connectPeer(t, rogue, receiver) + err := rogue.Send(context.Background(), receiver.ID(), []byte("intrusion")) + require.Error(t, err, "unauthenticated peer must not reach the TSS protocol") + + select { + case m := <-msgs: + t.Fatalf("handler received message from unauthorized peer: %s", m) + case <-time.After(500 * time.Millisecond): + } +} + +func TestNetwork_AuthorizedPeerDeliversDuringUnauthenticatedFlood(t *testing.T) { + acl := newAllowlist() + receiver := newTestNetwork(t, acl.authorized) + validator := newTestNetwork(t, nil) + acl.allow(validator.ID()) + msgs := collectMessages(t, receiver) + + const rogues = 8 + var wg sync.WaitGroup + for i := range rogues { + rogue := newTestNetwork(t, nil) + connectPeer(t, rogue, receiver) + wg.Add(1) + go func(r *Network, i int) { + defer wg.Done() + for j := range 5 { + _ = r.Send(context.Background(), receiver.ID(), fmt.Appendf(nil, "flood-%d-%d", i, j)) + } + }(rogue, i) + } + + connectPeer(t, validator, receiver) + require.NoError(t, validator.Send(context.Background(), receiver.ID(), []byte("ack"))) + wg.Wait() + + select { + case m := <-msgs: + assert.Equal(t, validator.ID()+":ack", m) + case <-time.After(5 * time.Second): + t.Fatal("validator message not delivered during unauthenticated flood") + } + + select { + case m := <-msgs: + t.Fatalf("received unexpected message: %s", m) + case <-time.After(500 * time.Millisecond): + } +} + +func TestNetwork_ResetsStreamAfterPeerRevoked(t *testing.T) { + acl := newAllowlist() + receiver := newTestNetwork(t, acl.authorized) + validator := newTestNetwork(t, nil) + acl.allow(validator.ID()) + msgs := collectMessages(t, receiver) + + connectPeer(t, validator, receiver) + require.NoError(t, validator.Send(context.Background(), receiver.ID(), []byte("before"))) + select { + case m := <-msgs: + assert.Equal(t, validator.ID()+":before", m) + case <-time.After(5 * time.Second): + t.Fatal("message from authorized peer not delivered") + } + + // Revoke: the existing connection survives the gater, but handleStream + // must reset new streams from the now-unauthorized peer. + acl.revoke(validator.ID()) + _ = validator.Send(context.Background(), receiver.ID(), []byte("after")) + + select { + case m := <-msgs: + t.Fatalf("handler received message from revoked peer: %s", m) + case <-time.After(500 * time.Millisecond): + } +} + + diff --git a/universalClient/tss/sessionmanager/fund_migrate_e2e_test.go b/universalClient/tss/sessionmanager/fund_migrate_e2e_test.go new file mode 100644 index 000000000..685295d7c --- /dev/null +++ b/universalClient/tss/sessionmanager/fund_migrate_e2e_test.go @@ -0,0 +1,263 @@ +package sessionmanager + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/universalClient/store" + "github.com/pushchain/push-chain-node/universalClient/tss/coordinator" + utsstypes "github.com/pushchain/push-chain-node/x/utss/types" + "github.com/pushchain/push-chain-node/x/uvalidator/types" +) + +func fundMigrateStoreEvent(t *testing.T, oldKeyID string) *store.Event { + t.Helper() + data, err := json.Marshal(utsstypes.FundMigrationInitiatedEventData{OldKeyID: oldKeyID}) + require.NoError(t, err) + return &store.Event{ + EventID: "fm-e2e", + Type: store.EventTypeSignFundMigrate, + EventData: data, + } +} + +func activeValidators(addrs ...string) []*types.UniversalValidator { + set := make([]*types.UniversalValidator, 0, len(addrs)) + for _, a := range addrs { + set = append(set, makeActiveValidator(a)) + } + return set +} + +func partyIDs(vs []*types.UniversalValidator) []string { + ids := make([]string, 0, len(vs)) + for _, v := range vs { + ids = append(ids, v.IdentifyInfo.CoreValidatorAddress) + } + return ids +} + +// End to end across both components: the coordinator selects the participants, +// then a participant validates the setup message it receives. +// +// The two sides derive the answer independently, so a change to one that the +// other does not mirror leaves a selection the coordinator can legitimately +// make and every participant rejects. Neither side's own tests catch that. +func TestFundMigrate_CoordinatorSelectionPassesParticipantValidation(t *testing.T) { + ctx := context.Background() + + // The finding's scenario: the old key has 3 shareholders and the validator + // set has since grown to 10. + oldKey := &utsstypes.TssKey{KeyId: "old-key", Participants: []string{"v1", "v2", "v3"}} + + sm, coord, _, _, _, _ := setupTestSessionManager(t) + setCoordinatorPushCore(coord, &mockPushCore{ + keysByID: map[string]*utsstypes.TssKey{"old-key": oldKey}, + }) + setCoordinatorValidators(coord, activeValidators( + "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10")) + + event := fundMigrateStoreEvent(t, "old-key") + + // Selection is randomised, so repeat rather than trusting one draw. + for i := 0; i < 100; i++ { + selected, err := coord.SelectParticipants(ctx, *event, coord.Validators()) + require.NoError(t, err, "coordinator could not select signers") + + ids := partyIDs(selected) + assert.ElementsMatch(t, []string{"v1", "v2", "v3"}, ids, + "coordinator selected a validator that holds no share of the old key") + + require.NoError(t, sm.validateParticipants(ctx, ids, event), + "participant rejected a selection the coordinator legitimately made") + } +} + +// The same round trip for an outbound, which must keep using the current +// validator set on both sides. +func TestSignOutbound_CoordinatorSelectionPassesParticipantValidation(t *testing.T) { + ctx := context.Background() + + sm, coord, _, _, _, _ := setupTestSessionManager(t) + all := activeValidators("v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10") + setCoordinatorValidators(coord, all) + + event := &store.Event{EventID: "ob-e2e", Type: store.EventTypeSignOutbound} + + for i := 0; i < 100; i++ { + selected, err := coord.SelectParticipants(ctx, *event, coord.Validators()) + require.NoError(t, err) + + ids := partyIDs(selected) + require.Len(t, ids, coordinator.CalculateThreshold(len(all))) + require.NoError(t, sm.validateParticipants(ctx, ids, event)) + } +} + +// Validation must be tied to the old key, not merely lenient. A set that meets +// the count but contains a validator holding no share is still rejected. +func TestFundMigrate_ValidationRejectsNonShareholders(t *testing.T) { + ctx := context.Background() + + sm, coord, _, _, _, _ := setupTestSessionManager(t) + setCoordinatorPushCore(coord, &mockPushCore{ + keysByID: map[string]*utsstypes.TssKey{ + "old-key": {KeyId: "old-key", Participants: []string{"v1", "v2", "v3"}}, + }, + }) + setCoordinatorValidators(coord, activeValidators( + "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10")) + + event := fundMigrateStoreEvent(t, "old-key") + + t.Run("newcomer in an otherwise valid set", func(t *testing.T) { + err := sm.validateParticipants(ctx, []string{"v1", "v2", "v10"}, event) + require.Error(t, err) + assert.Contains(t, err.Error(), "v10") + }) + + t.Run("all newcomers, count satisfied", func(t *testing.T) { + err := sm.validateParticipants(ctx, []string{"v8", "v9", "v10"}, event) + require.Error(t, err) + }) + + t.Run("below the old key threshold", func(t *testing.T) { + err := sm.validateParticipants(ctx, []string{"v1", "v2"}, event) + require.Error(t, err) + assert.Contains(t, err.Error(), "below required threshold 3") + }) + + t.Run("exactly the shareholders is accepted", func(t *testing.T) { + require.NoError(t, sm.validateParticipants(ctx, []string{"v1", "v2", "v3"}, event)) + }) +} + +// A larger old key, so the accepted count is a strict subset of shareholders +// rather than all of them, and the current set is not what sizes it. +func TestFundMigrate_ValidationUsesOldKeyThresholdNotCurrentSet(t *testing.T) { + ctx := context.Background() + + sm, coord, _, _, _, _ := setupTestSessionManager(t) + setCoordinatorPushCore(coord, &mockPushCore{ + keysByID: map[string]*utsstypes.TssKey{ + "old-key": {KeyId: "old-key", Participants: []string{"v1", "v2", "v3", "v4", "v5", "v6"}}, + }, + }) + // 6 shareholders among 12 validators. Old key threshold is 5, the current + // set's would be 9, which no set of shareholders could ever satisfy. + setCoordinatorValidators(coord, activeValidators( + "v1", "v2", "v3", "v4", "v5", "v6", "n1", "n2", "n3", "n4", "n5", "n6")) + + event := fundMigrateStoreEvent(t, "old-key") + + require.Equal(t, 5, coordinator.CalculateThreshold(6)) + require.Equal(t, 9, coordinator.CalculateThreshold(12)) + + t.Run("old key threshold is accepted", func(t *testing.T) { + require.NoError(t, sm.validateParticipants(ctx, []string{"v1", "v2", "v3", "v4", "v5"}, event)) + }) + + t.Run("all shareholders is accepted", func(t *testing.T) { + require.NoError(t, sm.validateParticipants(ctx, []string{"v1", "v2", "v3", "v4", "v5", "v6"}, event)) + }) + + t.Run("one below the old key threshold is rejected", func(t *testing.T) { + err := sm.validateParticipants(ctx, []string{"v1", "v2", "v3", "v4"}, event) + require.Error(t, err) + assert.Contains(t, err.Error(), "below required threshold 5") + }) +} + +// Some shareholders are gone but enough remain to sign. The threshold must +// still be the old key's, not one derived from the survivors: deriving it from +// the survivors lowers the bar every time a shareholder drops out, so a +// coordinator could open a session below the quorum the key was created under. +func TestFundMigrate_ValidationThresholdDoesNotShrinkWithSurvivors(t *testing.T) { + ctx := context.Background() + + sm, coord, _, _, _, _ := setupTestSessionManager(t) + setCoordinatorPushCore(coord, &mockPushCore{ + keysByID: map[string]*utsstypes.TssKey{ + "old-key": {KeyId: "old-key", Participants: []string{"v1", "v2", "v3", "v4", "v5", "v6"}}, + }, + }) + // v6 is gone, so 5 of the 6 shareholders survive. The old key still requires + // 5, while a threshold over the survivors would be only 4. + setCoordinatorValidators(coord, activeValidators("v1", "v2", "v3", "v4", "v5", "n1", "n2", "n3")) + + event := fundMigrateStoreEvent(t, "old-key") + + require.Equal(t, 5, coordinator.CalculateThreshold(6), "old key threshold") + require.Equal(t, 4, coordinator.CalculateThreshold(5), "threshold over survivors") + + t.Run("four survivors is below the old key threshold", func(t *testing.T) { + err := sm.validateParticipants(ctx, []string{"v1", "v2", "v3", "v4"}, event) + require.Error(t, err) + assert.Contains(t, err.Error(), "below required threshold 5") + }) + + t.Run("all five survivors is accepted", func(t *testing.T) { + require.NoError(t, sm.validateParticipants(ctx, []string{"v1", "v2", "v3", "v4", "v5"}, event)) + }) + + t.Run("the coordinator selects exactly those five", func(t *testing.T) { + selected, err := coord.SelectParticipants(ctx, *event, coord.Validators()) + require.NoError(t, err) + ids := partyIDs(selected) + assert.ElementsMatch(t, []string{"v1", "v2", "v3", "v4", "v5"}, ids) + require.NoError(t, sm.validateParticipants(ctx, ids, event)) + }) +} + +// Too few shareholders left to sign at all. Both sides must refuse, and the +// coordinator must not dispatch a set it knows cannot reach quorum. +func TestFundMigrate_BothSidesFailClosedWhenShareholdersGone(t *testing.T) { + ctx := context.Background() + + sm, coord, _, _, _, _ := setupTestSessionManager(t) + setCoordinatorPushCore(coord, &mockPushCore{ + keysByID: map[string]*utsstypes.TssKey{ + "old-key": {KeyId: "old-key", Participants: []string{"v1", "v2", "v3", "v4", "v5", "v6"}}, + }, + }) + // Only 4 of the 6 shareholders remain, one short of the threshold of 5. + setCoordinatorValidators(coord, activeValidators("v1", "v2", "v3", "v4", "n1", "n2", "n3", "n4")) + + event := fundMigrateStoreEvent(t, "old-key") + + _, err := coord.SelectParticipants(ctx, *event, coord.Validators()) + require.Error(t, err, "coordinator dispatched a set that cannot reach quorum") + + err = sm.validateParticipants(ctx, []string{"v1", "v2", "v3", "v4"}, event) + require.Error(t, err) +} + +// Validation must not fall open when the old key cannot be resolved. +func TestFundMigrate_ValidationFailsClosedOnUnresolvableKey(t *testing.T) { + ctx := context.Background() + + sm, coord, _, _, _, _ := setupTestSessionManager(t) + setCoordinatorValidators(coord, activeValidators("v1", "v2", "v3", "v4", "v5")) + + // The default mock returns a key with no participants for any id. + setCoordinatorPushCore(coord, &mockPushCore{}) + + err := sm.validateParticipants(ctx, []string{"v1", "v2", "v3", "v4"}, fundMigrateStoreEvent(t, "old-key")) + require.Error(t, err) + assert.Contains(t, err.Error(), "resolve fund migration signers") + + t.Run("malformed event data", func(t *testing.T) { + event := &store.Event{ + EventID: "fm-bad", + Type: store.EventTypeSignFundMigrate, + EventData: []byte("not json"), + } + err := sm.validateParticipants(ctx, []string{"v1", "v2", "v3", "v4"}, event) + require.Error(t, err) + assert.Contains(t, err.Error(), "resolve fund migration signers") + }) +} diff --git a/universalClient/tss/sessionmanager/sessionmanager.go b/universalClient/tss/sessionmanager/sessionmanager.go index c3967df6b..1fed06be4 100644 --- a/universalClient/tss/sessionmanager/sessionmanager.go +++ b/universalClient/tss/sessionmanager/sessionmanager.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "math/big" + "slices" "sync" "time" @@ -24,6 +25,7 @@ import ( "github.com/pushchain/push-chain-node/universalClient/tss/keyshare" uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" utsstypes "github.com/pushchain/push-chain-node/x/utss/types" + uvalidatortypes "github.com/pushchain/push-chain-node/x/uvalidator/types" ) // SendFunc is a function type for sending messages to participants. @@ -178,7 +180,7 @@ func (sm *SessionManager) handleSetupMessage(ctx context.Context, senderPeerID s } // 5. Validate participants list matches event protocol requirements - if err := sm.validateParticipants(msg.Participants, event); err != nil { + if err := sm.validateParticipants(ctx, msg.Participants, event); err != nil { return fmt.Errorf("participants validation failed: %w", err) } @@ -196,6 +198,18 @@ func (sm *SessionManager) handleSetupMessage(ctx context.Context, senderPeerID s } } + // 6c. Everything validated above came from message fields, but the DKLS + // session runs on msg.Payload, and the two arrive unbound. Require the setup + // blob to carry exactly what we approved, before the ACK, so no shares are + // ever produced for a setup we did not verify. + if err := verifySetupMatchesValidated(msg, event.Type); err != nil { + sm.logger.Error().Err(err). + Str("event_id", msg.EventID). + Str("coordinator", senderPeerID). + Msg("setup message does not match the validated request - rejecting") + return err + } + // 7. Create session based on protocol type session, err := sm.createSession(ctx, event, msg) if err != nil { @@ -414,9 +428,8 @@ func (sm *SessionManager) handleSignatureBroadcast(ctx context.Context, senderPe // Persist as SIGNED via the same path a local sign-completion uses, so // signing_data lands on event_data in the format txbroadcaster expects. rebuiltReq := &common.UnsignedSigningReq{ - SigningHash: msg.SignedData.SigningHash, - Nonce: msg.SignedData.Nonce, - TSSFundMigrationAmount: msg.SignedData.TSSFundMigrationAmount, + SigningHash: msg.SignedData.SigningHash, + Nonce: msg.SignedData.Nonce, } if err := sm.handleSigningComplete(ctx, msg.EventID, event.EventData, msg.SignedData.Signature, rebuiltReq); err != nil { return fmt.Errorf("persist signature from broadcast: %w", err) @@ -502,10 +515,9 @@ func (sm *SessionManager) handleSignFinished(ctx context.Context, eventID string // SIGNED and can vote on failure. Best-effort: failed sends are logged but // do not abort. Recovery via sweeper retry covers any peers we miss. sm.broadcastSignature(ctx, eventID, &coordinator.SignedDataPayload{ - Signature: result.Signature, - SigningHash: signingReq.SigningHash, - Nonce: signingReq.Nonce, - TSSFundMigrationAmount: signingReq.TSSFundMigrationAmount, + Signature: result.Signature, + SigningHash: signingReq.SigningHash, + Nonce: signingReq.Nonce, }) sm.logger.Info().Str("event_id", eventID).Msg("sign session finished successfully") @@ -739,9 +751,24 @@ func (sm *SessionManager) createSession(ctx context.Context, event *store.Event, // validateParticipants validates that participants match protocol requirements. // For keygen/keyrefresh: participants must match exactly with eligible participants (same elements). // For sign: participants must be a valid >2/3 subset of eligible participants. -func (sm *SessionManager) validateParticipants(participants []string, event *store.Event) error { - // Get eligible validators for this protocol - eligible := sm.coordinator.GetEligibleUV(string(event.Type)) +func (sm *SessionManager) validateParticipants(ctx context.Context, participants []string, event *store.Event) error { + // Get eligible validators for this protocol. + // + // Fund migration is signed with the old key's shares, so both who may take + // part and how many are required come from that key rather than from the + // current validator set. Resolved through the coordinator so the check here + // mirrors the selection exactly. + var eligible []*uvalidatortypes.UniversalValidator + var fundMigrateRequired int + if event.Type == store.EventTypeSignFundMigrate { + var err error + eligible, fundMigrateRequired, err = sm.coordinator.FundMigrateEligible(ctx, *event) + if err != nil { + return fmt.Errorf("resolve fund migration signers: %w", err) + } + } else { + eligible = sm.coordinator.GetEligibleUV(string(event.Type)) + } if len(eligible) == 0 { return fmt.Errorf("no eligible validators for protocol") } @@ -780,8 +807,8 @@ func (sm *SessionManager) validateParticipants(participants []string, event *sto } } - case store.EventTypeSignOutbound, store.EventTypeSignFundMigrate: - // For SIGN and FUND_MIGRATE the coordinator picks a random threshold subset (>2/3 of eligible) + case store.EventTypeSignOutbound: + // For SIGN the coordinator picks a random threshold subset (>2/3 of eligible) // rather than all eligible validators. Accept any subset as long as it meets the threshold // minimum; all participants are already verified eligible by the eligibleSet check above. threshold := coordinator.CalculateThreshold(len(eligibleList)) @@ -790,6 +817,15 @@ func (sm *SessionManager) validateParticipants(participants []string, event *sto event.Type, len(participants), threshold, len(eligibleList)) } + case store.EventTypeSignFundMigrate: + // The old key's threshold, not the current set's. Sizing this from the + // live validator set would reject a legitimate selection whenever the + // set has grown since that key was created. + if len(participants) < fundMigrateRequired { + return fmt.Errorf("%s participants count %d is below required threshold %d (shareholders still eligible: %d)", + event.Type, len(participants), fundMigrateRequired, len(eligibleList)) + } + default: return fmt.Errorf("unknown protocol type: %s", event.Type) } @@ -940,16 +976,15 @@ func (sm *SessionManager) verifyOutboundSigningRequest(ctx context.Context, even return nil } - // Guard against stale / replayed nonces: reject if coordinator's nonce is below the - // last finalized nonce on chain (i.e. that nonce has already been committed). + // Bound the coordinator's nonce on both sides: below finalized it is already + // committed, and far above it would never mine, freezing the outbound. // We only hard-reject on a definitive answer — warn and skip if we can't determine it. if tssAddr, addrErr := sm.getTSSAddress(ctx); addrErr != nil { sm.logger.Warn().Err(addrErr).Str("chain", chainID).Msg("cannot get TSS address for nonce check, skipping") - } else if finalizedNonce, nonceErr := builder.GetNextNonce(ctx, tssAddr, true /* useFinalized */); nonceErr != nil { + } else if finalizedNonce, ceilingBase, nonceErr := nonceBounds(ctx, builder, tssAddr); nonceErr != nil { sm.logger.Warn().Err(nonceErr).Str("chain", chainID).Msg("cannot get finalized nonce for check, skipping") - } else if req.Nonce < finalizedNonce { - return fmt.Errorf("coordinator assigned nonce %d is below chain finalized nonce %d for %s — nonce already used on chain", - req.Nonce, finalizedNonce, chainID) + } else if err := checkNonceInRange(req.Nonce, finalizedNonce, ceilingBase, chainID); err != nil { + return err } // Use coordinator's nonce so our computed hash matches @@ -977,6 +1012,124 @@ func (sm *SessionManager) verifyOutboundSigningRequest(ctx context.Context, even return nil } +// verifySetupMatchesValidated requires the coordinator's DKLS setup blob to +// carry exactly the values the follower validated from the message fields. +// DKLS runs on the blob, so without this the validated values are decorative: +// a coordinator can present legitimate ones for checking and embed different +// ones in Payload. +// +// Participants are checked for every protocol. Sign types additionally bind the +// signing hash; key-lifecycle types additionally bind the threshold, which the +// session constructors accept but ignore, so the embedded value is what the +// protocol actually runs with. +func verifySetupMatchesValidated(msg *coordinator.Message, eventType string) error { + if err := setupBindsParticipants(msg.Payload, msg.Participants); err != nil { + return err + } + if eventType == store.EventTypeSignOutbound || eventType == store.EventTypeSignFundMigrate { + if msg.UnsignedSigningReq == nil { + return fmt.Errorf("sign setup has no signing request to bind against") + } + return setupBindsHash(msg.Payload, msg.UnsignedSigningReq.SigningHash) + } + return setupBindsThreshold(msg.Payload, msg.Participants) +} + +// setupBindsThreshold requires the setup blob to embed the threshold the +// follower derives from the validated participants. Without it a coordinator can +// embed a lower one and elicit help producing a weaker key than was agreed. +func setupBindsThreshold(setupData []byte, validated []string) error { + expected := coordinator.CalculateThreshold(len(validated)) + embedded, err := dkls.SetupThreshold(setupData) + if err != nil { + return fmt.Errorf("cannot decode setup message threshold: %w", err) + } + if embedded != expected { + return fmt.Errorf("setup message threshold %d does not match expected %d for %d participants", + embedded, expected, len(validated)) + } + return nil +} + +// setupBindsHash requires the setup blob to embed exactly the verified hash. +func setupBindsHash(setupData, verifiedHash []byte) error { + if len(verifiedHash) == 0 { + return fmt.Errorf("no verified signing hash to bind setup message to") + } + embedded, err := dkls.SetupMessageHash(setupData) + if err != nil { + return fmt.Errorf("cannot decode setup message to check signing hash: %w", err) + } + if !bytes.Equal(embedded, verifiedHash) { + return fmt.Errorf("setup message signs hash %s but verified hash is %s", + hex.EncodeToString(embedded), hex.EncodeToString(verifiedHash)) + } + return nil +} + +// setupBindsParticipants requires the setup blob to embed exactly the validated +// participants, in the same order. Index order is part of the protocol, so a +// reorder is as consequential as a substitution. +func setupBindsParticipants(setupData []byte, validated []string) error { + if len(validated) == 0 { + return fmt.Errorf("no validated participants to bind setup message to") + } + embedded, err := dkls.SetupParticipants(setupData) + if err != nil { + return fmt.Errorf("cannot decode setup message participants: %w", err) + } + if !slices.Equal(embedded, validated) { + return fmt.Errorf("setup message participants %v do not match validated participants %v", + embedded, validated) + } + return nil +} + +// maxNonceGap bounds how far above the ceiling base a coordinator may assign. +// An honest coordinator starts at the pending nonce and increments at most +// coordinator.PerChainCap times per poll, so pending+PerChainCap is the true +// ceiling; 2x absorbs nonce skew between our RPC view and the coordinator's. +// +// The base is the pending nonce, not the finalized one: a BROADCASTED event no +// longer counts toward the in-flight cap but still holds a nonce in the +// mempool, so pending-minus-finalized grows while a chain is congested. Anchored +// to finalized, any fixed gap would eventually reject honest coordinators. +const maxNonceGap = 2 * coordinator.PerChainCap + +// checkNonceInRange rejects a coordinator-assigned nonce that is already +// committed on chain, or so far ahead it would never mine — which would freeze +// the outbound with its PRC20 already burned at the gateway. +// ceilingBase is the pending nonce where available, else the finalized nonce. +func checkNonceInRange(assigned, finalized, ceilingBase uint64, target string) error { + if assigned < finalized { + return fmt.Errorf("coordinator assigned nonce %d is below chain finalized nonce %d for %s — nonce already used on chain", + assigned, finalized, target) + } + if ceilingBase < finalized { + ceilingBase = finalized + } + if assigned > ceilingBase+maxNonceGap { + return fmt.Errorf("coordinator assigned nonce %d exceeds nonce %d by more than %d for %s — gap nonce would never mine", + assigned, ceilingBase, maxNonceGap, target) + } + return nil +} + +// nonceBounds returns the finalized nonce and the ceiling base for signer. +// A failed pending lookup falls back to the finalized nonce, which is stricter +// but never wrong; a failed finalized lookup is reported so the caller skips. +func nonceBounds(ctx context.Context, builder common.TxBuilder, signer string) (finalized, ceilingBase uint64, err error) { + finalized, err = builder.GetNextNonce(ctx, signer, true /* useFinalized */) + if err != nil { + return 0, 0, err + } + pending, pErr := builder.GetNextNonce(ctx, signer, false /* pending */) + if pErr != nil || pending < finalized { + return finalized, finalized, nil + } + return finalized, pending, nil +} + // verifyFundMigrationSigningRequest validates the coordinator's fund migration signing request. // It independently rebuilds the signing hash from the event data and compares it with the coordinator's hash. func (sm *SessionManager) verifyFundMigrationSigningRequest(ctx context.Context, event *store.Event, req *common.UnsignedSigningReq) error { @@ -1003,6 +1156,10 @@ func (sm *SessionManager) verifyFundMigrationSigningRequest(ctx context.Context, if err != nil { return fmt.Errorf("failed to derive current TSS address: %w", err) } + transferAmount, ok := new(big.Int).SetString(migrationData.TransferAmount, 10) + if !ok || transferAmount.Sign() <= 0 { + return fmt.Errorf("migration event carries no usable transfer amount: %q", migrationData.TransferAmount) + } // Get chain client and tx builder if sm.chains == nil { @@ -1020,15 +1177,12 @@ func (sm *SessionManager) verifyFundMigrationSigningRequest(ctx context.Context, return nil } - // Guard against stale / replayed nonces: reject if coordinator's nonce is below the - // last finalized nonce on chain for the old TSS address. - if finalizedNonce, nonceErr := builder.GetNextNonce(ctx, oldTSSAddr, true /* useFinalized */); nonceErr != nil { + // Bound the coordinator's nonce on both sides for the old TSS address. + if finalizedNonce, ceilingBase, nonceErr := nonceBounds(ctx, builder, oldTSSAddr); nonceErr != nil { sm.logger.Warn().Err(nonceErr).Str("chain", migrationData.Chain).Msg("cannot get finalized nonce for old TSS, skipping nonce check") - } else if req.Nonce < finalizedNonce { - return fmt.Errorf("coordinator assigned nonce %d is below chain finalized nonce %d for old TSS %s — nonce already used on chain", - req.Nonce, finalizedNonce, oldTSSAddr) + } else if err := checkNonceInRange(req.Nonce, finalizedNonce, ceilingBase, oldTSSAddr); err != nil { + return err } - // Rebuild fund migration signing request with coordinator's nonce. // Parsing must match what the coordinator did; otherwise the reconstructed // hash on OP-stack chains diverges and the verification below rejects it. @@ -1039,11 +1193,12 @@ func (sm *SessionManager) verifyFundMigrationSigningRequest(ctx context.Context, l1GasFee.SetString(migrationData.L1GasFee, 10) migrationFundData := &common.FundMigrationData{ - From: oldTSSAddr, - To: currentTSSAddr, - GasPrice: gasPrice, - GasLimit: migrationData.GasLimit, - L1GasFee: l1GasFee, + From: oldTSSAddr, + To: currentTSSAddr, + GasPrice: gasPrice, + GasLimit: migrationData.GasLimit, + L1GasFee: l1GasFee, + TransferAmount: transferAmount, } signingReq, err := builder.GetFundMigrationSigningRequest(ctx, migrationFundData, req.Nonce) if err != nil { @@ -1060,23 +1215,12 @@ func (sm *SessionManager) verifyFundMigrationSigningRequest(ctx context.Context, return fmt.Errorf("fund migration signing hash mismatch: our computed hash does not match coordinator's hash") } - // Defense-in-depth: hash match implies amount match, but cross-check explicitly so - // a wire-format bug, coordinator bug, or missing amount surfaces here rather than - // as a nil-deref / insufficient-balance error later in broadcast. - if req.TSSFundMigrationAmount == nil { - return fmt.Errorf("coordinator's signing request is missing TSSFundMigrationAmount") - } - if req.TSSFundMigrationAmount.Cmp(signingReq.TSSFundMigrationAmount) != 0 { - return fmt.Errorf("TSSFundMigrationAmount mismatch: coordinator=%s ours=%s", - req.TSSFundMigrationAmount.String(), signingReq.TSSFundMigrationAmount.String()) - } - sm.logger.Debug(). Str("event_id", event.EventID). Str("signing_hash", hex.EncodeToString(req.SigningHash)). Str("old_tss_addr", oldTSSAddr). Str("current_tss_addr", currentTSSAddr). - Msg("fund migration sign metadata verified - hash and amount match") + Msg("fund migration sign metadata verified") return nil } @@ -1091,8 +1235,6 @@ func (sm *SessionManager) getTSSAddress(ctx context.Context) (string, error) { } // handleSigningComplete handles post-sign steps. EVM: set status SIGNED and store payload (txlifecycle/signed runs BroadcastOutboundSigningRequest). Solana: enqueue for sequential per-chain broadcast (PDA nonce order). -// signingReq is the cached signing request from the coordinator setup message; for FUND_MIGRATE -// its TSSFundMigrationAmount is populated by verifyFundMigrationSigningRequest and persisted here. func (sm *SessionManager) handleSigningComplete(_ context.Context, eventID string, eventData []byte, signature []byte, signingReq *common.UnsignedSigningReq) error { if signingReq == nil { return fmt.Errorf("signing request is nil - cannot persist signing data") @@ -1104,7 +1246,6 @@ func (sm *SessionManager) handleSigningComplete(_ context.Context, eventID strin signature, signingReq.SigningHash, signingReq.Nonce, - signingReq.TSSFundMigrationAmount, ) if err != nil { return fmt.Errorf("failed to persist signing data: %w", err) @@ -1131,10 +1272,9 @@ func extractSignedDataFromEvent(event *store.Event) (*coordinator.SignedDataPayl } var raw struct { SigningData *struct { - Signature string `json:"signature"` - SigningHash string `json:"signing_hash"` - Nonce uint64 `json:"nonce"` - TSSFundMigrationAmount *big.Int `json:"tss_fund_migration_amount,omitempty"` + Signature string `json:"signature"` + SigningHash string `json:"signing_hash"` + Nonce uint64 `json:"nonce"` } `json:"signing_data,omitempty"` } if err := json.Unmarshal(event.EventData, &raw); err != nil { @@ -1152,9 +1292,8 @@ func extractSignedDataFromEvent(event *store.Event) (*coordinator.SignedDataPayl return nil, fmt.Errorf("decode signing_data.signing_hash hex: %w", err) } return &coordinator.SignedDataPayload{ - Signature: sigBytes, - SigningHash: hashBytes, - Nonce: raw.SigningData.Nonce, - TSSFundMigrationAmount: raw.SigningData.TSSFundMigrationAmount, + Signature: sigBytes, + SigningHash: hashBytes, + Nonce: raw.SigningData.Nonce, }, nil } diff --git a/universalClient/tss/sessionmanager/sessionmanager_test.go b/universalClient/tss/sessionmanager/sessionmanager_test.go index a54922c40..6f199c4d3 100644 --- a/universalClient/tss/sessionmanager/sessionmanager_test.go +++ b/universalClient/tss/sessionmanager/sessionmanager_test.go @@ -5,13 +5,16 @@ import ( "context" "encoding/hex" "encoding/json" + "errors" "fmt" - "math/big" "reflect" + "strings" "testing" "time" "unsafe" + session "go-wrapper/go-dkls/sessions" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -49,6 +52,9 @@ func containsAny(s string, substrings []string) bool { // block height (0 by default) so coordinator-at-block math is deterministic. type mockPushCore struct { block uint64 + + // Old key history, consulted when validating fund migration signers. + keysByID map[string]*utsstypes.TssKey } func (m *mockPushCore) GetLatestBlock(_ context.Context) (uint64, error) { @@ -59,6 +65,13 @@ func (m *mockPushCore) GetCurrentKey(_ context.Context) (*utsstypes.TssKey, erro return &utsstypes.TssKey{KeyId: "test-key"}, nil } +func (m *mockPushCore) GetKeyByID(_ context.Context, keyID string) (*utsstypes.TssKey, error) { + if key, ok := m.keysByID[keyID]; ok { + return key, nil + } + return &utsstypes.TssKey{KeyId: keyID}, nil +} + func (m *mockPushCore) GetAllUniversalValidators(_ context.Context) ([]*types.UniversalValidator, error) { return nil, nil } @@ -363,6 +376,11 @@ func setCoordinatorValidators(coord *coordinator.Coordinator, validators []*type if field.IsValid() { *(*[]*types.UniversalValidator)(unsafe.Pointer(field.UnsafeAddr())) = validators } + // Keep the cache fresh, otherwise a slow test trips the staleness halt and + // the snapshot comes back empty. + if refresh := coordValue.FieldByName("lastValidatorsRefreshAt"); refresh.IsValid() { + *(*time.Time)(unsafe.Pointer(refresh.UnsafeAddr())) = time.Now() + } } func makeActiveValidator(addr string) *types.UniversalValidator { @@ -394,54 +412,69 @@ func TestValidateParticipants(t *testing.T) { t.Run("SIGN: threshold subset is valid", func(t *testing.T) { // 3 of 4 eligible satisfies threshold(4)=3 - assert.NoError(t, sm.validateParticipants([]string{"v1", "v2", "v3"}, signEvent)) + assert.NoError(t, sm.validateParticipants(context.Background(), []string{"v1", "v2", "v3"}, signEvent)) }) t.Run("SIGN: all eligible is also valid (threshold is a minimum)", func(t *testing.T) { - assert.NoError(t, sm.validateParticipants([]string{"v1", "v2", "v3", "v4"}, signEvent)) + assert.NoError(t, sm.validateParticipants(context.Background(), []string{"v1", "v2", "v3", "v4"}, signEvent)) }) t.Run("SIGN: below threshold is rejected", func(t *testing.T) { // 2 < threshold(4)=3 - err := sm.validateParticipants([]string{"v1", "v2"}, signEvent) + err := sm.validateParticipants(context.Background(), []string{"v1", "v2"}, signEvent) require.Error(t, err) assert.Contains(t, err.Error(), "threshold") }) t.Run("SIGN: non-eligible participant is rejected", func(t *testing.T) { - err := sm.validateParticipants([]string{"v1", "v2", "unknown"}, signEvent) + err := sm.validateParticipants(context.Background(), []string{"v1", "v2", "unknown"}, signEvent) require.Error(t, err) assert.Contains(t, err.Error(), "not eligible") }) - // --- SIGN_FUND_MIGRATE: same threshold rules as SIGN_OUTBOUND --- + // --- SIGN_FUND_MIGRATE: rules come from the old key, not the current set --- - fmEvent := &store.Event{EventID: "fm-1", Type: store.EventTypeSignFundMigrate} + // The old key's shareholders are v1..v4, matching the current set here, so + // the threshold is the same 3 as for SIGN_OUTBOUND above. The two diverge + // once the sets differ, covered in fund_migrate_e2e_test.go. + setCoordinatorPushCore(coord, &mockPushCore{ + keysByID: map[string]*utsstypes.TssKey{ + "old-key": {KeyId: "old-key", Participants: []string{"v1", "v2", "v3", "v4"}}, + }, + }) + fmEvent := fundMigrateStoreEvent(t, "old-key") t.Run("SIGN_FUND_MIGRATE: threshold subset is valid", func(t *testing.T) { - assert.NoError(t, sm.validateParticipants([]string{"v1", "v2", "v3"}, fmEvent)) + assert.NoError(t, sm.validateParticipants(context.Background(), []string{"v1", "v2", "v3"}, fmEvent)) }) t.Run("SIGN_FUND_MIGRATE: below threshold is rejected", func(t *testing.T) { - err := sm.validateParticipants([]string{"v1", "v2"}, fmEvent) + err := sm.validateParticipants(context.Background(), []string{"v1", "v2"}, fmEvent) require.Error(t, err) assert.Contains(t, err.Error(), "threshold") }) + t.Run("SIGN_FUND_MIGRATE: event without an old key id is rejected", func(t *testing.T) { + bare := &store.Event{EventID: "fm-bare", Type: store.EventTypeSignFundMigrate} + err := sm.validateParticipants(context.Background(), []string{"v1", "v2", "v3"}, bare) + require.Error(t, err) + assert.Contains(t, err.Error(), "resolve fund migration signers") + }) + // --- KEYGEN: exact-match rules (all eligible must participate) --- t.Run("KEYGEN: all eligible is valid", func(t *testing.T) { - assert.NoError(t, sm.validateParticipants([]string{"v1", "v2", "v3", "v4"}, keygenEvent)) + assert.NoError(t, sm.validateParticipants(context.Background(), []string{"v1", "v2", "v3", "v4"}, keygenEvent)) }) t.Run("KEYGEN: missing participant is rejected", func(t *testing.T) { - err := sm.validateParticipants([]string{"v1", "v2", "v3"}, keygenEvent) // v4 missing + err := sm.validateParticipants(context.Background(), []string{"v1", "v2", "v3"}, keygenEvent) // v4 missing require.Error(t, err) assert.Contains(t, err.Error(), "does not match eligible count") }) t.Run("KEYGEN: non-eligible participant is rejected", func(t *testing.T) { - err := sm.validateParticipants([]string{"v1", "v2", "v3", "v4", "unknown"}, keygenEvent) + err := sm.validateParticipants(context.Background(), []string{"v1", "v2", "v3", "v4", "unknown"}, keygenEvent) require.Error(t, err) assert.Contains(t, err.Error(), "not eligible") }) @@ -466,18 +499,17 @@ func TestSessionManager_Integration(t *testing.T) { Type: "setup", EventID: event.EventID, Participants: []string{"validator1", "validator2", "validator3"}, - Payload: []byte("invalid setup data"), // Will fail when creating session + Payload: []byte("invalid setup data"), // rejected before a session is created } - // This will fail at session creation or GetLatestBlockNum, but validation should pass + // Rejected at the setup-binding check (the payload is not a decodable DKLS + // setup), or earlier at GetLatestBlockNum. Either way validation must not + // let an unbound payload reach session creation. err := sm.HandleIncomingMessage(ctx, "peer1", &msg) - // We expect an error because we can't create a real DKLS session with invalid data - // or because GetLatestBlockNum fails assert.Error(t, err) - // Error should be about session creation, DKLS library, or no endpoints assert.True(t, - containsAny(err.Error(), []string{"failed to create session", "DKLS", "dkls", "session", "no endpoints"}), - "error should be about session creation or endpoints, got: %s", err.Error()) + containsAny(err.Error(), []string{"failed to create session", "DKLS", "dkls", "session", "setup message", "no endpoints"}), + "error should be about setup binding, session creation or endpoints, got: %s", err.Error()) } func TestVerifySigningRequest_OutboundDisabled(t *testing.T) { @@ -622,6 +654,40 @@ func TestVerifyFundMigrationSigningRequest_Validation(t *testing.T) { assert.Contains(t, err.Error(), "failed to parse fund migration event data") }) + // Rejected before any chain call: nothing deterministic to sign. + t.Run("event without a pinned transfer amount is rejected", func(t *testing.T) { + const validPubkey = "024e3b81af9c2234cad09d679ce6035ed1392347ce64ce405f5dcd36228a25de6e" + for _, tc := range []struct{ name, amount string }{ + {"missing", ""}, + {"zero", "0"}, + {"negative", "-1"}, + {"not a number", "abc"}, + } { + t.Run(tc.name, func(t *testing.T) { + eventDataBytes, err := json.Marshal(utsstypes.FundMigrationInitiatedEventData{ + OldTssPubkey: validPubkey, + CurrentTssPubkey: validPubkey, + Chain: "eip155:1", + GasPrice: "20000000000", + GasLimit: 21000, + L1GasFee: "0", + TransferAmount: tc.amount, + }) + require.NoError(t, err) + event := &store.Event{ + EventID: "fm-no-amount-" + tc.name, + Type: store.EventTypeSignFundMigrate, + EventData: eventDataBytes, + } + err = sm.verifyFundMigrationSigningRequest(ctx, event, &common.UnsignedSigningReq{ + SigningHash: []byte{0x01}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "no usable transfer amount") + }) + } + }) + t.Run("invalid old TSS pubkey is rejected", func(t *testing.T) { migrationData := utsstypes.FundMigrationInitiatedEventData{ OldTssPubkey: "not-a-valid-pubkey", @@ -675,6 +741,7 @@ func TestVerifyFundMigrationSigningRequest_Validation(t *testing.T) { GasPrice: "1000000000", GasLimit: 21100, L1GasFee: "150", + TransferAmount: "500000000000000000", } eventDataBytes, _ := json.Marshal(migrationData) event := &store.Event{ @@ -686,7 +753,7 @@ func TestVerifyFundMigrationSigningRequest_Validation(t *testing.T) { req := &common.UnsignedSigningReq{SigningHash: []byte{0x01, 0x02}} err := sm.verifyFundMigrationSigningRequest(ctx, event, req) assert.NoError(t, err) - assert.Nil(t, req.TSSFundMigrationAmount, "amount stays nil when chain/builder is skipped") + }) } @@ -892,10 +959,9 @@ func TestSendACK(t *testing.T) { ) signed := &coordinator.SignedDataPayload{ - Signature: bytes.Repeat([]byte{0xaa}, 64), - SigningHash: bytes.Repeat([]byte{0xbb}, 32), - Nonce: 42, - TSSFundMigrationAmount: big.NewInt(123_456), + Signature: bytes.Repeat([]byte{0xaa}, 64), + SigningHash: bytes.Repeat([]byte{0xbb}, 32), + Nonce: 42, } require.NoError(t, sm.sendACK(context.Background(), "coord-peer", "evt-signed", signed)) @@ -907,8 +973,6 @@ func TestSendACK(t *testing.T) { assert.Equal(t, signed.Signature, msg.SignedData.Signature) assert.Equal(t, signed.SigningHash, msg.SignedData.SigningHash) assert.Equal(t, signed.Nonce, msg.SignedData.Nonce) - require.NotNil(t, msg.SignedData.TSSFundMigrationAmount) - assert.Equal(t, 0, signed.TSSFundMigrationAmount.Cmp(msg.SignedData.TSSFundMigrationAmount)) }) } @@ -1125,7 +1189,9 @@ func TestHandleSigningComplete(t *testing.T) { assert.False(t, hasAmount, "tss_fund_migration_amount is omitted for outbound events") }) - t.Run("fund migration signing complete persists tss_fund_migration_amount", func(t *testing.T) { + // The amount is not carried through signing data any more: broadcast reads it + // from the migration event, so nothing raceable rides with the signature. + t.Run("fund migration signing complete does not carry the amount", func(t *testing.T) { event := store.Event{ EventID: "fm-complete-1", BlockHeight: 250, @@ -1136,9 +1202,8 @@ func TestHandleSigningComplete(t *testing.T) { require.NoError(t, testDB.Create(&event).Error) req := &common.UnsignedSigningReq{ - SigningHash: []byte{0xca, 0xfe}, - Nonce: 3, - TSSFundMigrationAmount: new(big.Int).SetUint64(123456789), + SigningHash: []byte{0xca, 0xfe}, + Nonce: 3, } err := sm.handleSigningComplete(context.Background(), "fm-complete-1", event.EventData, []byte{0xbe, 0xef}, req) require.NoError(t, err) @@ -1147,17 +1212,12 @@ func TestHandleSigningComplete(t *testing.T) { require.NoError(t, testDB.Where("event_id = ?", "fm-complete-1").First(&updated).Error) assert.Equal(t, store.StatusSigned, updated.Status) - // Decode the field into *big.Int directly — unmarshalling into map[string]any - // would coerce the JSON number into float64 and lose precision for wei values. - var decoded struct { - SigningData struct { - TSSFundMigrationAmount *big.Int `json:"tss_fund_migration_amount"` - } `json:"signing_data"` - } - require.NoError(t, json.Unmarshal(updated.EventData, &decoded)) - require.NotNil(t, decoded.SigningData.TSSFundMigrationAmount, - "tss_fund_migration_amount must survive the sign→broadcast handoff so broadcast reproduces the signed tx") - assert.Equal(t, "123456789", decoded.SigningData.TSSFundMigrationAmount.String()) + var rawData map[string]any + require.NoError(t, json.Unmarshal(updated.EventData, &rawData)) + signingData, ok := rawData["signing_data"].(map[string]any) + require.True(t, ok) + _, hasAmount := signingData["tss_fund_migration_amount"] + assert.False(t, hasAmount, "the amount is read from the event at broadcast, not carried here") }) } @@ -1343,3 +1403,322 @@ func TestExtractSignedDataFromEvent_CorruptDataIsObservable(t *testing.T) { assert.Equal(t, uint64(42), signed.Nonce) }) } + +// A coordinator-assigned nonce must sit within [finalized, ceilingBase+maxNonceGap]. +// Below is already committed; far above never mines and freezes the outbound +// with its PRC20 already burned at the gateway. +func TestCheckNonceInRange(t *testing.T) { + const finalized = uint64(100) + + t.Run("equal to finalized is accepted", func(t *testing.T) { + require.NoError(t, checkNonceInRange(finalized, finalized, finalized, "eip155:1")) + }) + + t.Run("within the cap above pending is accepted", func(t *testing.T) { + require.NoError(t, checkNonceInRange(finalized+coordinator.PerChainCap, finalized, finalized, "eip155:1")) + }) + + t.Run("exactly at the gap limit is accepted", func(t *testing.T) { + require.NoError(t, checkNonceInRange(finalized+maxNonceGap, finalized, finalized, "eip155:1")) + }) + + // While a chain is congested, BROADCASTED events free the in-flight cap but + // still hold mempool nonces, so pending runs far ahead of finalized. Honest + // coordinators assign from pending and must not be rejected. + t.Run("congestion: nonce far above finalized but near pending is accepted", func(t *testing.T) { + pending := finalized + 500 + require.NoError(t, checkNonceInRange(pending+coordinator.PerChainCap, finalized, pending, "eip155:1")) + }) + + t.Run("below finalized is rejected", func(t *testing.T) { + err := checkNonceInRange(finalized-1, finalized, finalized, "eip155:1") + require.Error(t, err) + assert.Contains(t, err.Error(), "already used on chain") + }) + + t.Run("one past the gap limit is rejected", func(t *testing.T) { + err := checkNonceInRange(finalized+maxNonceGap+1, finalized, finalized, "eip155:1") + require.Error(t, err) + assert.Contains(t, err.Error(), "would never mine") + }) + + t.Run("far-future gap nonce is rejected even when congested", func(t *testing.T) { + pending := finalized + 500 + err := checkNonceInRange(finalized+(1<<32), finalized, pending, "eip155:1") + require.Error(t, err) + assert.Contains(t, err.Error(), "would never mine") + }) + + // A stale pending lookup must never widen the window below finalized. + t.Run("ceiling base below finalized falls back to finalized", func(t *testing.T) { + err := checkNonceInRange(finalized+maxNonceGap+1, finalized, finalized-50, "eip155:1") + require.Error(t, err) + assert.Contains(t, err.Error(), "would never mine") + }) + + // SVM reports 0 from GetNextNonce and signs nonce 0; it must not be rejected. + t.Run("zero nonce on a nonce-less chain is accepted", func(t *testing.T) { + require.NoError(t, checkNonceInRange(0, 0, 0, "solana:devnet")) + }) +} + +// nonceBuilder is a partial TxBuilder: only GetNextNonce is implemented, so any +// other call panics loudly rather than silently returning a zero value. +type nonceBuilder struct { + common.TxBuilder + finalized, pending uint64 + finalizedErr, pendingErr error +} + +func (b *nonceBuilder) GetNextNonce(_ context.Context, _ string, useFinalized bool) (uint64, error) { + if useFinalized { + return b.finalized, b.finalizedErr + } + return b.pending, b.pendingErr +} + +func TestNonceBounds(t *testing.T) { + ctx := context.Background() + + t.Run("pending above finalized becomes the ceiling base", func(t *testing.T) { + fin, base, err := nonceBounds(ctx, &nonceBuilder{finalized: 100, pending: 140}, "0xtss") + require.NoError(t, err) + assert.Equal(t, uint64(100), fin) + assert.Equal(t, uint64(140), base) + }) + + // Falling back to finalized is stricter, never wrong. + t.Run("pending lookup failure falls back to finalized", func(t *testing.T) { + fin, base, err := nonceBounds(ctx, &nonceBuilder{ + finalized: 100, + pendingErr: errors.New("rpc down"), + }, "0xtss") + require.NoError(t, err) + assert.Equal(t, uint64(100), fin) + assert.Equal(t, uint64(100), base) + }) + + // A stale pending read must never lower the ceiling below finalized. + t.Run("pending below finalized falls back to finalized", func(t *testing.T) { + fin, base, err := nonceBounds(ctx, &nonceBuilder{finalized: 100, pending: 60}, "0xtss") + require.NoError(t, err) + assert.Equal(t, uint64(100), fin) + assert.Equal(t, uint64(100), base) + }) + + // Callers skip the nonce check entirely when finalized is unavailable. + t.Run("finalized lookup failure errors", func(t *testing.T) { + _, _, err := nonceBounds(ctx, &nonceBuilder{finalizedErr: errors.New("rpc down")}, "0xtss") + require.Error(t, err) + }) +} + +// A follower verifies UnsignedSigningReq.SigningHash, but DKLS signs the hash +// embedded in Message.Payload, and the two arrive unbound. Without this check a +// coordinator can present a legitimate hash for verification and embed an +// attacker-chosen one in the setup, harvesting honest shares over it. +func TestSetupBindsHash(t *testing.T) { + participantIDs := []byte("party1\x00party2") + keyID := make([]byte, 32) + + legitHash := make([]byte, 32) + copy(legitHash, "legitimate-outbound-hash-32bytes") + attackerHash := make([]byte, 32) + copy(attackerHash, "attacker-chosen-vault-call-digest") + + legitSetup, err := session.DklsSignSetupMsgNew(keyID, nil, legitHash, participantIDs) + require.NoError(t, err) + attackerSetup, err := session.DklsSignSetupMsgNew(keyID, nil, attackerHash, participantIDs) + require.NoError(t, err) + + t.Run("accepts setup that signs the verified hash", func(t *testing.T) { + require.NoError(t, setupBindsHash(legitSetup, legitHash)) + }) + + // The reported attack. + t.Run("rejects setup embedding a different hash", func(t *testing.T) { + err := setupBindsHash(attackerSetup, legitHash) + require.Error(t, err) + assert.Contains(t, err.Error(), "setup message signs hash") + }) + + t.Run("rejects undecodable setup", func(t *testing.T) { + require.Error(t, setupBindsHash([]byte("not-a-dkls-setup"), legitHash)) + require.Error(t, setupBindsHash(nil, legitHash)) + }) + + t.Run("rejects missing verified hash", func(t *testing.T) { + err := setupBindsHash(legitSetup, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "no verified signing hash") + }) +} + +// Keygen, keyrefresh and quorumchange have the same split as the sign path: we +// validate msg.Participants, but the session runs on the list embedded in +// Payload. The threshold cannot be bound this way, see verifySetupBindsParticipants. +func TestSetupBindsParticipants(t *testing.T) { + validated := []string{"validator1", "validator2", "validator3"} + encode := func(ids []string) []byte { + return []byte(strings.Join(ids, "\x00")) + } + + legitSetup, err := session.DklsKeygenSetupMsgNew(2, nil, encode(validated)) + require.NoError(t, err) + + t.Run("accepts setup with the validated participants", func(t *testing.T) { + require.NoError(t, setupBindsParticipants(legitSetup, validated)) + }) + + t.Run("rejects setup with a substituted participant", func(t *testing.T) { + swapped, err := session.DklsKeygenSetupMsgNew(2, nil, + encode([]string{"validator1", "validator2", "attacker"})) + require.NoError(t, err) + err = setupBindsParticipants(swapped, validated) + require.Error(t, err) + assert.Contains(t, err.Error(), "do not match validated participants") + }) + + t.Run("rejects setup with a dropped participant", func(t *testing.T) { + fewer, err := session.DklsKeygenSetupMsgNew(2, nil, + encode([]string{"validator1", "validator2"})) + require.NoError(t, err) + require.Error(t, setupBindsParticipants(fewer, validated)) + }) + + // Index order is part of the protocol, so a reorder is as consequential as + // a substitution. + t.Run("rejects reordered participants", func(t *testing.T) { + reordered, err := session.DklsKeygenSetupMsgNew(2, nil, + encode([]string{"validator3", "validator2", "validator1"})) + require.NoError(t, err) + require.Error(t, setupBindsParticipants(reordered, validated)) + }) + + t.Run("rejects undecodable setup and missing validated list", func(t *testing.T) { + require.Error(t, setupBindsParticipants([]byte("not-a-setup"), validated)) + require.Error(t, setupBindsParticipants(nil, validated)) + require.Error(t, setupBindsParticipants(legitSetup, nil)) + }) +} + +// The regression the finding asks for: a Payload whose embedded hash differs +// from the verified SigningHash must refuse session creation and produce no +// shares. Driven through handleSetupMessage so it covers the wiring, not just +// the comparison helper. +func TestHandleSetupMessage_RejectsPayloadHashMismatch(t *testing.T) { + _, coord, evtStore, keyshareMgr, _, testDB := setupTestSessionManager(t) + ctx := context.Background() + + // Sign-eligible validators are the ACTIVE ones in the fixture. + participants := []string{"validator1", "validator2"} + participantIDs := []byte(strings.Join(participants, "\x00")) + keyID := make([]byte, 32) + + legitHash := make([]byte, 32) + copy(legitHash, "legitimate-outbound-hash-32bytes") + attackerHash := make([]byte, 32) + copy(attackerHash, "attacker-chosen-vault-call-digest") + + newRecordingSM := func() (*SessionManager, *int) { + sends := 0 + sm := NewSessionManager( + evtStore, coord, keyshareMgr, nil, nil, + func(context.Context, string, []byte) error { sends++; return nil }, + "validator1", 3*time.Minute, 30*time.Second, 60, zerolog.Nop(), nil, + ) + return sm, &sends + } + + newEvent := func(t *testing.T, id string) { + t.Helper() + require.NoError(t, testDB.Create(&store.Event{ + EventID: id, BlockHeight: 100, + Type: store.EventTypeSignOutbound, + Status: store.StatusConfirmed, + EventData: []byte(`{"destination_chain":"eip155:11155111"}`), + }).Error) + } + + t.Run("substituted payload hash is refused, no session, no shares", func(t *testing.T) { + newEvent(t, "sign-mismatch") + sm, sends := newRecordingSM() + + // Coordinator shows the legitimate hash but ships a setup over its own. + attackerSetup, err := session.DklsSignSetupMsgNew(keyID, nil, attackerHash, participantIDs) + require.NoError(t, err) + + err = sm.HandleIncomingMessage(ctx, "peer1", &coordinator.Message{ + Type: coordinator.MessageTypeSetup, + EventID: "sign-mismatch", + Participants: participants, + Payload: attackerSetup, + UnsignedSigningReq: &common.UnsignedSigningReq{SigningHash: legitHash, Nonce: 1}, + }) + + require.Error(t, err) + assert.Contains(t, err.Error(), "setup message signs hash") + + sm.mu.RLock() + sessionCount := len(sm.sessions) + sm.mu.RUnlock() + assert.Zero(t, sessionCount, "no session may be created for a mismatched setup") + assert.Zero(t, *sends, "no ACK or share may be emitted for a mismatched setup") + }) + + // Positive control: with the same wiring, a setup over the verified hash must + // get past the binding check, so the rejection above is the binding and not + // some earlier validation failing. + t.Run("matching payload hash passes the binding check", func(t *testing.T) { + newEvent(t, "sign-match") + sm, _ := newRecordingSM() + + legitSetup, err := session.DklsSignSetupMsgNew(keyID, nil, legitHash, participantIDs) + require.NoError(t, err) + + err = sm.HandleIncomingMessage(ctx, "peer1", &coordinator.Message{ + Type: coordinator.MessageTypeSetup, + EventID: "sign-match", + Participants: participants, + Payload: legitSetup, + UnsignedSigningReq: &common.UnsignedSigningReq{SigningHash: legitHash, Nonce: 1}, + }) + + if err != nil { + assert.NotContains(t, err.Error(), "setup message signs hash", + "matching setup must not be rejected by the hash binding") + assert.NotContains(t, err.Error(), "do not match validated participants", + "matching setup must not be rejected by the participant binding") + } + }) +} + +// The session constructors accept a threshold and ignore it, so the setup blob's +// embedded threshold is what the protocol runs with. A coordinator embedding a +// lower one would elicit help producing a weaker key than the participants +// agreed to, which is worse than a bad signature since it persists. +func TestSetupBindsThreshold(t *testing.T) { + validated := []string{"validator1", "validator2", "validator3"} + expected := coordinator.CalculateThreshold(len(validated)) + encode := func(ids []string) []byte { return []byte(strings.Join(ids, "\x00")) } + + t.Run("accepts the expected threshold", func(t *testing.T) { + setup, err := session.DklsKeygenSetupMsgNew(expected, nil, encode(validated)) + require.NoError(t, err) + require.NoError(t, setupBindsThreshold(setup, validated)) + }) + + t.Run("rejects a downgraded threshold", func(t *testing.T) { + require.Greater(t, expected, 1, "fixture must allow a strictly lower threshold") + downgraded, err := session.DklsKeygenSetupMsgNew(expected-1, nil, encode(validated)) + require.NoError(t, err) + err = setupBindsThreshold(downgraded, validated) + require.Error(t, err) + assert.Contains(t, err.Error(), "does not match expected") + }) + + t.Run("rejects undecodable setup", func(t *testing.T) { + require.Error(t, setupBindsThreshold([]byte("not-a-setup"), validated)) + require.Error(t, setupBindsThreshold(nil, validated)) + }) +} diff --git a/universalClient/tss/tss.go b/universalClient/tss/tss.go index 83e3398fa..ecd287262 100644 --- a/universalClient/tss/tss.go +++ b/universalClient/tss/tss.go @@ -105,6 +105,7 @@ type Node struct { txBroadcaster *txbroadcaster.Broadcaster txResolver *txresolver.Resolver expirySweeper *expirysweeper.Sweeper + keyshareSweeper *keyshare.Sweeper // Network configuration (used during Start) networkCfg libp2pnet.Config @@ -238,20 +239,12 @@ func NewNode(ctx context.Context, cfg Config) (*Node, error) { registeredPeers: make(map[string]bool), } - getTSSAddress := func(ctx context.Context) (string, error) { - if node.coordinator == nil { - return "", fmt.Errorf("coordinator not initialized") - } - return node.coordinator.GetTSSAddress(ctx) - } - node.txResolver = txresolver.NewResolver(txresolver.Config{ EventStore: evtStore, Chains: cfg.Chains, PushSigner: cfg.PushSigner, CheckInterval: sessionExpiryCheckInterval, Logger: logger, - GetTSSAddress: getTSSAddress, }) node.txBroadcaster = txbroadcaster.NewBroadcaster(txbroadcaster.Config{ @@ -259,7 +252,6 @@ func NewNode(ctx context.Context, cfg Config) (*Node, error) { Chains: cfg.Chains, CheckInterval: sessionExpiryCheckInterval, Logger: logger, - GetTSSAddress: getTSSAddress, }) node.expirySweeper = expirysweeper.NewSweeper(expirysweeper.Config{ @@ -269,6 +261,12 @@ func NewNode(ctx context.Context, cfg Config) (*Node, error) { Logger: logger, }) + node.keyshareSweeper = keyshare.NewSweeper(keyshare.Config{ + Keyshares: mgr, + PushCore: cfg.PushCore, + Logger: logger, + }) + return node, nil } @@ -285,34 +283,9 @@ func (n *Node) Start(ctx context.Context) error { n.logger.Debug().Msg("starting TSS node") - // Start libp2p network - net, err := libp2pnet.New(ctx, n.networkCfg, n.logger) - if err != nil { - return fmt.Errorf("failed to start libp2p network: %w", err) - } - n.network = net - - // Register global message handler - if err := net.RegisterHandler(n.onReceive); err != nil { - net.Close() - return fmt.Errorf("failed to register message handler: %w", err) - } - - // Recover IN_PROGRESS events on startup. Two-pass: - // 1. Rows whose event_data already carries signing_data → SIGNED - // (signature was persisted but status got clobbered by a race). - // 2. Remaining IN_PROGRESS → CONFIRMED (genuine mid-session crashes). - signedRecovered, confirmedReset, err := n.eventStore.RecoverInProgressEvents() - if err != nil { - n.logger.Warn().Err(err).Msg("failed to recover IN_PROGRESS events, continuing anyway") - } else if signedRecovered > 0 || confirmedReset > 0 { - n.logger.Info(). - Int64("signed_recovered", signedRecovered). - Int64("confirmed_reset", confirmedReset). - Msg("recovered IN_PROGRESS events on node startup") - } - - // Create coordinator with send function using node's Send method + // Create coordinator with send function using node's Send method. + // Created before the network so the connection gater and message handler + // never observe a nil coordinator or session manager. if n.coordinator == nil { coord := coordinator.NewCoordinator( n.eventStore, @@ -351,6 +324,36 @@ func (n *Node) Start(ctx context.Context) error { n.sessionManager = sessionMgr } + // Only Universal Validators may connect and open TSS streams + n.networkCfg.Authorizer = n.coordinator.IsKnownPeer + + // Start libp2p network + net, err := libp2pnet.New(ctx, n.networkCfg, n.logger) + if err != nil { + return fmt.Errorf("failed to start libp2p network: %w", err) + } + n.network = net + + // Register global message handler + if err := net.RegisterHandler(n.onReceive); err != nil { + net.Close() + return fmt.Errorf("failed to register message handler: %w", err) + } + + // Recover IN_PROGRESS events on startup. Two-pass: + // 1. Rows whose event_data already carries signing_data → SIGNED + // (signature was persisted but status got clobbered by a race). + // 2. Remaining IN_PROGRESS → CONFIRMED (genuine mid-session crashes). + signedRecovered, confirmedReset, err := n.eventStore.RecoverInProgressEvents() + if err != nil { + n.logger.Warn().Err(err).Msg("failed to recover IN_PROGRESS events, continuing anyway") + } else if signedRecovered > 0 || confirmedReset > 0 { + n.logger.Info(). + Int64("signed_recovered", signedRecovered). + Int64("confirmed_reset", confirmedReset). + Msg("recovered IN_PROGRESS events on node startup") + } + // Start coordinator n.coordinator.Start(ctx) @@ -366,6 +369,9 @@ func (n *Node) Start(ctx context.Context) error { // Start expiry sweeper (CONFIRMED past expiry → REVERTED) n.expirySweeper.Start(ctx) + // Start keyshare GC (delete shares superseded by quorum change / key refresh) + n.keyshareSweeper.Start(ctx) + n.logger.Info(). Str("peer_id", net.ID()). Strs("addrs", net.ListenAddrs()). diff --git a/universalClient/tss/txbroadcaster/broadcaster.go b/universalClient/tss/txbroadcaster/broadcaster.go index bc993fae3..772734698 100644 --- a/universalClient/tss/txbroadcaster/broadcaster.go +++ b/universalClient/tss/txbroadcaster/broadcaster.go @@ -18,7 +18,6 @@ type Config struct { Chains *externalchains.Chains CheckInterval time.Duration Logger zerolog.Logger - GetTSSAddress func(ctx context.Context) (string, error) } type Broadcaster struct { @@ -26,7 +25,6 @@ type Broadcaster struct { chains *externalchains.Chains checkInterval time.Duration logger zerolog.Logger - getTSSAddress func(ctx context.Context) (string, error) } func NewBroadcaster(cfg Config) *Broadcaster { @@ -39,7 +37,6 @@ func NewBroadcaster(cfg Config) *Broadcaster { chains: cfg.Chains, checkInterval: interval, logger: cfg.Logger.With().Str("component", "txbroadcaster").Logger(), - getTSSAddress: cfg.GetTSSAddress, } } diff --git a/universalClient/tss/txbroadcaster/broadcaster_test.go b/universalClient/tss/txbroadcaster/broadcaster_test.go index dae85fb3e..028448ba1 100644 --- a/universalClient/tss/txbroadcaster/broadcaster_test.go +++ b/universalClient/tss/txbroadcaster/broadcaster_test.go @@ -11,6 +11,7 @@ import ( "time" "unsafe" + "github.com/ethereum/go-ethereum/crypto" "github.com/rs/zerolog" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -25,6 +26,7 @@ import ( "github.com/pushchain/push-chain-node/universalClient/externalchains" "github.com/pushchain/push-chain-node/universalClient/externalchains/common" "github.com/pushchain/push-chain-node/universalClient/store" + "github.com/pushchain/push-chain-node/universalClient/tss/coordinator" "github.com/pushchain/push-chain-node/universalClient/tss/eventstore" "github.com/pushchain/push-chain-node/universalClient/tss/txflow" ) @@ -120,10 +122,27 @@ func newTestChains(t *testing.T, chainID string, vmType uregistrytypes.VmType, c return c } +// testBroadcastSigningKeyHex signs the outbound fixtures. The broadcaster derives +// the nonce domain from the signature, so it has to be a real one. +const testBroadcastSigningKeyHex = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + +// testBroadcastSigner is the address recovered from those fixtures, i.e. the +// nonce domain the broadcaster must query. +var testBroadcastSigner = func() string { + key, _ := crypto.HexToECDSA(testBroadcastSigningKeyHex) + addr, _ := coordinator.DeriveEVMAddressFromPubkey(hex.EncodeToString(crypto.CompressPubkey(&key.PublicKey))) + return addr +}() + func makeSignedOutboundData(t *testing.T, destChain string, nonce uint64) []byte { t.Helper() - sig := hex.EncodeToString(make([]byte, 64)) - hash := hex.EncodeToString(make([]byte, 32)) + key, err := crypto.HexToECDSA(testBroadcastSigningKeyHex) + require.NoError(t, err) + hashBytes := crypto.Keccak256([]byte("test outbound signing hash")) + sigBytes, err := crypto.Sign(hashBytes, key) + require.NoError(t, err) + sig := hex.EncodeToString(sigBytes) + hash := hex.EncodeToString(hashBytes) data := txflow.SignedOutboundData{ OutboundCreatedEvent: uexecutortypes.OutboundCreatedEvent{ TxID: "tx-123", @@ -200,17 +219,53 @@ func getEvent(t *testing.T, db *gorm.DB, eventID string) store.Event { return ev } -func newBroadcaster(evtStore *eventstore.Store, ch *externalchains.Chains, tssAddr string) *Broadcaster { - getTSSAddr := func(ctx context.Context) (string, error) { return tssAddr, nil } +func newBroadcaster(evtStore *eventstore.Store, ch *externalchains.Chains) *Broadcaster { return NewBroadcaster(Config{ EventStore: evtStore, Chains: ch, CheckInterval: 0, // uses default, doesn't matter for direct calls Logger: zerolog.Nop(), - GetTSSAddress: getTSSAddr, }) } +// The rotation case, mirroring the resolver. The broadcaster must query the +// nonce of the key that signed, not whichever key is current: after a rotation +// they are separate EOAs, and reading the successor's sequence would report a +// still-free nonce as consumed. +func TestEVM_BroadcastError_AfterRotation_ChecksSigningKeyNonce(t *testing.T) { + evtStore, db := setupTestDB(t) + builder := &mockTxBuilder{} + client := &mockChainClient{builder: builder} + ch := newTestChains(t, "eip155:1", uregistrytypes.VmType_EVM, client) + + rotatedKey, err := crypto.HexToECDSA("8a1f9a8f9c8b7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e") + require.NoError(t, err) + rotatedSigner, err := coordinator.DeriveEVMAddressFromPubkey( + hex.EncodeToString(crypto.CompressPubkey(&rotatedKey.PublicKey))) + require.NoError(t, err) + require.NotEqual(t, testBroadcastSigner, rotatedSigner) + + // Signed under the original key at nonce 5. + insertSignedEvent(t, db, "ev-rotated", "eip155:1", 5) + + builder.On("BroadcastOutboundSigningRequest", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return("0xabc", fmt.Errorf("already known")) + builder.On("VerifyBroadcastedTx", mock.Anything, "0xabc"). + Return(false, uint64(0), uint64(0), uint8(0), nil) + // The signing key's nonce 5 is still free, so the tx can still mine. + builder.On("GetNextNonce", mock.Anything, testBroadcastSigner, true).Return(uint64(5), nil) + // The rotated key has moved past it. Reading this domain is the bug. + builder.On("GetNextNonce", mock.Anything, rotatedSigner, true).Return(uint64(42), nil) + + b := newBroadcaster(evtStore, ch) + b.processSigned(context.Background()) + + builder.AssertCalled(t, "GetNextNonce", mock.Anything, testBroadcastSigner, true) + builder.AssertNotCalled(t, "GetNextNonce", mock.Anything, rotatedSigner, true) + require.Equal(t, store.StatusSigned, getEvent(t, db, "ev-rotated").Status, + "signing key nonce still free means retry, not a consumed-nonce transition") +} + func TestEVM_BroadcastError_NonceConsumed_MarksBroadcasted(t *testing.T) { // Broadcast fails with txHash, finalized nonce shows consumed → BROADCASTED. evtStore, db := setupTestDB(t) @@ -225,9 +280,9 @@ func TestEVM_BroadcastError_NonceConsumed_MarksBroadcasted(t *testing.T) { // VerifyBroadcastedTx=not found → fall through to the nonce-consumed check. builder.On("VerifyBroadcastedTx", mock.Anything, "0xabc"). Return(false, uint64(0), uint64(0), uint8(0), nil) - builder.On("GetNextNonce", mock.Anything, "0xTSS", true).Return(uint64(10), nil) + builder.On("GetNextNonce", mock.Anything, testBroadcastSigner, true).Return(uint64(10), nil) - b := newBroadcaster(evtStore, ch, "0xTSS") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -251,7 +306,7 @@ func TestEVM_BroadcastError_TxOnChain_MarksBroadcasted(t *testing.T) { builder.On("VerifyBroadcastedTx", mock.Anything, "0xabc"). Return(true, uint64(100), uint64(3), uint8(1), nil) - b := newBroadcaster(evtStore, ch, "0xTSS") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -272,7 +327,7 @@ func TestEVM_BroadcastSuccess_MarksBroadcasted(t *testing.T) { builder.On("BroadcastOutboundSigningRequest", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return("0xabc123", nil) - b := newBroadcaster(evtStore, ch, "0xTSS") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -294,7 +349,7 @@ func TestEVM_BroadcastAssemblyFails_StaysSigned(t *testing.T) { builder.On("BroadcastOutboundSigningRequest", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return("", fmt.Errorf("connection refused")) - b := newBroadcaster(evtStore, ch, "0xTSS") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -315,44 +370,61 @@ func TestEVM_BroadcastFails_WithTxHash_NonceNotConsumed_StaysSigned(t *testing.T Return("0xabc", fmt.Errorf("gas too low")) builder.On("VerifyBroadcastedTx", mock.Anything, "0xabc"). Return(false, uint64(0), uint64(0), uint8(0), nil) - builder.On("GetNextNonce", mock.Anything, "0xTSS", true).Return(uint64(5), nil) + builder.On("GetNextNonce", mock.Anything, testBroadcastSigner, true).Return(uint64(5), nil) - b := newBroadcaster(evtStore, ch, "0xTSS") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") require.Equal(t, store.StatusSigned, ev.Status) // stays SIGNED } -func TestEVM_GetTSSAddressNil_UsesEmptyAddress(t *testing.T) { - // getTSSAddress is nil → empty string passed to GetNextNonce on broadcast error. +// An unrecoverable signer leaves no nonce domain to query. The broadcaster must +// defer rather than fall back to another address, which previously meant asking +// for the nonce of the empty string. +func TestEVM_SignerUnrecoverable_StaysSigned(t *testing.T) { evtStore, db := setupTestDB(t) builder := &mockTxBuilder{} client := &mockChainClient{builder: builder} ch := newTestChains(t, "eip155:1", uregistrytypes.VmType_EVM, client) - insertSignedEvent(t, db, "ev-1", "eip155:1", 5) + insertSignedEventUnsigned(t, db, "ev-1", "eip155:1", 5) builder.On("BroadcastOutboundSigningRequest", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return("0xabc", fmt.Errorf("already known")) builder.On("VerifyBroadcastedTx", mock.Anything, "0xabc"). Return(false, uint64(0), uint64(0), uint8(0), nil) - // Expect empty address since GetTSSAddress is nil. - builder.On("GetNextNonce", mock.Anything, "", true).Return(uint64(10), nil) - b := NewBroadcaster(Config{ - EventStore: evtStore, - Chains: ch, - Logger: zerolog.Nop(), - GetTSSAddress: nil, // explicitly nil - }) + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) - ev := getEvent(t, db, "ev-1") - require.Equal(t, store.StatusBroadcasted, ev.Status) - builder.AssertCalled(t, "GetNextNonce", mock.Anything, "", true) + require.Equal(t, store.StatusSigned, getEvent(t, db, "ev-1").Status) + builder.AssertNotCalled(t, "GetNextNonce", mock.Anything, mock.Anything, mock.Anything) } +// insertSignedEventUnsigned inserts a SIGNED outbound whose signature cannot be +// recovered, standing in for a legacy or malformed payload. +func insertSignedEventUnsigned(t *testing.T, db *gorm.DB, eventID, destChain string, nonce uint64) { + t.Helper() + data := txflow.SignedOutboundData{ + OutboundCreatedEvent: uexecutortypes.OutboundCreatedEvent{ + TxID: "tx-123", UniversalTxId: "utx-456", DestinationChain: destChain, + Recipient: "0xRecipient", Amount: "1000000", + }, + SigningData: &txflow.SigningData{ + Signature: hex.EncodeToString(make([]byte, 64)), + SigningHash: hex.EncodeToString(make([]byte, 32)), + Nonce: nonce, + }, + } + b, err := json.Marshal(data) + require.NoError(t, err) + require.NoError(t, db.Create(&store.Event{ + EventID: eventID, BlockHeight: 100, ExpiryBlockHeight: 99999, + Type: "SIGN_OUTBOUND", ConfirmationType: "STANDARD", + Status: store.StatusSigned, EventData: b, + }).Error) +} func TestSVM_DeadlineZero_ClusterConfirmsExpiry_MarksBroadcasted(t *testing.T) { // Legacy event without a signing deadline. `now > 0` enters the deadline // branch and any fresh cluster time (>> 0) trips the expiry case → @@ -365,7 +437,7 @@ func TestSVM_DeadlineZero_ClusterConfirmsExpiry_MarksBroadcasted(t *testing.T) { insertSignedEvent(t, db, "ev-1", "solana:mainnet", 0) builder.On("IsAlreadyExecuted", mock.Anything, "tx-123").Return(false, time.Now().Unix(), nil) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -388,7 +460,7 @@ func TestSVM_BroadcastSuccess_MarksBroadcasted(t *testing.T) { builder.On("BroadcastOutboundSigningRequest", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return("solTxSig123", nil) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -410,7 +482,7 @@ func TestSVM_BroadcastFails_PDAExists_MarksBroadcasted(t *testing.T) { Return("", fmt.Errorf("tx simulation failed: account already exists")) builder.On("IsAlreadyExecuted", mock.Anything, "tx-123").Return(true, int64(0), nil) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -432,7 +504,7 @@ func TestSVM_BroadcastFails_BeforeDeadline_StaysSigned(t *testing.T) { Return("", fmt.Errorf("simulation failed: invalid instruction")) builder.On("IsAlreadyExecuted", mock.Anything, "tx-123").Return(false, int64(0), nil) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -451,7 +523,7 @@ func TestSVM_BroadcastFails_PastDeadline_MarksBroadcastedForRevert(t *testing.T) // PDA absent, cluster time = now (fresh) and well past deadline → cluster-confirmed expiry. builder.On("IsAlreadyExecuted", mock.Anything, "tx-123").Return(false, time.Now().Unix(), nil) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -471,7 +543,7 @@ func TestSVM_PastLocalDeadline_ExecutedByPeer_MarksBroadcasted(t *testing.T) { insertSignedSVMEventWithDeadline(t, db, "ev-1", "solana:mainnet", 0, time.Now().Unix()-3600) builder.On("IsAlreadyExecuted", mock.Anything, "tx-123").Return(true, time.Now().Unix(), nil) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -497,7 +569,7 @@ func TestSVM_PastLocalDeadline_ClusterSaysStillInWindow_FallsThroughToBroadcast( builder.On("BroadcastOutboundSigningRequest", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return("tx-hash-ok", nil) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -516,7 +588,7 @@ func TestSVM_PastLocalDeadline_RPCError_StaysSigned(t *testing.T) { insertSignedSVMEventWithDeadline(t, db, "ev-1", "solana:mainnet", 0, time.Now().Unix()-3600) builder.On("IsAlreadyExecuted", mock.Anything, "tx-123").Return(false, int64(0), fmt.Errorf("RPC down")) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -538,7 +610,7 @@ func TestSVM_BroadcastFails_PDACheckFails_StaysSigned(t *testing.T) { Return("", fmt.Errorf("RPC timeout")) builder.On("IsAlreadyExecuted", mock.Anything, "tx-123").Return(false, int64(0), fmt.Errorf("RPC down")) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "ev-1") @@ -551,7 +623,7 @@ func TestProcessSigned_NoEvents_DoesNothing(t *testing.T) { client := &mockChainClient{builder: builder} ch := newTestChains(t, "eip155:1", uregistrytypes.VmType_EVM, client) - b := newBroadcaster(evtStore, ch, "0xTSS") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) // no panic, no calls builder.AssertNotCalled(t, "GetNextNonce", mock.Anything, mock.Anything, mock.Anything) @@ -559,7 +631,7 @@ func TestProcessSigned_NoEvents_DoesNothing(t *testing.T) { func TestProcessSigned_NilChains_DoesNothing(t *testing.T) { evtStore, _ := setupTestDB(t) - b := newBroadcaster(evtStore, nil, "") + b := newBroadcaster(evtStore, nil) b.processSigned(context.Background()) // should not panic } @@ -576,7 +648,7 @@ func TestProcessSigned_MultipleEvents(t *testing.T) { builder.On("BroadcastOutboundSigningRequest", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return("0xabc", nil) - b := newBroadcaster(evtStore, ch, "0xTSS") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev1 := getEvent(t, db, "ev-1") @@ -589,7 +661,7 @@ func TestMarkBroadcasted_FormatsCAIPTxHash(t *testing.T) { evtStore, db := setupTestDB(t) insertSignedEvent(t, db, "ev-1", "eip155:1", 5) - b := newBroadcaster(evtStore, nil, "") + b := newBroadcaster(evtStore, nil) ev := getEvent(t, db, "ev-1") b.markBroadcasted(&ev, "eip155:1", "0xdeadbeef") @@ -602,7 +674,7 @@ func TestMarkBroadcasted_EmptyTxHash(t *testing.T) { evtStore, db := setupTestDB(t) insertSignedEvent(t, db, "ev-1", "solana:mainnet", 3) - b := newBroadcaster(evtStore, nil, "") + b := newBroadcaster(evtStore, nil) ev := getEvent(t, db, "ev-1") b.markBroadcasted(&ev, "solana:mainnet", "") @@ -617,7 +689,7 @@ const testNewTSSPubkey = "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac func makeSignedFundMigrationData(t *testing.T, chainID string, nonce uint64) []byte { t.Helper() - return makeSignedFundMigrationDataWithTransfer(t, chainID, nonce, nil) + return makeSignedFundMigrationDataWithTransfer(t, chainID, nonce, big.NewInt(500_000_000_000_000_000)) } func makeSignedFundMigrationDataWithTransfer(t *testing.T, chainID string, nonce uint64, transferAmount *big.Int) []byte { @@ -635,12 +707,12 @@ func makeSignedFundMigrationDataWithTransfer(t *testing.T, chainID string, nonce GasPrice: "1000000000", GasLimit: 21100, L1GasFee: "150", + TransferAmount: transferAmountString(transferAmount), }, SigningData: &txflow.SigningData{ - Signature: sig, - SigningHash: hash, - Nonce: nonce, - TSSFundMigrationAmount: transferAmount, + Signature: sig, + SigningHash: hash, + Nonce: nonce, }, } b, err := json.Marshal(data) @@ -648,6 +720,13 @@ func makeSignedFundMigrationDataWithTransfer(t *testing.T, chainID string, nonce return b } +func transferAmountString(v *big.Int) string { + if v == nil { + return "" + } + return v.String() +} + func insertSignedFundMigrationEvent(t *testing.T, db *gorm.DB, eventID, chainID string, nonce uint64) { t.Helper() event := store.Event{ @@ -684,7 +763,7 @@ func TestFundMigrationEVM_BroadcastSuccess(t *testing.T) { mock.Anything). Return("0xmigrate123", nil) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "fm-1") @@ -717,14 +796,14 @@ func TestFundMigrationEVM_TSSFundMigrationAmountThreaded(t *testing.T) { builder.On("BroadcastFundMigrationTx", mock.Anything, - mock.MatchedBy(func(req *common.UnsignedSigningReq) bool { - return req.TSSFundMigrationAmount != nil && req.TSSFundMigrationAmount.String() == "777000000000000000" - }), mock.Anything, + mock.MatchedBy(func(data *common.FundMigrationData) bool { + return data.TransferAmount != nil && data.TransferAmount.String() == "777000000000000000" + }), mock.Anything). Return("0xmigrate777", nil) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "fm-transfer") @@ -746,7 +825,7 @@ func TestFundMigrationEVM_BroadcastFails_NonceConsumed(t *testing.T) { Return(false, uint64(0), uint64(0), uint8(0), nil) builder.On("GetNextNonce", mock.Anything, mock.Anything, true).Return(uint64(10), nil) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "fm-1") @@ -755,7 +834,7 @@ func TestFundMigrationEVM_BroadcastFails_NonceConsumed(t *testing.T) { func TestMarkBroadcasted_NonExistentEvent(t *testing.T) { evtStore, _ := setupTestDB(t) - b := newBroadcaster(evtStore, nil, "") + b := newBroadcaster(evtStore, nil) ev := &store.Event{EventID: "does-not-exist"} b.markBroadcasted(ev, "eip155:1", "0xdeadbeef") @@ -766,7 +845,7 @@ func TestMarkBroadcasted_SetsAllFields(t *testing.T) { evtStore, db := setupTestDB(t) insertSignedEvent(t, db, "ev-fields", "eip155:1", 5) - b := newBroadcaster(evtStore, nil, "") + b := newBroadcaster(evtStore, nil) ev := getEvent(t, db, "ev-fields") b.markBroadcasted(&ev, "eip155:42", "0xcafe") @@ -819,7 +898,7 @@ func TestFundMigrationEVM_BroadcastFails_NonceNotConsumed_StaysSigned(t *testing Return(false, uint64(0), uint64(0), uint8(0), nil) builder.On("GetNextNonce", mock.Anything, mock.Anything, true).Return(uint64(3), nil) - b := newBroadcaster(evtStore, ch, "") + b := newBroadcaster(evtStore, ch) b.processSigned(context.Background()) ev := getEvent(t, db, "fm-1") diff --git a/universalClient/tss/txbroadcaster/evm.go b/universalClient/tss/txbroadcaster/evm.go index 0358e3553..2658ea729 100644 --- a/universalClient/tss/txbroadcaster/evm.go +++ b/universalClient/tss/txbroadcaster/evm.go @@ -68,17 +68,15 @@ func (b *Broadcaster) broadcastOutboundEVM(ctx context.Context, event *store.Eve return } - tssAddress := "" - if b.getTSSAddress != nil { - var addrErr error - tssAddress, addrErr = b.getTSSAddress(ctx) - if addrErr != nil { - log.Warn().Err(addrErr).Msg("failed to get TSS address for nonce check, will retry next tick") - return - } + // Nonce check must use the key that signed this tx, not the live TSS: after a + // rotation they are different EOAs with unrelated nonce sequences. + signer, signedNonce, ok := txflow.RecoverOutboundSigner(event) + if !ok { + log.Warn().Msg("could not recover signing key for nonce check, will retry next tick") + return } - b.checkNonceAndMarkBroadcasted(ctx, event, builder, chainID, txHash, tssAddress, data.SigningData.Nonce, broadcastErr) + b.checkNonceAndMarkBroadcasted(ctx, event, builder, chainID, txHash, signer, signedNonce, broadcastErr) } // broadcastFundMigrationEVM broadcasts a signed EVM fund migration transaction. @@ -120,12 +118,19 @@ func (b *Broadcaster) broadcastFundMigrationEVM(ctx context.Context, event *stor l1GasFee := new(big.Int) l1GasFee.SetString(data.L1GasFee, 10) + transferAmount, ok := new(big.Int).SetString(data.TransferAmount, 10) + if !ok || transferAmount.Sign() <= 0 { + log.Warn().Str("transfer_amount", data.TransferAmount).Msg("event carries no usable transfer amount") + return + } + migrationData := &common.FundMigrationData{ - From: oldTSSAddr, - To: currentTSSAddr, - GasPrice: gasPrice, - GasLimit: data.GasLimit, - L1GasFee: l1GasFee, + From: oldTSSAddr, + To: currentTSSAddr, + GasPrice: gasPrice, + GasLimit: data.GasLimit, + L1GasFee: l1GasFee, + TransferAmount: transferAmount, } txHash, broadcastErr := builder.BroadcastFundMigrationTx(ctx, signingReq, migrationData, signature) diff --git a/universalClient/tss/txflow/parse.go b/universalClient/tss/txflow/parse.go index 673857511..cd48cb088 100644 --- a/universalClient/tss/txflow/parse.go +++ b/universalClient/tss/txflow/parse.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" + "github.com/ethereum/go-ethereum/crypto" + "github.com/pushchain/push-chain-node/universalClient/externalchains/common" "github.com/pushchain/push-chain-node/universalClient/store" "github.com/pushchain/push-chain-node/universalClient/tss/coordinator" @@ -22,9 +24,8 @@ func DecodeSigningData(sd *SigningData) (*common.UnsignedSigningReq, []byte, err return nil, nil, fmt.Errorf("failed to decode signature: %w", err) } return &common.UnsignedSigningReq{ - SigningHash: signingHash, - Nonce: sd.Nonce, - TSSFundMigrationAmount: sd.TSSFundMigrationAmount, + SigningHash: signingHash, + Nonce: sd.Nonce, }, signature, nil } @@ -50,9 +51,41 @@ func ReadSigningDeadline(event *store.Event) int64 { return data.SigningDeadline } +// RecoverOutboundSigner returns the EVM address that actually signed a SIGNED or +// BROADCASTED outbound, recovered from the persisted signature and signing hash. +// +// Nonces are per-EOA, so a nonce check is only meaningful against the key that +// signed. Outbound SigningData carries no key id, and after a TSS rotation the +// current key is a different EOA with an unrelated nonce sequence — comparing a +// K1-signed nonce against K2's would report "consumed" while K1's nonce is still +// free. Recovering from the signature binds the check to the right key without +// persisting anything new, so events signed before this existed are covered too. +// +// Returns ok=false when the signer cannot be established, which callers must +// treat as "defer", never as evidence the transaction did not execute. +func RecoverOutboundSigner(event *store.Event) (signer string, nonce uint64, ok bool) { + var data SignedOutboundData + if err := json.Unmarshal(event.EventData, &data); err != nil || data.SigningData == nil { + return "", 0, false + } + req, signature, err := DecodeSigningData(data.SigningData) + if err != nil || len(signature) != 65 || len(req.SigningHash) != 32 { + return "", 0, false + } + pub, err := crypto.SigToPub(req.SigningHash, signature) + if err != nil || pub == nil { + return "", 0, false + } + addr, err := coordinator.DeriveEVMAddressFromPubkey(hex.EncodeToString(crypto.CompressPubkey(pub))) + if err != nil { + return "", 0, false + } + return addr, data.SigningData.Nonce, true +} + // ReadFundMigrationSigner derives the sender EVM address (old TSS) and reads // the signed nonce from a fund migration event payload. Returns ok=false on -// missing/invalid fields — caller defers in that case. +// missing/invalid fields, and the caller defers in that case. func ReadFundMigrationSigner(event *store.Event) (signer string, nonce uint64, ok bool) { var data SignedFundMigrationData if err := json.Unmarshal(event.EventData, &data); err != nil || data.SigningData == nil || data.OldTssPubkey == "" { diff --git a/universalClient/tss/txflow/parse_test.go b/universalClient/tss/txflow/parse_test.go new file mode 100644 index 000000000..1062b0a3c --- /dev/null +++ b/universalClient/tss/txflow/parse_test.go @@ -0,0 +1,102 @@ +package txflow + +import ( + "encoding/hex" + "encoding/json" + "testing" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/universalClient/store" + "github.com/pushchain/push-chain-node/universalClient/tss/coordinator" +) + +const ( + keyAHex = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + keyBHex = "8a1f9a8f9c8b7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e" +) + +func signerAddr(t *testing.T, keyHex string) string { + t.Helper() + key, err := crypto.HexToECDSA(keyHex) + require.NoError(t, err) + addr, err := coordinator.DeriveEVMAddressFromPubkey(hex.EncodeToString(crypto.CompressPubkey(&key.PublicKey))) + require.NoError(t, err) + return addr +} + +// outboundEvent builds a SIGNED outbound payload signed by keyHex, matching what +// sessionManager persists. +func outboundEvent(t *testing.T, keyHex string, nonce uint64) *store.Event { + t.Helper() + key, err := crypto.HexToECDSA(keyHex) + require.NoError(t, err) + hash := crypto.Keccak256([]byte("signing hash")) + sig, err := crypto.Sign(hash, key) + require.NoError(t, err) + + b, err := json.Marshal(map[string]any{ + "tx_id": "tx-1", "utx_id": "utx-1", "destination_chain": "eip155:1", + "signing_data": map[string]any{ + "nonce": nonce, + "signature": hex.EncodeToString(sig), + "signing_hash": hex.EncodeToString(hash), + }, + }) + require.NoError(t, err) + return &store.Event{EventData: b} +} + +func TestRecoverOutboundSigner(t *testing.T) { + t.Run("recovers the address that signed", func(t *testing.T) { + signer, nonce, ok := RecoverOutboundSigner(outboundEvent(t, keyAHex, 5)) + require.True(t, ok) + assert.Equal(t, signerAddr(t, keyAHex), signer) + assert.Equal(t, uint64(5), nonce) + }) + + // The point of the change: two keys are two EOAs with unrelated nonce + // sequences, so the recovered signer has to follow the key that signed rather + // than whichever key is current. + t.Run("different keys recover to different addresses", func(t *testing.T) { + a, _, okA := RecoverOutboundSigner(outboundEvent(t, keyAHex, 5)) + b, _, okB := RecoverOutboundSigner(outboundEvent(t, keyBHex, 5)) + require.True(t, okA) + require.True(t, okB) + assert.NotEqual(t, a, b) + assert.Equal(t, signerAddr(t, keyBHex), b) + }) + + // Every failure has to report ok=false. A wrong address would be worse than + // no address: it produces a confident answer about the wrong nonce domain. + t.Run("unusable payloads report failure", func(t *testing.T) { + cases := map[string]*store.Event{ + "not json": {EventData: []byte("{")}, + "no signing data": {EventData: []byte(`{"tx_id":"tx-1"}`)}, + "short signature": {EventData: []byte(`{"signing_data":{"nonce":5,"signature":"deadbeef","signing_hash":"` + + hex.EncodeToString(crypto.Keccak256([]byte("h"))) + `"}}`)}, + "bad hex": {EventData: []byte(`{"signing_data":{"nonce":5,"signature":"zz","signing_hash":"zz"}}`)}, + "short hash": {EventData: []byte(`{"signing_data":{"nonce":5,"signature":"` + + hex.EncodeToString(make([]byte, 65)) + `","signing_hash":"00"}}`)}, + "unrecoverable signature": {EventData: []byte(`{"signing_data":{"nonce":5,"signature":"` + + hex.EncodeToString(make([]byte, 65)) + `","signing_hash":"` + + hex.EncodeToString(crypto.Keccak256([]byte("h"))) + `"}}`)}, + } + for name, ev := range cases { + t.Run(name, func(t *testing.T) { + _, _, ok := RecoverOutboundSigner(ev) + assert.False(t, ok) + }) + } + }) + + // Same signature, different persisted nonce: the nonce is read from the + // payload, the domain from the signature. They are independent. + t.Run("nonce comes from the payload", func(t *testing.T) { + _, nonce, ok := RecoverOutboundSigner(outboundEvent(t, keyAHex, 99)) + require.True(t, ok) + assert.Equal(t, uint64(99), nonce) + }) +} diff --git a/universalClient/tss/txflow/types.go b/universalClient/tss/txflow/types.go index c51a31f81..81ab272bc 100644 --- a/universalClient/tss/txflow/types.go +++ b/universalClient/tss/txflow/types.go @@ -8,8 +8,6 @@ package txflow import ( - "math/big" - uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" utsstypes "github.com/pushchain/push-chain-node/x/utss/types" ) @@ -19,10 +17,9 @@ import ( // — broadcaster to assemble + send the tx, resolver to compare the signed // nonce against the chain's finalized nonce. type SigningData struct { - Signature string `json:"signature"` // hex-encoded 64/65 byte signature - SigningHash string `json:"signing_hash"` // hex-encoded signing hash - Nonce uint64 `json:"nonce"` - TSSFundMigrationAmount *big.Int `json:"tss_fund_migration_amount,omitempty"` + Signature string `json:"signature"` // hex-encoded 64/65 byte signature + SigningHash string `json:"signing_hash"` // hex-encoded signing hash + Nonce uint64 `json:"nonce"` } // SignedOutboundData wraps OutboundCreatedEvent with the signing data the diff --git a/universalClient/tss/txresolver/evm.go b/universalClient/tss/txresolver/evm.go index 9167a5162..83773b91c 100644 --- a/universalClient/tss/txresolver/evm.go +++ b/universalClient/tss/txresolver/evm.go @@ -21,9 +21,13 @@ import ( // - Tx not found, nonce check unavailable → stay BROADCASTED (retry) // // The nonce IS the give-up signal; there is no max-retry counter. The two -// flows differ only in (a) which vote function records success/failure and -// (b) where the signer address comes from — current TSS for outbound, OLD TSS -// (derived from the event's old pubkey) for fund migration. +// flows differ only in which vote function records success/failure. +// +// Both check the nonce against the key that actually signed, never the current +// TSS: nonces are per-EOA, so after a rotation the live key is a different EOA +// whose sequence says nothing about an outbound signed under the previous one. +// Outbound recovers that signer from the signature, fund migration derives it +// from the event's old pubkey. // // Shared types (SignedOutboundData / SigningData) and helpers (DecodeSigningData, // ReadSignedNonce, ReadFundMigrationSigner, CheckNonce, NonceVerdict) live in @@ -164,21 +168,12 @@ func (r *Resolver) resolveFundMigrationEVM(ctx context.Context, event *store.Eve func (r *Resolver) outboundSigner(ctx context.Context, event *store.Event) (string, uint64, bool) { log := r.logger.With().Str("event_id", event.EventID).Logger() - signedNonce, ok := txflow.ReadSignedNonce(event) + signer, signedNonce, ok := txflow.RecoverOutboundSigner(event) if !ok { - log.Warn().Msg("EVM tx not found and signed nonce unavailable, staying BROADCASTED") - return "", 0, false - } - if r.getTSSAddress == nil { - log.Warn().Msg("EVM tx not found and no TSS-address resolver configured, staying BROADCASTED") - return "", 0, false - } - addr, err := r.getTSSAddress(ctx) - if err != nil { - log.Debug().Err(err).Msg("could not fetch TSS address, will retry next tick") + log.Warn().Msg("EVM tx not found and signing key unrecoverable, staying BROADCASTED") return "", 0, false } - return addr, signedNonce, true + return signer, signedNonce, true } // rewindToSigned moves a BROADCASTED event back to SIGNED so the broadcaster diff --git a/universalClient/tss/txresolver/resolver.go b/universalClient/tss/txresolver/resolver.go index bbe851c00..1b52906ba 100644 --- a/universalClient/tss/txresolver/resolver.go +++ b/universalClient/tss/txresolver/resolver.go @@ -25,7 +25,6 @@ type Config struct { PushSigner *pushsigner.Signer CheckInterval time.Duration Logger zerolog.Logger - GetTSSAddress func(ctx context.Context) (string, error) } type Resolver struct { @@ -34,7 +33,6 @@ type Resolver struct { pushSigner *pushsigner.Signer checkInterval time.Duration logger zerolog.Logger - getTSSAddress func(ctx context.Context) (string, error) } func NewResolver(cfg Config) *Resolver { @@ -48,7 +46,6 @@ func NewResolver(cfg Config) *Resolver { pushSigner: cfg.PushSigner, checkInterval: interval, logger: cfg.Logger.With().Str("component", "txresolver").Logger(), - getTSSAddress: cfg.GetTSSAddress, } } diff --git a/universalClient/tss/txresolver/resolver_test.go b/universalClient/tss/txresolver/resolver_test.go index b13aec472..73bcd7e56 100644 --- a/universalClient/tss/txresolver/resolver_test.go +++ b/universalClient/tss/txresolver/resolver_test.go @@ -2,12 +2,14 @@ package txresolver import ( "context" + "encoding/hex" "encoding/json" "reflect" "testing" "time" "unsafe" + "github.com/ethereum/go-ethereum/crypto" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -23,6 +25,7 @@ import ( "github.com/pushchain/push-chain-node/universalClient/externalchains" "github.com/pushchain/push-chain-node/universalClient/externalchains/common" "github.com/pushchain/push-chain-node/universalClient/store" + "github.com/pushchain/push-chain-node/universalClient/tss/coordinator" "github.com/pushchain/push-chain-node/universalClient/tss/eventstore" ) @@ -181,19 +184,6 @@ func newResolver(evtStore *eventstore.Store, ch *externalchains.Chains) *Resolve }) } -// newResolverWithTSSAddress builds a Resolver that returns a fixed TSS address -// from GetTSSAddress — needed by tests that exercise the EVM nonce-based -// retry/revert path. -func newResolverWithTSSAddress(evtStore *eventstore.Store, ch *externalchains.Chains, addr string) *Resolver { - return NewResolver(Config{ - EventStore: evtStore, - Chains: ch, - CheckInterval: 0, - Logger: zerolog.Nop(), - GetTSSAddress: func(ctx context.Context) (string, error) { return addr, nil }, - }) -} - func TestParseCAIPTxHash(t *testing.T) { t.Run("valid CAIP tx hash", func(t *testing.T) { chainID, txHash, err := parseCAIPTxHash("eip155:1:0xabc123") @@ -933,14 +923,39 @@ func makeOutboundEventDataWithNonce(txID, utxID, destChain string, nonce uint64) "tx_id": txID, "utx_id": utxID, "destination_chain": destChain, - "signing_data": map[string]any{ - "nonce": nonce, - }, + "signing_data": testOutboundSigningData(testSigningKeyHex, nonce), }) return b } -const testEVMTSSAddr = "0x4D353565442Eb33b66ef88E14336F3F4Bf3a02FB" +// The resolver recovers the nonce domain from the signature, so payloads have to +// carry a real one. Two fixed keys stand in for a TSS key and its rotation +// successor; signing with one and checking the other's nonce is the bug. +const ( + testSigningKeyHex = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + testRotatedSigningKeyHex = "8a1f9a8f9c8b7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e" +) + +func testOutboundSigningData(keyHex string, nonce uint64) map[string]any { + key, _ := crypto.HexToECDSA(keyHex) + hash := crypto.Keccak256([]byte("test outbound signing hash")) + sig, _ := crypto.Sign(hash, key) + return map[string]any{ + "nonce": nonce, + "signature": hex.EncodeToString(sig), + "signing_hash": hex.EncodeToString(hash), + } +} + +func testSignerAddr(keyHex string) string { + key, _ := crypto.HexToECDSA(keyHex) + addr, _ := coordinator.DeriveEVMAddressFromPubkey(hex.EncodeToString(crypto.CompressPubkey(&key.PublicKey))) + return addr +} + +// The address that signed the payloads above, i.e. the nonce domain the resolver +// must query. Not a configured value any more — it comes from the signature. +var testEVMTSSAddr = testSignerAddr(testSigningKeyHex) func TestResolveOutboundEVM_NotFound_NonceConsumed_Reverts(t *testing.T) { // Tx not found AND signed nonce < finalized nonce → another tx consumed @@ -960,7 +975,7 @@ func TestResolveOutboundEVM_NotFound_NonceConsumed_Reverts(t *testing.T) { // Finalized nonce = 7 → our nonce 5 is past finalized → consumed. builder.On("GetNextNonce", mock.Anything, testEVMTSSAddr, true).Return(uint64(7), nil) - resolver := newResolverWithTSSAddress(evtStore, ch, testEVMTSSAddr) + resolver := newResolver(evtStore, ch) resolver.processBroadcasted(context.Background()) ev := getEvent(t, db, "ev-consumed-1") @@ -968,6 +983,39 @@ func TestResolveOutboundEVM_NotFound_NonceConsumed_Reverts(t *testing.T) { builder.AssertCalled(t, "GetNextNonce", mock.Anything, testEVMTSSAddr, true) } +// A receipt RPC failure combined with a consumed nonce is the dangerous +// combination: the nonce alone reads as "another tx took our slot, ours never +// executed", so a failure vote goes out against an outbound the destination has +// already paid. The error must stop the resolver before the nonce is consulted. +func TestResolveOutboundEVM_ReceiptError_NonceConsumed_DoesNotVoteFailure(t *testing.T) { + evtStore, db := setupTestDB(t) + builder := &mockTxBuilder{} + client := &mockChainClient{builder: builder} + ch := newTestChains(t, "eip155:1", uregistrytypes.VmType_EVM, client) + + eventData := makeOutboundEventDataWithNonce("tx-100", "utx-200", "eip155:1", 5) + insertBroadcastedEvent(t, db, "ev-18826", "eip155:1", "eip155:1:0xalreadypaid", eventData) + + builder.On("VerifyBroadcastedTx", mock.Anything, "0xalreadypaid"). + Return(false, uint64(0), uint64(0), uint8(0), assert.AnError).Once() + // Finalized nonce 7 > signed nonce 5, so the nonce check would say "consumed". + builder.On("GetNextNonce", mock.Anything, testEVMTSSAddr, true).Return(uint64(7), nil) + + resolver := newResolver(evtStore, ch) + resolver.processBroadcasted(context.Background()) + + require.Equal(t, store.StatusBroadcasted, getEvent(t, db, "ev-18826").Status) + builder.AssertNotCalled(t, "GetNextNonce", mock.Anything, mock.Anything, mock.Anything) + + // RPC recovers and the receipt shows the destination did execute successfully. + builder.On("VerifyBroadcastedTx", mock.Anything, "0xalreadypaid"). + Return(true, uint64(500), uint64(20), uint8(1), nil).Once() + + resolver.processBroadcasted(context.Background()) + + require.Equal(t, store.StatusCompleted, getEvent(t, db, "ev-18826").Status) +} + func TestResolveOutboundEVM_NotFound_NonceUnconsumed_RewindsToSigned(t *testing.T) { // Tx not found AND signed nonce >= finalized nonce → tx may still land // (or was dropped from mempool). Rewind to SIGNED so the broadcaster @@ -985,7 +1033,7 @@ func TestResolveOutboundEVM_NotFound_NonceUnconsumed_RewindsToSigned(t *testing. // Finalized nonce = 5 → our nonce 5 not yet finalized. builder.On("GetNextNonce", mock.Anything, testEVMTSSAddr, true).Return(uint64(5), nil) - resolver := newResolverWithTSSAddress(evtStore, ch, testEVMTSSAddr) + resolver := newResolver(evtStore, ch) resolver.processBroadcasted(context.Background()) ev := getEvent(t, db, "ev-unconsumed-1") @@ -1006,7 +1054,7 @@ func TestResolveOutboundEVM_NotFound_NonceRPCError_StaysBroadcasted(t *testing.T Return(false, uint64(0), uint64(0), uint8(0), nil) builder.On("GetNextNonce", mock.Anything, testEVMTSSAddr, true).Return(uint64(0), assert.AnError) - resolver := newResolverWithTSSAddress(evtStore, ch, testEVMTSSAddr) + resolver := newResolver(evtStore, ch) resolver.processBroadcasted(context.Background()) ev := getEvent(t, db, "ev-rpc-err-1") @@ -1028,7 +1076,7 @@ func TestResolveOutboundEVM_NotFound_SignedNonceMissing_StaysBroadcasted(t *test builder.On("VerifyBroadcastedTx", mock.Anything, "0xmissing"). Return(false, uint64(0), uint64(0), uint8(0), nil) - resolver := newResolverWithTSSAddress(evtStore, ch, testEVMTSSAddr) + resolver := newResolver(evtStore, ch) resolver.processBroadcasted(context.Background()) ev := getEvent(t, db, "ev-no-nonce") @@ -1036,52 +1084,69 @@ func TestResolveOutboundEVM_NotFound_SignedNonceMissing_StaysBroadcasted(t *test builder.AssertNotCalled(t, "GetNextNonce", mock.Anything, mock.Anything, mock.Anything) } -func TestResolveOutboundEVM_NotFound_TSSAddressFetchError_StaysBroadcasted(t *testing.T) { - // Tx not found and GetTSSAddress callback errors → defer (retry next tick). +// The nonce domain is derived from the signature, so if the signer cannot be +// recovered there is no domain to check. That must defer, never fall through to +// some other key's sequence. +func TestResolveOutboundEVM_NotFound_SignerUnrecoverable_StaysBroadcasted(t *testing.T) { evtStore, db := setupTestDB(t) builder := &mockTxBuilder{} client := &mockChainClient{builder: builder} ch := newTestChains(t, "eip155:1", uregistrytypes.VmType_EVM, client) - eventData := makeOutboundEventDataWithNonce("tx-100", "utx-200", "eip155:1", 5) - insertBroadcastedEvent(t, db, "ev-tss-err", "eip155:1", "eip155:1:0xmissing", eventData) + eventData, _ := json.Marshal(map[string]any{ + "tx_id": "tx-100", "utx_id": "utx-200", "destination_chain": "eip155:1", + "signing_data": map[string]any{ + "nonce": 5, + "signature": "deadbeef", // not 65 bytes + "signing_hash": hex.EncodeToString(crypto.Keccak256([]byte("h"))), + }, + }) + insertBroadcastedEvent(t, db, "ev-nosigner", "eip155:1", "eip155:1:0xmissing", eventData) builder.On("VerifyBroadcastedTx", mock.Anything, "0xmissing"). Return(false, uint64(0), uint64(0), uint8(0), nil) - resolver := NewResolver(Config{ - EventStore: evtStore, - Chains: ch, - CheckInterval: 0, - Logger: zerolog.Nop(), - GetTSSAddress: func(ctx context.Context) (string, error) { return "", assert.AnError }, - }) + resolver := newResolver(evtStore, ch) resolver.processBroadcasted(context.Background()) - ev := getEvent(t, db, "ev-tss-err") - require.Equal(t, store.StatusBroadcasted, ev.Status) + require.Equal(t, store.StatusBroadcasted, getEvent(t, db, "ev-nosigner").Status) builder.AssertNotCalled(t, "GetNextNonce", mock.Anything, mock.Anything, mock.Anything) } -func TestResolveOutboundEVM_NotFound_NoTSSAddressResolver_StaysBroadcasted(t *testing.T) { - // Tx not found and GetTSSAddress is nil → can't run nonce check → defer. +// F-2026-18827. An outbound signed under K1 is still BROADCASTED when the TSS +// rotates to K2. K1 and K2 are separate EOAs with unrelated nonce sequences, so +// checking K2's would report the nonce consumed and fail-vote a transaction K1 +// can still land, while the refund path remints. The check must follow the key +// that signed. +func TestResolveOutboundEVM_NotFound_AfterRotation_ChecksSigningKeyNonce(t *testing.T) { evtStore, db := setupTestDB(t) builder := &mockTxBuilder{} client := &mockChainClient{builder: builder} ch := newTestChains(t, "eip155:1", uregistrytypes.VmType_EVM, client) + k1 := testSignerAddr(testSigningKeyHex) + k2 := testSignerAddr(testRotatedSigningKeyHex) + require.NotEqual(t, k1, k2) + + // Signed under K1 at nonce 5, still unresolved. eventData := makeOutboundEventDataWithNonce("tx-100", "utx-200", "eip155:1", 5) - insertBroadcastedEvent(t, db, "ev-no-tss-1", "eip155:1", "eip155:1:0xmissing", eventData) + insertBroadcastedEvent(t, db, "ev-rotated", "eip155:1", "eip155:1:0xmissing", eventData) builder.On("VerifyBroadcastedTx", mock.Anything, "0xmissing"). Return(false, uint64(0), uint64(0), uint8(0), nil) + // K1 nonce 5 is still free, so this tx can still mine. + builder.On("GetNextNonce", mock.Anything, k1, true).Return(uint64(5), nil) + // K2 has moved well past 5. Reading this domain is the bug. + builder.On("GetNextNonce", mock.Anything, k2, true).Return(uint64(42), nil) - resolver := newResolver(evtStore, ch) // no GetTSSAddress configured + // The live TSS is K2, the rotation successor. + resolver := newResolver(evtStore, ch) resolver.processBroadcasted(context.Background()) - ev := getEvent(t, db, "ev-no-tss-1") - require.Equal(t, store.StatusBroadcasted, ev.Status) - builder.AssertNotCalled(t, "GetNextNonce", mock.Anything, mock.Anything, mock.Anything) + builder.AssertCalled(t, "GetNextNonce", mock.Anything, k1, true) + builder.AssertNotCalled(t, "GetNextNonce", mock.Anything, k2, true) + require.Equal(t, store.StatusSigned, getEvent(t, db, "ev-rotated").Status, + "K1 nonce still free means rebroadcast, not a failure vote") } func TestResolveOutboundEVM_VerifyError_StaysBroadcasted(t *testing.T) { diff --git a/utils/address.go b/utils/address.go index 0a9ea1f64..cf52c362f 100644 --- a/utils/address.go +++ b/utils/address.go @@ -58,22 +58,41 @@ func ConvertAnyAddressesToBytes[T ByteType](addr ...string) ([]T, error) { return res, nil } -// get address pair returns both the cosmos and the 0x addresses, or an error +// GetAddressPair returns both the cosmos and the 0x addresses, or an error. +// +// The address MUST decode to exactly 20 bytes. The Cosmos SDK accepts bech32 +// account addresses of up to 255 bytes, while common.BytesToAddress silently +// keeps only the RIGHTMOST 20 bytes. A longer address would therefore collapse +// onto an unrelated EVM address - e.g. 0x01 || +// truncates to the uexecutor module itself - so reject it instead of +// truncating. func GetAddressPair(addr string) (sdk.AccAddress, common.Address, error) { bz, err := ConvertAnyAddressToBytes(addr) if err != nil { return nil, common.Address{}, err } + if len(bz) != common.AddressLength { + return nil, common.Address{}, fmt.Errorf( + "invalid address length for %q: got %d bytes, want %d", addr, len(bz), common.AddressLength) + } + return sdk.AccAddress(bz), common.BytesToAddress(bz), nil } +// MustConvertCosmosToHex returns the 0x form of addr, or an empty string when +// addr cannot be represented as a 20-byte EVM address. +// +// It never panics and never truncates: the previous common.Address(bz) +// conversion panicked for inputs shorter than 20 bytes and silently kept the +// LEFTMOST 20 bytes for longer ones - the opposite end from +// common.BytesToAddress used elsewhere in this file. func MustConvertCosmosToHex(addr string) string { bz, err := ConvertAnyAddressToBytes(addr) - if err != nil { + if err != nil || len(bz) != common.AddressLength { return "" } - return common.Address(bz).Hex() + return common.BytesToAddress(bz).Hex() } // create an enum for COSMOS, 0x, or EITHER diff --git a/utils/address_test.go b/utils/address_test.go new file mode 100644 index 000000000..7695564cf --- /dev/null +++ b/utils/address_test.go @@ -0,0 +1,92 @@ +package utils_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/utils" +) + +// uexecutorModuleEVMAddr is sha256("uexecutor")[:20] rendered as an EVM address. +const uexecutorModuleEVMAddr = "0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7" + +// bech32OfLength returns a bech32 account address that decodes to exactly n bytes. +func bech32OfLength(n int) string { + bz := make([]byte, n) + for i := range bz { + bz[i] = byte(i + 1) + } + return sdk.AccAddress(bz).String() +} + +// TestGetAddressPair_RejectsNon20ByteAddresses is the regression test for +// F-2026-18200 remediation 2: anything that does not decode to exactly 20 bytes +// must be rejected rather than silently truncated. +func TestGetAddressPair_RejectsNon20ByteAddresses(t *testing.T) { + for _, length := range []int{19, 21, 22, 32} { + addr := bech32OfLength(length) + _, _, err := utils.GetAddressPair(addr) + require.Error(t, err, "%d-byte address must be rejected", length) + require.Contains(t, err.Error(), "invalid address length") + } +} + +func TestGetAddressPair_Accepts20ByteAddresses(t *testing.T) { + bz := make([]byte, common.AddressLength) + for i := range bz { + bz[i] = byte(i + 1) + } + + cosmosAddr, evmAddr, err := utils.GetAddressPair(sdk.AccAddress(bz).String()) + require.NoError(t, err) + require.Equal(t, sdk.AccAddress(bz), cosmosAddr) + require.Equal(t, common.BytesToAddress(bz), evmAddr) + + // The 0x form must round-trip as well. + cosmosAddr, evmAddr, err = utils.GetAddressPair(common.BytesToAddress(bz).Hex()) + require.NoError(t, err) + require.Equal(t, sdk.AccAddress(bz), cosmosAddr) + require.Equal(t, common.BytesToAddress(bz), evmAddr) +} + +// TestGetAddressPair_ModuleAliasRejected documents the exact attack: an over-long +// address whose rightmost 20 bytes are the uexecutor module account truncates +// onto the module's EVM address, which the UEA trusts unconditionally. +func TestGetAddressPair_ModuleAliasRejected(t *testing.T) { + moduleAddr := authtypes.NewModuleAddress("uexecutor") + require.Len(t, moduleAddr, common.AddressLength) + require.Equal(t, uexecutorModuleEVMAddr, common.BytesToAddress(moduleAddr).Hex()) + + for _, prefixLen := range []int{1, 2, 12} { + aliased := sdk.AccAddress(append(make([]byte, prefixLen), moduleAddr...)) + // Without the length check this collapses onto the module address. + require.Equal(t, uexecutorModuleEVMAddr, common.BytesToAddress(aliased).Hex()) + + _, evmAddr, err := utils.GetAddressPair(aliased.String()) + require.Error(t, err, "aliased %d-byte address must be rejected", len(aliased)) + require.Equal(t, common.Address{}, evmAddr) + } +} + +// TestMustConvertCosmosToHex checks the second truncation site: it must neither +// panic on short input nor keep the leftmost 20 bytes of a long one. +func TestMustConvertCosmosToHex(t *testing.T) { + bz := make([]byte, common.AddressLength) + for i := range bz { + bz[i] = byte(i + 1) + } + require.Equal(t, common.BytesToAddress(bz).Hex(), utils.MustConvertCosmosToHex(sdk.AccAddress(bz).String())) + + for _, length := range []int{19, 21, 22, 32} { + require.NotPanics(t, func() { + require.Empty(t, utils.MustConvertCosmosToHex(bech32OfLength(length)), + "%d-byte address must not be converted", length) + }) + } + + require.Empty(t, utils.MustConvertCosmosToHex("not-a-bech32-address")) +} diff --git a/utils/canonical.go b/utils/canonical.go index ef15471e0..c74e74075 100644 --- a/utils/canonical.go +++ b/utils/canonical.go @@ -20,6 +20,18 @@ const ( const base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +// A base58-encoded 64-byte Solana signature is always 64..88 characters: 88 is +// ceil(512 / log2(58)) for a full-range value, and 64 is the all-zero case +// (each leading zero byte encodes as one '1'). Outside that band the decode can +// never produce 64 bytes, so its result would be discarded — see +// canonicalizeSolanaTxHash. mr-tron/base58's decoder is quadratic (for each of +// n characters it walks ceil(n/4) limbs), so decoding attacker-supplied strings +// only to throw the result away is an unmetered CPU sink on public query paths. +const ( + solanaSigBase58MinLen = 64 + solanaSigBase58MaxLen = 88 +) + // CAIP2Namespace returns the namespace component of a CAIP-2 chain id // ("eip155:1" → "eip155"). Returns "" when the id has no namespace. func CAIP2Namespace(chain string) string { @@ -119,8 +131,13 @@ func canonicalizeSolanaTxHash(s string) (string, error) { if strings.HasPrefix(canon, "0x") { return canon, nil } - if raw, decErr := base58.Decode(canon); decErr == nil && len(raw) == 64 { - return "0x" + hex.EncodeToString(raw), nil + // Only attempt the decode for lengths that can actually yield 64 bytes. + // This is output-equivalent for every possible input: a string outside the + // band already falls through to `return canon` below, decode or not. + if n := len(canon); n >= solanaSigBase58MinLen && n <= solanaSigBase58MaxLen { + if raw, decErr := base58.Decode(canon); decErr == nil && len(raw) == 64 { + return "0x" + hex.EncodeToString(raw), nil + } } return canon, nil } diff --git a/utils/canonical_test.go b/utils/canonical_test.go index f11c211e1..018a497a8 100644 --- a/utils/canonical_test.go +++ b/utils/canonical_test.go @@ -2,7 +2,10 @@ package utils_test import ( "encoding/hex" + "math/rand" + "strings" "testing" + "time" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/mr-tron/base58" @@ -138,6 +141,75 @@ func TestCAIP2Namespace(t *testing.T) { require.Equal(t, "", utils.CAIP2Namespace("no-colon")) } +// referenceSolanaTxHash reproduces the pre-fix behaviour for pure-base58 input: +// decode unconditionally, convert only on an exact 64-byte result, otherwise +// return the input untouched. The length band added in canonicalizeSolanaTxHash +// must not change the result for any input. +func referenceSolanaTxHash(s string) string { + if raw, err := base58.Decode(s); err == nil && len(raw) == 64 { + return "0x" + hex.EncodeToString(raw) + } + return s +} + +func TestCanonicalizeTxHashByNamespace_Solana_LengthBandIsOutputEquivalent(t *testing.T) { + // Only 64..88 base58 chars can decode to exactly 64 bytes, so the band gate + // is a pure performance change. Sweep across it — 63/64/88/89 are the edges. + rng := rand.New(rand.NewSource(1)) + alphabet := []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") + + lengths := []int{1, 2, 31, 32, 43, 44, 63, 64, 65, 87, 88, 89, 90, 128, 200, 300} + for n := 3; n < 63; n += 7 { + lengths = append(lengths, n) + } + + for _, n := range lengths { + for variant := 0; variant < 4; variant++ { + b := make([]byte, n) + for i := range b { + switch variant { + case 0: + b[i] = '1' // all-zero decode: the short edge of the band + case 1: + b[i] = 'z' // largest digit: the long edge + default: + b[i] = alphabet[rng.Intn(len(alphabet))] + } + } + in := string(b) + require.Equal(t, referenceSolanaTxHash(in), + utils.LenientCanonicalizeTxHash("solana:devnet", in), + "length band changed the result for a %d-char input %q", n, in) + } + } +} + +func TestCanonicalizeTxHashByNamespace_Solana_RealSignatureStillConverges(t *testing.T) { + // The band must not break the case it exists to serve: an 88-char base58 + // signature still folds to 0x-hex. + got, err := utils.CanonicalizeTxHashByNamespace("solana:devnet", solSig) + require.NoError(t, err) + require.Equal(t, "0x", got[:2]) + require.Len(t, got, 2+128) +} + +func TestCanonicalizeTxHashByNamespace_Solana_OversizedInputDoesNotDecode(t *testing.T) { + // F-2026-18821: mr-tron/base58 decoding is quadratic, and the result for an + // out-of-band length is discarded. Before the fix a single 1e5-char decode + // measured 4.5-29s (and InboundKeys does three of them); after, no decode + // runs at all. The bound is loose enough not to flake on a busy CI box while + // still failing hard on any return to O(n^2). + huge := strings.Repeat("z", 100_000) + + start := time.Now() + got := utils.LenientCanonicalizeTxHash("solana:devnet", huge) + elapsed := time.Since(start) + + require.Equal(t, huge, got, "out-of-band input must pass through unchanged") + require.Less(t, elapsed, time.Second, + "oversized base58 tx_hash must not be decoded (took %s)", elapsed) +} + // AddressToBytes32: an EVM address goes into the low 20 bytes (bytes32(uint160(addr))). func TestAddressToBytes32_EVM_LowAligned(t *testing.T) { addr := "0x000000000000000000000000000000000000dEaD" diff --git a/x/uexecutor/README.md b/x/uexecutor/README.md index c1d5a96a8..c94a2beba 100755 --- a/x/uexecutor/README.md +++ b/x/uexecutor/README.md @@ -222,7 +222,7 @@ Vote messages check `IsBondedUniversalValidator` and `IsTombstonedUniversalValid The cryptographic binding is enforced inside the UEA contract's `executeUniversalTx` (see [`UEA_EVM.sol`](https://github.com/pushchain/push-chain-core-contracts/blob/86e20e2d26819e7cc885549f08c66895221dfab0/src/uea/UEA_EVM.sol#L145) and [`UEA_SVM.sol`](https://github.com/pushchain/push-chain-core-contracts/blob/86e20e2d26819e7cc885549f08c66895221dfab0/src/uea/UEA_SVM.sol)): 1. The contract holds the owner's public key as **immutable bytes** set at UEA deployment via `initialize(_id, _factory)`. There is no code path that mutates this after init. -2. `executeUniversalTx(payload, signature)` verifies the `signature` (passed in as `MsgExecutePayload.VerificationData`) against this stored owner — ECDSA recovery for EVM-origin owners, the Ed25519 precompile (`0x00…00ca`) for SVM-origin owners. +2. `executeUniversalTx(payload, signature)` verifies the `signature` (passed in as `MsgExecutePayload.VerificationData`) against this stored owner — ECDSA recovery for EVM-origin owners, the Ed25519 precompile (`0xEC…01`) for SVM-origin owners. 3. The signed payload hash includes a contract-tracked `nonce` (monotonic per UEA) and optional `deadline`, providing replay and freshness protection. 4. If signature verification fails, the contract reverts. The revert propagates as `execErr` from `CallUEAExecutePayload`; the keeper returns the error from `ExecutePayload`; the entire Cosmos transaction (including any partial gas-fee deduction) rolls back atomically. **No state changes survive a failed signature check.** diff --git a/x/uexecutor/keeper/admin_execute.go b/x/uexecutor/keeper/admin_execute.go new file mode 100644 index 000000000..a341f9e0b --- /dev/null +++ b/x/uexecutor/keeper/admin_execute.go @@ -0,0 +1,103 @@ +package keeper + +import ( + "context" + "fmt" + + "cosmossdk.io/errors" + sdkErrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/pushchain/push-chain-node/x/uexecutor/types" + uvalidatortypes "github.com/pushchain/push-chain-node/x/uvalidator/types" +) + +// ExecuteStuckInbound finalizes a stuck inbound ballot as PASSED and runs the +// pipeline a finalizing vote would have, so the user receives the funds. +// +// Sibling of RevertStuckInbound, which is the wrong resolution when a ballot's +// preserved YES votes already clear the recomputed threshold — RecomputeBallotQuorum +// leaves those PENDING forever, and a refund was the only hatch (F-2026-18147). +// +// Requires PENDING-unreachable with YES >= VotingThreshold. EXPIRED belongs to +// RevertStuckInbound: no quorum ever formed, so there is nothing to act on. +// +// The ballot key is derived from the supplied inbound, so the admin cannot +// execute anything other than the payload the validators voted on. +func (k Keeper) ExecuteStuckInbound(ctx context.Context, inbound types.Inbound) (utxId string, err error) { + // Same canonical form as the vote path, so the admin-supplied payload + // derives the same ballot key / UTX key the votes did. + inbound.Canonicalize() + + if vErr := inbound.ValidateBasic(); vErr != nil { + return "", errors.Wrap(sdkErrors.ErrInvalidRequest, vErr.Error()) + } + + ballotKey, err := types.GetInboundBallotKey(inbound) + if err != nil { + return "", errors.Wrap(sdkErrors.ErrInvalidRequest, fmt.Sprintf("failed to derive ballot key: %s", err)) + } + + ballot, err := k.uvalidatorKeeper.GetBallot(ctx, ballotKey) + if err != nil { + return "", errors.Wrap(sdkErrors.ErrNotFound, fmt.Sprintf("ballot for inbound not found (key=%s): %s", ballotKey, err)) + } + + if gErr := requireCarriedUnreachablePending(ballotKey, ballot); gErr != nil { + return "", errors.Wrap(sdkErrors.ErrInvalidRequest, + fmt.Sprintf("%s. An EXPIRED ballot never reached quorum at all - use MsgRevertStuckInbound to refund this inbound on the source chain instead", + gErr)) + } + + universalTxKey := types.GetInboundUniversalTxKey(inbound) + if has, hErr := k.HasUniversalTx(ctx, universalTxKey); hErr != nil { + return "", fmt.Errorf("failed to check utx existence: %w", hErr) + } else if has { + return "", errors.Wrap(sdkErrors.ErrInvalidRequest, + fmt.Sprintf("universal tx %s already exists for this inbound", universalTxKey)) + } + + // Finalize before executing, mirroring the vote path's ordering: uvalidator + // marks the ballot PASSED (firing the PendingInbounds bookkeeping hook) and + // only then does the post-finalization pipeline run. The hook may clear the + // pending entry the pipeline would otherwise clear; that removal is a no-op + // on an absent key. + if fErr := k.uvalidatorKeeper.MarkBallotFinalized(ctx, ballotKey, uvalidatortypes.BallotStatus_BALLOT_STATUS_PASSED); fErr != nil { + return "", fmt.Errorf("failed to finalize ballot %s: %w", ballotKey, fErr) + } + + yes, _ := ballot.CountVotes() + k.Logger().Info("admin execute: stuck inbound ballot finalized", + "utx_id", universalTxKey, + "ballot_id", ballotKey, + "yes_votes", yes, + "voting_threshold", ballot.VotingThreshold, + "eligible_voters", len(ballot.EligibleVoters), + "source_chain", inbound.SourceChain, + "amount", inbound.Amount, + ) + + if execErr := k.finalizeInboundAndExecute(ctx, inbound, universalTxKey); execErr != nil { + return "", execErr + } + + return universalTxKey, nil +} + +// requireCarriedUnreachablePending accepts only the shape an admin hatch may +// finalize: PENDING, no vote left to cast, YES already at the threshold. +// Threshold is checked explicitly, not inferred — both vote sites hardcode +// SUCCESS today, but this must not depend on that. +func requireCarriedUnreachablePending(ballotKey string, ballot uvalidatortypes.Ballot) error { + if !ballot.IsUnreachablePending() { + return fmt.Errorf("ballot %s status is %s; admin execute requires PENDING with every eligible voter already voted (no further vote can be cast). "+ + "A pending ballot that still has an unvoted eligible voter has to be finalized by that voter through the normal vote flow", + ballotKey, ballot.Status.String()) + } + + if yes, _ := ballot.CountVotes(); int64(yes) < ballot.VotingThreshold { + return fmt.Errorf("ballot %s has %d YES vote(s) against a voting threshold of %d; admin execute may only finalize a ballot the validators actually carried", + ballotKey, yes, ballot.VotingThreshold) + } + + return nil +} diff --git a/x/uexecutor/keeper/admin_execute_outbound.go b/x/uexecutor/keeper/admin_execute_outbound.go new file mode 100644 index 000000000..a1edaf8cd --- /dev/null +++ b/x/uexecutor/keeper/admin_execute_outbound.go @@ -0,0 +1,119 @@ +package keeper + +import ( + "context" + "fmt" + "strings" + + "cosmossdk.io/errors" + sdkErrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/pushchain/push-chain-node/utils" + "github.com/pushchain/push-chain-node/x/uexecutor/types" + uvalidatortypes "github.com/pushchain/push-chain-node/x/uvalidator/types" +) + +// ExecuteStuckOutbound settles an outbound whose ballot can no longer finalize, +// running the same pipeline a finalizing vote would have. Accepts EXPIRED, and +// PENDING-unreachable with the threshold met (F-2026-18147). +// +// The outcome follows observed_tx.success — success settles, failure mints the +// tokens back and refunds gas — so one message covers both. The ballot key is +// derived from the supplied observation, so the admin can only settle against +// something validators actually voted on. +func (k Keeper) ExecuteStuckOutbound( + ctx context.Context, + utxId string, + outboundId string, + observedTx types.OutboundObservation, +) (string, error) { + // Located first because canonicalizing the tx hash needs DestinationChain. + utx, found, err := k.GetUniversalTx(ctx, utxId) + if err != nil { + return "", err + } + if !found { + return "", errors.Wrap(sdkErrors.ErrNotFound, fmt.Sprintf("UniversalTx not found: %s", utxId)) + } + if utx.OutboundTx == nil { + return "", errors.Wrap(sdkErrors.ErrNotFound, fmt.Sprintf("no outbound tx found in UniversalTx %s", utxId)) + } + + var outbound types.OutboundTx + found = false + for _, ob := range utx.OutboundTx { + if ob.Id == outboundId { + outbound = *ob + found = true + break + } + } + if !found { + return "", errors.Wrap(sdkErrors.ErrNotFound, fmt.Sprintf("outbound %s not found in UniversalTx %s", outboundId, utxId)) + } + + // Canonicalize before deriving the key — it is a digest over these fields. + observedTx.TxHash = utils.LenientCanonicalizeTxHash(outbound.DestinationChain, observedTx.TxHash) + observedTx.GasFeeUsed = strings.TrimSpace(observedTx.GasFeeUsed) + observedTx.ErrorMsg = strings.TrimSpace(observedTx.ErrorMsg) + + if vErr := observedTx.ValidateBasic(); vErr != nil { + return "", errors.Wrap(sdkErrors.ErrInvalidRequest, vErr.Error()) + } + + ballotKey, err := types.GetOutboundBallotKey(utxId, outboundId, observedTx) + if err != nil { + return "", errors.Wrap(sdkErrors.ErrInvalidRequest, fmt.Sprintf("failed to derive ballot key: %s", err)) + } + + ballot, err := k.uvalidatorKeeper.GetBallot(ctx, ballotKey) + if err != nil { + return "", errors.Wrap(sdkErrors.ErrNotFound, fmt.Sprintf("ballot for outbound not found (key=%s): %s", ballotKey, err)) + } + + // EXPIRED is already terminal; PENDING-unreachable is terminal in fact but + // not in the record, so only that one needs the ballot driven to PASSED. + finalizeBallot := false + if ballot.Status != uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED { + if gErr := requireCarriedUnreachablePending(ballotKey, ballot); gErr != nil { + return "", errors.Wrap(sdkErrors.ErrInvalidRequest, + fmt.Sprintf("%s. Admin execute of an outbound requires an EXPIRED ballot, or a PENDING one every eligible voter has already voted on whose YES votes meet the threshold", + gErr)) + } + finalizeBallot = true + } + + // Idempotency barrier. An EXPIRED ballot is deliberately left untouched + // below, so the outbound's own status is the only record of settlement. + if outbound.OutboundStatus != types.Status_PENDING { + return "", errors.Wrap(sdkErrors.ErrInvalidRequest, + fmt.Sprintf("outbound with key %s is already finalized (status %s)", outboundId, outbound.OutboundStatus.String())) + } + + // EXPIRED is left alone: MarkBallotFinalized takes only PASSED/REJECTED. + if finalizeBallot { + if fErr := k.uvalidatorKeeper.MarkBallotFinalized(ctx, ballotKey, uvalidatortypes.BallotStatus_BALLOT_STATUS_PASSED); fErr != nil { + return "", fmt.Errorf("failed to finalize ballot %s: %w", ballotKey, fErr) + } + } + + yes, _ := ballot.CountVotes() + k.Logger().Info("admin execute: settling stuck outbound", + "utx_id", utxId, + "outbound_id", outboundId, + "ballot_id", ballotKey, + "ballot_status", ballot.Status.String(), + "ballot_finalized", finalizeBallot, + "yes_votes", yes, + "voting_threshold", ballot.VotingThreshold, + "eligible_voters", len(ballot.EligibleVoters), + "dest_chain", outbound.DestinationChain, + "success", observedTx.Success, + ) + + if settleErr := k.finalizeOutboundAndSettle(ctx, utxId, outboundId, outbound, observedTx); settleErr != nil { + return "", settleErr + } + + return outboundId, nil +} diff --git a/x/uexecutor/keeper/admin_revert.go b/x/uexecutor/keeper/admin_revert.go index d7a606941..e68df558f 100644 --- a/x/uexecutor/keeper/admin_revert.go +++ b/x/uexecutor/keeper/admin_revert.go @@ -13,14 +13,82 @@ import ( ) // RevertStuckInbound creates an INBOUND_REVERT outbound for an inbound whose -// ballot has expired without finalizing. The revert outbound enters the normal +// ballot can no longer finalize. The revert outbound enters the normal // PendingOutbounds flow; UVs sign it via TSS and broadcast it to the source // chain, refunding the user. // -// Strict precondition: the ballot for the supplied inbound must be in EXPIRED -// state. Admin must run MsgRecomputeBallotQuorum first to drive a stuck ballot -// to EXPIRED if it isn't already (recompute auto-expires when no eligible -// voters remain). +// Precondition: the ballot for the supplied inbound must be either +// +// - EXPIRED, or +// - PENDING but provably unreachable - every eligible voter has already voted +// (Ballot.IsUnreachablePending). +// +// The second case exists because RecomputeBallotQuorum can leave a ballot +// permanently stuck (F-2026-18147). It preserves the votes of still-eligible +// voters, lowers the threshold, and returns PENDING without ever calling +// CheckIfFinalizingVote. If the preserved votes already fill every slot there is +// no vote left to cast - Ballot.AddVote rejects repeat votes - so nothing can +// move the ballot off PENDING and the deposit sits in the source gateway +// forever. Such a ballot is terminal in fact whatever its stored status says, so +// the hatch treats it as terminal too. This holds for both stuck shapes: YES +// already at or above the recomputed threshold (should have passed, never will) +// and YES below it (can never reach it). +// +// The deliberate limits of that widening: +// +// - A PENDING ballot with an unvoted eligible voter is still refused. It can +// finalize normally, and reverting would race a legitimate vote. +// - A PENDING ballot with no eligible voters at all is refused too. Recompute +// rebuilds the voter list from the live UV set, so it either gains real +// voters or auto-expires; a shipped path already resolves it. +// - Fixing this inside RecomputeBallotQuorum by calling CheckIfFinalizingVote +// was rejected. That marks the ballot PASSED without running VoteInbound's +// post-finalization pipeline, so no UniversalTx is ever built +// (msg_vote_inbound.go builds one only when that specific vote finalizes) +// and BallotHooks returns early on PASSED without minting or executing. The +// funds would stay stuck AND the ballot would no longer be PENDING, so +// recompute could not be retried - strictly worse than leaving it alone. +// +// This route reverts rather than executes: the user is refunded on the source +// chain instead of receiving bridged funds on Push. For a ballot whose YES votes +// met the threshold that is the less generous of the two resolutions, and it is +// the deliberate trade for a change that stays inside the module that owns +// inbound execution. MsgExecuteStuckInbound is the sibling hatch for that case +// and executes instead; which of the two to use stays the admin's call, so this +// one deliberately keeps accepting PENDING-unreachable. +// +// The ballot record itself is left untouched. The HasUniversalTx guard below is +// the idempotency barrier, and mutating ballot status from x/uexecutor would +// fire the uvalidator terminal hook and re-enter inbound routing for an inbound +// this call is already resolving. +// +// REJECTED stays refused, deliberately and not by omission (F-2026-18801): a +// supermajority affirmatively voted that the observation is invalid, so a revert +// outbound would pay real funds out of the TSS-controlled vault against a +// deposit the validator set concluded never happened. PENDING-unreachable is the +// opposite situation - nobody can act at all - which is why it is accepted while +// REJECTED is not. +// +// REJECTED is refused deliberately, not by omission (F-2026-18801). The two +// terminal-failure statuses mean opposite things: +// +// - EXPIRED is uncertainty. Quorum never formed, so we do not know whether the +// deposit happened; the funds may genuinely be sitting in the source-chain +// gateway. Refunding is the right instinct. +// - REJECTED is a supermajority of universal validators affirmatively voting +// that the observation is invalid. Building a revert outbound for that would +// pay real funds out of the TSS-controlled vault against a deposit the +// validator set concluded never occurred. +// +// It is also unreachable for inbounds today: REJECTED is only produced by +// Ballot.IsFinalizingVote's threshold-FAILURE branch, and VoteOnInboundBallot +// hardcodes VOTE_RESULT_SUCCESS - an inbound observer either votes for what it +// saw or stays silent, there is no "I assert this did not happen" vote. So an +// inbound ballot terminates PASSED or EXPIRED, never REJECTED. +// +// If a negative-vote path for inbounds is ever added, this refusal must be +// revisited as a design decision rather than silently inherited; see the +// unreachable-status warning in BallotHooks.afterInboundBallotTerminal. // // Returns the new UTX ID and revert outbound ID for telemetry. func (k Keeper) RevertStuckInbound(ctx context.Context, inbound types.Inbound) (utxId, outboundId string, err error) { @@ -44,9 +112,17 @@ func (k Keeper) RevertStuckInbound(ctx context.Context, inbound types.Inbound) ( return "", "", errors.Wrap(sdkErrors.ErrNotFound, fmt.Sprintf("ballot for inbound not found (key=%s): %s", ballotKey, err)) } - if ballot.Status != uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED { + var revertReason string + switch { + case ballot.Status == uvalidatortypes.BallotStatus_BALLOT_STATUS_EXPIRED: + revertReason = "admin revert: stuck ballot expired" + case ballot.IsUnreachablePending(): + revertReason = "admin revert: pending ballot unreachable, every eligible voter has already voted" + default: return "", "", errors.Wrap(sdkErrors.ErrInvalidRequest, - fmt.Sprintf("ballot %s status is %s; admin revert requires EXPIRED (use MsgRecomputeBallotQuorum to drive a stuck pending ballot to EXPIRED)", + fmt.Sprintf("ballot %s status is %s; admin revert requires EXPIRED, or PENDING with every eligible voter already voted (no further vote can be cast). "+ + "MsgRecomputeBallotQuorum rebuilds the eligible-voter set from the live UV set and marks the ballot EXPIRED only when zero eligible voters remain, "+ + "so a pending ballot that still has an unvoted eligible voter has to be finalized by that voter through the normal vote flow", ballotKey, ballot.Status.String())) } @@ -63,28 +139,42 @@ func (k Keeper) RevertStuckInbound(ctx context.Context, inbound types.Inbound) ( InboundTx: &inbound, PcTx: []*types.PCTx{{ Status: "FAILED", - ErrorMsg: "admin revert: stuck ballot expired", + ErrorMsg: revertReason, }}, } if cErr := k.CreateUniversalTx(ctx, universalTxKey, utx); cErr != nil { return "", "", fmt.Errorf("failed to create utx for revert: %w", cErr) } - revertOutbound := k.buildRevertOutbound(sdkCtx, &inbound) + revertOutbound, buildErr := k.buildRevertOutbound(sdkCtx, &inbound) if revertOutbound == nil { - return "", "", fmt.Errorf("failed to build revert outbound for inbound %s", universalTxKey) + return "", "", fmt.Errorf("failed to build revert outbound for inbound %s: %w", universalTxKey, buildErr) + } + if buildErr != nil { + // Gas metadata was unresolvable, so the revert is recorded ABORTED instead of + // entering the signing queue. It is still attached: the attempt stays auditable + // and it makes the UTX eligible for RESCUE_FUNDS, which is the remaining route + // back to the user. + k.Logger().Error("admin revert: revert outbound recorded without gas metadata", + "utx_id", universalTxKey, + "outbound_id", revertOutbound.Id, + "error", buildErr.Error(), + ) } - if attachErr := k.attachOutboundsToUtx(sdkCtx, universalTxKey, []*types.OutboundTx{revertOutbound}, "admin revert: stuck ballot expired"); attachErr != nil { + if attachErr := k.attachOutboundsToUtx(sdkCtx, universalTxKey, []*types.OutboundTx{revertOutbound}, revertReason); attachErr != nil { return "", "", fmt.Errorf("failed to attach revert outbound: %w", attachErr) } k.Logger().Info("admin revert: inbound revert outbound created", "utx_id", universalTxKey, "outbound_id", revertOutbound.Id, + "status", revertOutbound.OutboundStatus.String(), "source_chain", inbound.SourceChain, "recipient", revertOutbound.Recipient, "amount", revertOutbound.Amount, + "ballot_status", ballot.Status.String(), + "reason", revertReason, ) return universalTxKey, revertOutbound.Id, nil diff --git a/x/uexecutor/keeper/ballot_hooks.go b/x/uexecutor/keeper/ballot_hooks.go index 85e53c5e4..541e39d1b 100644 --- a/x/uexecutor/keeper/ballot_hooks.go +++ b/x/uexecutor/keeper/ballot_hooks.go @@ -134,6 +134,30 @@ func (h BallotHooks) afterInboundBallotTerminal( // All variants are terminal-failure (EXPIRED or REJECTED). Preserve // the full audit trail in ExpiredInbounds for the future escape-hatch // refund flow. + // + // Only EXPIRED is reachable here for inbounds: REJECTED comes solely from + // Ballot.IsFinalizingVote's threshold-FAILURE branch, and VoteOnInboundBallot + // hardcodes VOTE_RESULT_SUCCESS. An inbound observer votes for what it saw or + // stays silent; disagreement forks the ballot key into a separate variant + // rather than voting against one. The admin hatch (RevertStuckInbound) + // therefore accepts EXPIRED only, and refuses REJECTED deliberately - see the + // reasoning there. + // + // Shout if that ever stops being true. A REJECTED inbound reaching this point + // means someone added a negative-vote path and silently reopened a terminal + // state with no refund route (F-2026-18801). + for _, v := range entry.Variants { + if v.TerminalStatus == uvalidatortypes.BallotStatus_BALLOT_STATUS_REJECTED { + h.k.Logger().Error( + "REJECTED inbound ballot variant reached terminal routing - this should be unreachable; "+ + "inbound votes are SUCCESS-only. RevertStuckInbound will refuse this entry, leaving it "+ + "with no shipped refund path. Revisit F-2026-18801 before shipping inbound negative voting.", + "utx_key", utxKey, + "ballot_id", v.BallotId, + ) + } + } + sdkCtx := sdk.UnwrapSDKContext(ctx) return h.k.ExpiredInbounds.Set(ctx, utxKey, types.ExpiredInboundEntry{ UtxKey: utxKey, diff --git a/x/uexecutor/keeper/build_revert_outbound.go b/x/uexecutor/keeper/build_revert_outbound.go index 34bf79dcd..f5430802c 100644 --- a/x/uexecutor/keeper/build_revert_outbound.go +++ b/x/uexecutor/keeper/build_revert_outbound.go @@ -1,13 +1,37 @@ package keeper import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/pushchain/push-chain-node/x/uexecutor/types" ) -// buildRevertOutbound creates an INBOUND_REVERT outbound with gas fields populated -// from the UniversalCore contract via getOutboundTxGasAndFees. -func (k Keeper) buildRevertOutbound(sdkCtx sdk.Context, inbound *types.Inbound) *types.OutboundTx { +// buildRevertOutbound creates an INBOUND_REVERT outbound that returns a failed +// inbound's funds on the source chain. +// +// The gas fields (gas token / fee / price / limit) are resolved from the +// UniversalCore contract and are mandatory: the universal validators refuse to +// sign an outbound whose gas price is zero or missing, so a revert built without +// them can never be broadcast, and re-resolving the metadata later does not +// rewrite the fields already stored on the outbound. +// +// Failure to resolve them is therefore never silent. The outbound is returned +// marked Status_ABORTED with an AbortReason instead of Status_PENDING, together +// with a non-nil error describing what failed: +// +// - it is still worth recording. The attempt stays in the audit trail and it +// makes the universal tx eligible for RESCUE_FUNDS, which is the recovery +// route for funds that never made it back to the user. +// - it must never be queued for signing. attachOutboundsToUtx enforces that by +// indexing only PENDING outbounds into PendingOutbounds. +// +// A nil outbound together with a non-nil error means nothing could be built at all. +func (k Keeper) buildRevertOutbound(sdkCtx sdk.Context, inbound *types.Inbound) (*types.OutboundTx, error) { + if inbound == nil { + return nil, fmt.Errorf("cannot build revert outbound: inbound is nil") + } + recipient := inbound.Sender if inbound.RevertInstructions != nil && inbound.RevertInstructions.FundRecipient != "" { recipient = inbound.RevertInstructions.FundRecipient @@ -32,30 +56,46 @@ func (k Keeper) buildRevertOutbound(sdkCtx sdk.Context, inbound *types.Inbound) // which is only for a failed export. if inbound.IsPc20 { outbound.Pc20ContractAddress = inbound.AssetAddr - return outbound + return outbound, nil } // Look up the PRC20 address for this external token tokenCfg, err := k.uregistryKeeper.GetTokenConfig(sdkCtx, inbound.SourceChain, inbound.AssetAddr) if err != nil || tokenCfg.NativeRepresentation == nil || tokenCfg.NativeRepresentation.ContractAddress == "" { - k.Logger().Warn("failed to get PRC20 for revert outbound gas lookup, proceeding without gas fields", + lookupErr := err + if lookupErr == nil { + lookupErr = fmt.Errorf("token config has no native representation") + } + abortErr := fmt.Errorf("failed to resolve PRC20 for revert outbound of %s on %s: %w", + inbound.AssetAddr, inbound.SourceChain, lookupErr) + + k.Logger().Error("revert outbound aborted: PRC20 lookup failed", "chain", inbound.SourceChain, "asset", inbound.AssetAddr, - "error", err, + "outbound_id", outbound.Id, + "error", abortErr.Error(), ) - return outbound + + abortRevertOutbound(outbound, abortErr) + return outbound, abortErr } // Fetch gas fields from UniversalCore.getOutboundTxGasAndFees(prc20, 0) // 0 means use the contract's baseLimit for this chain gasToken, gasFee, gasPrice, gasLimit, err := k.GetGasFeeInfoForRevertOutbound(sdkCtx, tokenCfg.NativeRepresentation.ContractAddress) if err != nil { - k.Logger().Warn("failed to fetch gas fee info for revert outbound, proceeding without gas fields", + abortErr := fmt.Errorf("failed to fetch gas fee info for revert outbound of PRC20 %s on %s: %w", + tokenCfg.NativeRepresentation.ContractAddress, inbound.SourceChain, err) + + k.Logger().Error("revert outbound aborted: gas fee lookup failed", "chain", inbound.SourceChain, "prc20", tokenCfg.NativeRepresentation.ContractAddress, - "error", err, + "outbound_id", outbound.Id, + "error", abortErr.Error(), ) - return outbound + + abortRevertOutbound(outbound, abortErr) + return outbound, abortErr } outbound.GasToken = gasToken @@ -63,5 +103,18 @@ func (k Keeper) buildRevertOutbound(sdkCtx sdk.Context, inbound *types.Inbound) outbound.GasPrice = gasPrice outbound.GasLimit = gasLimit - return outbound + return outbound, nil +} + +// abortRevertOutbound marks a half-built revert outbound as ABORTED with a reason. +// It mirrors the shape AbortOutbound writes for outbounds that are already attached +// to a universal tx; the matching outbound_aborted event is emitted by +// attachOutboundsToUtx, which is where the universal tx id is known. +func abortRevertOutbound(outbound *types.OutboundTx, reason error) { + outbound.OutboundStatus = types.Status_ABORTED + outbound.AbortReason = reason.Error() + outbound.GasToken = "" + outbound.GasFee = "" + outbound.GasPrice = "" + outbound.GasLimit = "" } diff --git a/x/uexecutor/keeper/build_revert_outbound_test.go b/x/uexecutor/keeper/build_revert_outbound_test.go new file mode 100644 index 000000000..e1d728f34 --- /dev/null +++ b/x/uexecutor/keeper/build_revert_outbound_test.go @@ -0,0 +1,247 @@ +package keeper_test + +import ( + "errors" + "math/big" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/ethereum/go-ethereum/common" + + "github.com/pushchain/push-chain-node/x/uexecutor/types" + uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types" +) + +const ( + revertSourceChain = "eip155:11155111" + revertAssetAddr = "0x0000000000000000000000000000000000000e07" + revertPRC20Addr = "0x0000000000000000000000000000000000000e06" + revertGasTokenHex = "0x0000000000000000000000000000000000001111" +) + +// revertTestInbound is a non-CEA FUNDS inbound whose execution failed, i.e. the +// input to buildRevertOutbound. +func revertTestInbound() *types.Inbound { + return &types.Inbound{ + SourceChain: revertSourceChain, + TxHash: "0xdeadbeef", + LogIndex: "1", + Sender: "0x778d3206374F8ac265728e18E3fE2Ae6b93E4ce4", + Recipient: "0x778d3206374F8ac265728e18E3fE2Ae6b93E4ce4", + Amount: "1000000", + AssetAddr: revertAssetAddr, + TxType: types.TxType_FUNDS, + RevertInstructions: &types.RevertInstructions{ + FundRecipient: "0x527F3692F5C53CfA83F7689885995606F93b6164", + }, + } +} + +func revertTestTokenConfig() uregistrytypes.TokenConfig { + return uregistrytypes.TokenConfig{ + Chain: revertSourceChain, + Address: revertAssetAddr, + Enabled: true, + NativeRepresentation: &uregistrytypes.NativeRepresentation{ + ContractAddress: revertPRC20Addr, + }, + } +} + +// expectGasFeeCall stubs UniversalCore.getOutboundTxGasAndFees to return a +// well-formed 6-output response, i.e. the healthy path. +func expectGasFeeCall(t *testing.T, f *testFixture, gasFee, gasPrice, gasLimit *big.Int) { + t.Helper() + + ucABI, err := types.ParseUniversalCoreABI() + require.NoError(t, err) + + packed, err := ucABI.Methods["getOutboundTxGasAndFees"].Outputs.Pack( + common.HexToAddress(revertGasTokenHex), // gasToken + gasFee, // gasFee + big.NewInt(0), // protocolFee + gasPrice, // gasPrice + "eip155", // chainNamespace + gasLimit, // gasLimitUsed + ) + require.NoError(t, err) + + // cosmos/evm v0.6.0: GetGasFeeInfoForRevertOutbound builds the StateDB itself + // and passes it into CallEVM, so the mock must expect that call too. + f.mockEVMKeeper.EXPECT().NewStateDB(gomock.Any()).Return(nil).AnyTimes() + f.mockEVMKeeper.EXPECT(). + CallEVM(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + gomock.Any(), gomock.Any(), gomock.Eq("getOutboundTxGasAndFees"), gomock.Any(), gomock.Any()). + Return(&evmtypes.MsgEthereumTxResponse{Ret: packed}, nil). + AnyTimes() +} + +// attachRevert stores a UTX and runs the revert outbound through the same attach +// path the production callers use, so PendingOutbounds indexing is exercised. +func attachRevert(t *testing.T, f *testFixture, utxId string, ob *types.OutboundTx) { + t.Helper() + + require.NoError(t, f.k.UniversalTx.Set(f.ctx, utxId, types.UniversalTx{ + Id: utxId, + InboundTx: revertTestInbound(), + })) + f.mockUregistryKeeper.EXPECT(). + GetChainConfig(gomock.Any(), revertSourceChain). + Return(uregistrytypes.ChainConfig{Chain: revertSourceChain}, nil). + AnyTimes() + + require.NoError(t, f.k.TestAttachOutboundsToUtx(f.ctx, utxId, []*types.OutboundTx{ob}, "execution failed")) +} + +func hasEvent(events sdk.Events, evtType string) bool { + for _, e := range events { + if e.Type == evtType { + return true + } + } + return false +} + +// TestBuildRevertOutbound_HealthyPath is the regression guard for the untouched +// path: when the gas metadata resolves, the revert is PENDING, carries the exact +// values UniversalCore returned, and is indexed for universal-validator pickup. +func TestBuildRevertOutbound_HealthyPath(t *testing.T) { + f := setupPendingOutboundFixture(t) + + f.mockUregistryKeeper.EXPECT(). + GetTokenConfig(gomock.Any(), revertSourceChain, revertAssetAddr). + Return(revertTestTokenConfig(), nil). + AnyTimes() + expectGasFeeCall(t, f, big.NewInt(123_456), big.NewInt(1_000_000_000), big.NewInt(200_000)) + + inbound := revertTestInbound() + ob, err := f.k.TestBuildRevertOutbound(f.ctx, inbound) + require.NoError(t, err, "healthy gas metadata must not produce an error") + require.NotNil(t, ob) + + require.Equal(t, types.Status_PENDING, ob.OutboundStatus, "healthy revert must stay PENDING so UVs sign it") + require.Empty(t, ob.AbortReason, "healthy revert must carry no abort reason") + require.Equal(t, types.TxType_INBOUND_REVERT, ob.TxType) + require.Equal(t, revertSourceChain, ob.DestinationChain) + require.Equal(t, inbound.Amount, ob.Amount) + require.Equal(t, inbound.AssetAddr, ob.ExternalAssetAddr) + require.Equal(t, inbound.RevertInstructions.FundRecipient, ob.Recipient) + + // Gas fields exactly as UniversalCore returned them. + require.Equal(t, common.HexToAddress(revertGasTokenHex).Hex(), ob.GasToken) + require.Equal(t, "123456", ob.GasFee) + require.Equal(t, "1000000000", ob.GasPrice) + require.Equal(t, "200000", ob.GasLimit) + + // ...and it still enters the signing queue. + attachRevert(t, f, "utx-healthy", ob) + entry, err := f.k.PendingOutbounds.Get(f.ctx, ob.Id) + require.NoError(t, err, "a PENDING revert must be indexed in PendingOutbounds") + require.Equal(t, "utx-healthy", entry.UniversalTxId) +} + +// TestBuildRevertOutbound_GasFeeLookupFails is the headline case: the +// UniversalCore call reverts, so the outbound must be recorded ABORTED and must +// never reach the signing queue. +func TestBuildRevertOutbound_GasFeeLookupFails(t *testing.T) { + f := setupPendingOutboundFixture(t) + + f.mockUregistryKeeper.EXPECT(). + GetTokenConfig(gomock.Any(), revertSourceChain, revertAssetAddr). + Return(revertTestTokenConfig(), nil). + AnyTimes() + // cosmos/evm v0.6.0: GetGasFeeInfoForRevertOutbound builds the StateDB itself + // and passes it into CallEVM, so the mock must expect that call too. + f.mockEVMKeeper.EXPECT().NewStateDB(gomock.Any()).Return(nil).AnyTimes() + f.mockEVMKeeper.EXPECT(). + CallEVM(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + gomock.Any(), gomock.Any(), gomock.Eq("getOutboundTxGasAndFees"), gomock.Any(), gomock.Any()). + Return(nil, errors.New("execution reverted: ZeroGasPrice")). + AnyTimes() + + ob, err := f.k.TestBuildRevertOutbound(f.ctx, revertTestInbound()) + require.Error(t, err, "a gas-metadata failure must be reported, not swallowed") + require.Contains(t, err.Error(), "gas fee info") + require.NotNil(t, ob, "the aborted attempt is still returned so it can be recorded") + + require.Equal(t, types.Status_ABORTED, ob.OutboundStatus, + "an unsignable revert must be ABORTED, never PENDING") + require.NotEmpty(t, ob.AbortReason, "abort reason must explain why the revert could not be built") + require.Contains(t, ob.AbortReason, "ZeroGasPrice") + require.Empty(t, ob.GasFee) + require.Empty(t, ob.GasPrice) + require.Empty(t, ob.GasLimit) + require.Empty(t, ob.GasToken) + + // Recorded on the UTX for the audit trail... + attachRevert(t, f, "utx-aborted", ob) + utx, found, err := f.k.GetUniversalTx(f.ctx, "utx-aborted") + require.NoError(t, err) + require.True(t, found) + require.Len(t, utx.OutboundTx, 1) + require.Equal(t, types.Status_ABORTED, utx.OutboundTx[0].OutboundStatus) + + // ...but NOT queued for signing: an unsignable row here would sit forever. + has, err := f.k.PendingOutbounds.Has(f.ctx, ob.Id) + require.NoError(t, err) + require.False(t, has, "an ABORTED revert must never be indexed in PendingOutbounds") + + require.True(t, hasEvent(f.ctx.EventManager().Events(), "outbound_aborted"), + "an outbound_aborted event must be emitted so monitoring sees the failure") +} + +// TestBuildRevertOutbound_TokenConfigMissing covers the other fail-open branch: +// the PRC20 for the inbound asset cannot be resolved at all. +func TestBuildRevertOutbound_TokenConfigMissing(t *testing.T) { + f := setupPendingOutboundFixture(t) + + f.mockUregistryKeeper.EXPECT(). + GetTokenConfig(gomock.Any(), revertSourceChain, revertAssetAddr). + Return(uregistrytypes.TokenConfig{}, errors.New("token config not found")). + AnyTimes() + + ob, err := f.k.TestBuildRevertOutbound(f.ctx, revertTestInbound()) + require.Error(t, err) + require.Contains(t, err.Error(), "PRC20") + require.NotNil(t, ob) + require.Equal(t, types.Status_ABORTED, ob.OutboundStatus) + require.Contains(t, ob.AbortReason, "token config not found") + + attachRevert(t, f, "utx-no-token-config", ob) + has, err := f.k.PendingOutbounds.Has(f.ctx, ob.Id) + require.NoError(t, err) + require.False(t, has, "an ABORTED revert must never be indexed in PendingOutbounds") +} + +// TestBuildRevertOutbound_TokenConfigWithoutNativeRepresentation covers a token +// config that resolves but carries no PRC20 — the lookup returns no error, so the +// abort reason has to be synthesised. +func TestBuildRevertOutbound_TokenConfigWithoutNativeRepresentation(t *testing.T) { + f := setupPendingOutboundFixture(t) + + f.mockUregistryKeeper.EXPECT(). + GetTokenConfig(gomock.Any(), revertSourceChain, revertAssetAddr). + Return(uregistrytypes.TokenConfig{Chain: revertSourceChain, Address: revertAssetAddr}, nil). + AnyTimes() + + ob, err := f.k.TestBuildRevertOutbound(f.ctx, revertTestInbound()) + require.Error(t, err) + require.NotNil(t, ob) + require.Equal(t, types.Status_ABORTED, ob.OutboundStatus) + require.Contains(t, ob.AbortReason, "no native representation") +} + +// TestBuildRevertOutbound_NilInbound proves the (outbound, error) contract: a nil +// outbound only ever comes back with a non-nil error, which is what the admin +// revert path checks before it claims a revert was created. +func TestBuildRevertOutbound_NilInbound(t *testing.T) { + f := setupPendingOutboundFixture(t) + + ob, err := f.k.TestBuildRevertOutbound(f.ctx, nil) + require.Error(t, err) + require.Nil(t, ob) +} diff --git a/x/uexecutor/keeper/chain_meta.go b/x/uexecutor/keeper/chain_meta.go index 179644685..c5777cf71 100644 --- a/x/uexecutor/keeper/chain_meta.go +++ b/x/uexecutor/keeper/chain_meta.go @@ -46,6 +46,8 @@ func (k Keeper) SetChainMeta(ctx context.Context, chainID string, chainMeta type // VoteChainMeta processes a universal validator's vote on chain metadata (gas price + chain height). // // Rules: +// 0. The observed chain must be registered in x/uregistry. Unregistered chains are +// rejected before any state is touched (F-2026-18803). // 1. Each vote is stamped with the current block time (storedAt) when it is recorded // and either inserted (new validator) or updated in place (existing validator). // 2. The oracle is bootstrapped on the first EVM write only after at least @@ -60,6 +62,21 @@ func (k Keeper) SetChainMeta(ctx context.Context, chainID string, chainMeta type // 5. Price median and chain-height median are computed independently (upper median = len/2). // 6. After a successful EVM call, LastAppliedChainHeight is updated. func (k Keeper) VoteChainMeta(ctx context.Context, universalValidator sdk.ValAddress, observedChainId string, price, blockNumber uint64) error { + // F-2026-18803: check the chain is registered before any state read/write. + // A GetChainMeta miss below *creates* the row on the cold-start path, so a + // vote for an arbitrary chain id would otherwise mint an unbounded number of + // ChainMetas keys (the raw id is the IAVL key) that every node then walks in + // AfterValidatorRemoved. Gate on *registered*, not IsChainInboundEnabled: + // chain meta also feeds gas-price quoting for outbounds, so an inbound-only + // check would starve outbound-enabled chains. + if _, err := k.uregistryKeeper.GetChainConfig(ctx, observedChainId); err != nil { + k.Logger().Warn("chain meta vote rejected: chain not registered", + "chain_id", observedChainId, + "validator", universalValidator.String(), + ) + return sdkerrors.Wrapf(err, "chain %s is not registered", observedChainId) + } + sdkCtx := sdk.UnwrapSDKContext(ctx) now := uint64(sdkCtx.BlockTime().Unix()) diff --git a/x/uexecutor/keeper/chain_meta_test.go b/x/uexecutor/keeper/chain_meta_test.go new file mode 100644 index 000000000..a51ac98a5 --- /dev/null +++ b/x/uexecutor/keeper/chain_meta_test.go @@ -0,0 +1,176 @@ +package keeper_test + +import ( + "testing" + "time" + + "cosmossdk.io/collections" + "github.com/golang/mock/gomock" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/x/uexecutor/types" + uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types" +) + +const ( + registeredChainID = "eip155:11155111" + unregisteredChainID = "eip155:999999999" +) + +// setupChainMetaFixture builds the keeper fixture with a deterministic block +// time so storedAt/staleness arithmetic is stable. +func setupChainMetaFixture(t *testing.T) *testFixture { + t.Helper() + f := SetupTest(t) + f.ctx = f.ctx.WithBlockTime(time.Unix(1_700_000_000, 0)) + return f +} + +// registerChain makes the uregistry mock answer GetChainConfig for chain. +func registerChain(f *testFixture, chain string) { + f.mockUregistryKeeper.EXPECT(). + GetChainConfig(gomock.Any(), chain). + Return(uregistrytypes.ChainConfig{ + Chain: chain, + VmType: uregistrytypes.VmType_EVM, + Enabled: &uregistrytypes.ChainEnabled{ + IsInboundEnabled: true, + IsOutboundEnabled: true, + }, + }, nil). + AnyTimes() +} + +// unregisterChain makes the uregistry mock report chain as absent, exactly as +// the real keeper does (collections.ErrNotFound out of ChainConfigs.Get). +func unregisterChain(f *testFixture, chain string) { + f.mockUregistryKeeper.EXPECT(). + GetChainConfig(gomock.Any(), chain). + Return(uregistrytypes.ChainConfig{}, collections.ErrNotFound). + AnyTimes() +} + +// countChainMetas returns every key currently present in the ChainMetas map. +func countChainMetas(t *testing.T, f *testFixture) []string { + t.Helper() + var keys []string + require.NoError(t, f.k.ChainMetas.Walk(f.ctx, nil, func(chainID string, _ types.ChainMeta) (bool, error) { + keys = append(keys, chainID) + return false, nil + })) + return keys +} + +// F-2026-18803: a vote for a chain that is not in x/uregistry must be rejected +// *before* the keeper writes anything. The finding is not "an error is missing" +// — it is that the GetChainMeta miss creates the row on the cold-start path, so +// the store assertion is the one that matters. +func TestVoteChainMeta_UnregisteredChain_RejectedAndStoreUnchanged(t *testing.T) { + f := setupChainMetaFixture(t) + require := require.New(t) + + unregisterChain(f, unregisteredChainID) + + before := countChainMetas(t, f) + require.Empty(before) + + err := f.k.VoteChainMeta(f.ctx, sdk.ValAddress(f.addrs[0]), unregisteredChainID, 100_000_000_000, 12345) + + // Store first, deliberately: the finding is the *row being written*, not a + // missing error. Removing the registry gate must break this assertion. + has, hasErr := f.k.ChainMetas.Has(f.ctx, unregisteredChainID) + require.NoError(hasErr) + require.False(has, "unregistered chain must not create a ChainMetas row") + require.Equal(before, countChainMetas(t, f), "ChainMetas must be unchanged") + + require.Error(err) + require.Contains(err.Error(), "is not registered") +} + +// An attacker-shaped id (long, arbitrary) must not become an IAVL key either. +func TestVoteChainMeta_UnregisteredLongChainId_WritesNoKey(t *testing.T) { + f := setupChainMetaFixture(t) + require := require.New(t) + + longID := "eip155:" + for i := 0; i < 200; i++ { + longID += "9" + } + unregisterChain(f, longID) + + err := f.k.VoteChainMeta(f.ctx, sdk.ValAddress(f.addrs[0]), longID, 1, 1) + + require.Empty(countChainMetas(t, f), "no ChainMetas key may be minted for an unregistered id") + require.Error(err) +} + +// A registered chain keeps working: the first vote is recorded and creates the +// row (this is required — bootstrap quorum can never be reached otherwise). +func TestVoteChainMeta_RegisteredChain_CreatesRow(t *testing.T) { + f := setupChainMetaFixture(t) + require := require.New(t) + + registerChain(f, registeredChainID) + + valAddr := sdk.ValAddress(f.addrs[0]) + require.NoError(f.k.VoteChainMeta(f.ctx, valAddr, registeredChainID, 100_000_000_000, 12345)) + + stored, found, err := f.k.GetChainMeta(f.ctx, registeredChainID) + require.NoError(err) + require.True(found) + require.Equal(registeredChainID, stored.ObservedChainId) + require.Equal([]string{valAddr.String()}, stored.Signers) + require.Equal([]uint64{100_000_000_000}, stored.Prices) + require.Equal([]uint64{12345}, stored.ChainHeights) + require.Equal([]uint64{uint64(f.ctx.BlockTime().Unix())}, stored.StoredAts) + // Below the bootstrap quorum the oracle is not written. + require.Equal(uint64(0), stored.LastAppliedChainHeight) + + require.Equal([]string{registeredChainID}, countChainMetas(t, f)) +} + +// Existing pre-bootstrap accumulation behaviour is unchanged for a registered +// chain: votes below chainMetaMinVotesForFirstWrite are stored, not applied. +func TestVoteChainMeta_RegisteredChain_BootstrapAccumulationUnchanged(t *testing.T) { + f := setupChainMetaFixture(t) + require := require.New(t) + + registerChain(f, registeredChainID) + + val0 := sdk.ValAddress(f.addrs[0]) + val1 := sdk.ValAddress(f.addrs[1]) + + require.NoError(f.k.VoteChainMeta(f.ctx, val0, registeredChainID, 100_000_000_000, 12345)) + require.NoError(f.k.VoteChainMeta(f.ctx, val1, registeredChainID, 200_000_000_000, 12346)) + + stored, found, err := f.k.GetChainMeta(f.ctx, registeredChainID) + require.NoError(err) + require.True(found) + require.Len(stored.Signers, 2) + require.Equal([]uint64{100_000_000_000, 200_000_000_000}, stored.Prices) + require.Equal(uint64(0), stored.LastAppliedChainHeight, "two votes must not bootstrap the oracle") + + // A re-vote from the same validator still updates in place, not appends. + require.NoError(f.k.VoteChainMeta(f.ctx, val0, registeredChainID, 400_000_000_000, 12350)) + stored, _, err = f.k.GetChainMeta(f.ctx, registeredChainID) + require.NoError(err) + require.Len(stored.Signers, 2) + require.Equal(uint64(400_000_000_000), stored.Prices[0]) + require.Equal(uint64(12350), stored.ChainHeights[0]) +} + +// Registering one chain must not implicitly admit its neighbours. +func TestVoteChainMeta_OnlyRegisteredChainAdmitted(t *testing.T) { + f := setupChainMetaFixture(t) + require := require.New(t) + + registerChain(f, registeredChainID) + unregisterChain(f, unregisteredChainID) + + valAddr := sdk.ValAddress(f.addrs[0]) + require.NoError(f.k.VoteChainMeta(f.ctx, valAddr, registeredChainID, 1, 1)) + require.Error(f.k.VoteChainMeta(f.ctx, valAddr, unregisteredChainID, 1, 1)) + + require.Equal([]string{registeredChainID}, countChainMetas(t, f)) +} diff --git a/x/uexecutor/keeper/create_outbound.go b/x/uexecutor/keeper/create_outbound.go index 151787210..34188fe35 100644 --- a/x/uexecutor/keeper/create_outbound.go +++ b/x/uexecutor/keeper/create_outbound.go @@ -58,6 +58,14 @@ func (k Keeper) BuildOutboundsFromReceipt( outbound, err := k.buildOutboundFromEvent(ctx, event, receipt.Hash, lg.Index) if err != nil { + // Wrapped so the caller can surface an actionable reason: the bare + // collections.ErrNotFound ("not found") says nothing about which leg + // of a multicall failed. + return nil, fmt.Errorf("no token config for PRC20 %s on chain %s: %w", event.Token, event.ChainId, err) + } + + // The gateway payload is attacker-controlled and lands in state (F-2026-18146). + if err := types.ValidateOutboundPayloadBlobSize("payload", event.Payload); err != nil { return nil, err } @@ -267,22 +275,28 @@ func (k Keeper) AttachRescueOutboundFromReceipt( // never arrived on Push Chain and are still locked on the source chain. // // Non-CEA inbounds: the auto-generated INBOUND_REVERT outbound must exist and - // have reached REVERTED status, meaning TSS could not return the funds to the - // source chain and they are stuck (held by the gateway contract or in escrow). + // have reached REVERTED or ABORTED status. REVERTED means TSS tried and could + // not return the funds to the source chain; ABORTED means the revert could not + // even be built (its gas metadata was unresolvable) so it was never queued for + // signing. Either way the funds never came back and are stuck (held by the + // gateway contract or in escrow), which is exactly what rescue exists for. if originalUtx.InboundTx.IsCEA { if len(originalUtx.PcTx) == 0 || originalUtx.PcTx[0] == nil || originalUtx.PcTx[0].Status != "FAILED" { return fmt.Errorf("rescue: UTX %s CEA deposit did not fail", originalUtxId) } } else { - hasRevertedAutoRevert := false + hasUnrecoveredAutoRevert := false for _, ob := range originalUtx.OutboundTx { - if ob != nil && ob.TxType == types.TxType_INBOUND_REVERT && ob.OutboundStatus == types.Status_REVERTED { - hasRevertedAutoRevert = true + if ob == nil || ob.TxType != types.TxType_INBOUND_REVERT { + continue + } + if ob.OutboundStatus == types.Status_REVERTED || ob.OutboundStatus == types.Status_ABORTED { + hasUnrecoveredAutoRevert = true break } } - if !hasRevertedAutoRevert { - return fmt.Errorf("rescue: UTX %s has no reverted inbound-revert outbound", originalUtxId) + if !hasUnrecoveredAutoRevert { + return fmt.Errorf("rescue: UTX %s has no reverted or aborted inbound-revert outbound", originalUtxId) } } @@ -323,17 +337,34 @@ func (k Keeper) AttachRescueOutboundFromReceipt( if originalUtx.InboundTx.IsPc20 { externalAssetAddr = originalUtx.InboundTx.AssetAddr } else { - tokenCfg, err := k.uregistryKeeper.GetTokenConfigByPRC20( + // Derived from the original stuck inbound, never taken from the rescue + // event. The amount below is always the original inbound's, so accepting + // the caller's PRC20 as the asset identity would pair one asset's raw + // amount with another asset's identity, amplified by differing decimals. + tokenCfg, err := k.uregistryKeeper.GetTokenConfig( ctx, originalUtx.InboundTx.SourceChain, - event.PRC20, + originalUtx.InboundTx.AssetAddr, ) if err != nil { - return fmt.Errorf("rescue: token config not found for PRC20 %s on %s: %w", - event.PRC20, originalUtx.InboundTx.SourceChain, err) + return fmt.Errorf("rescue: no token config registered for original asset %s on %s: %w", + originalUtx.InboundTx.AssetAddr, originalUtx.InboundTx.SourceChain, err) + } + if tokenCfg.NativeRepresentation == nil || tokenCfg.NativeRepresentation.ContractAddress == "" { + return fmt.Errorf("rescue: token config for original asset %s on %s has no PRC20 representation", + originalUtx.InboundTx.AssetAddr, originalUtx.InboundTx.SourceChain) + } + derivedPRC20 := tokenCfg.NativeRepresentation.ContractAddress + + // The event still names a PRC20 and it must agree with the derived one. + // Lenient canonicalization mirrors uregistry's own canonicalPRC20. + if utils.LenientCanonicalizeEVMAddress(event.PRC20) != utils.LenientCanonicalizeEVMAddress(derivedPRC20) { + return fmt.Errorf( + "rescue: event PRC20 %s does not match PRC20 %s registered for original asset %s on %s", + event.PRC20, derivedPRC20, originalUtx.InboundTx.AssetAddr, originalUtx.InboundTx.SourceChain) } externalAssetAddr = tokenCfg.Address - prc20AssetAddr = event.PRC20 + prc20AssetAddr = derivedPRC20 } logIndex := fmt.Sprintf("%d", lg.Index) @@ -397,14 +428,28 @@ func (k Keeper) attachOutboundsToUtx( } } - // Write to pending outbounds index (inside UpdateUniversalTx closure for atomicity) - if err := k.PendingOutbounds.Set(ctx, outbound.Id, types.PendingOutboundEntry{ - OutboundId: outbound.Id, - UniversalTxId: utxId, - CreatedAt: ctx.BlockHeight(), - SigningDeadline: signingDeadline, - }); err != nil { - return fmt.Errorf("failed to set pending outbound index for %s: %w", outbound.Id, err) + // Only PENDING outbounds belong in the signing queue. Anything already + // ABORTED (e.g. a revert whose gas metadata could not be resolved) is + // recorded on the universal tx for the audit trail, but indexing it would + // park a row that can never be signed: no ballot forms for it, nothing + // removes it, and there is no admin abort for outbounds. + if outbound.OutboundStatus == types.Status_PENDING { + // Write to pending outbounds index (inside UpdateUniversalTx closure for atomicity) + if err := k.PendingOutbounds.Set(ctx, outbound.Id, types.PendingOutboundEntry{ + OutboundId: outbound.Id, + UniversalTxId: utxId, + CreatedAt: ctx.BlockHeight(), + SigningDeadline: signingDeadline, + }); err != nil { + return fmt.Errorf("failed to set pending outbound index for %s: %w", outbound.Id, err) + } + } else { + k.Logger().Warn("outbound attached without entering the signing queue", + "utx_id", utxId, + "outbound_id", outbound.Id, + "status", outbound.OutboundStatus.String(), + "abort_reason", outbound.AbortReason, + ) } var pcTxHash string @@ -447,6 +492,17 @@ func (k Keeper) attachOutboundsToUtx( if err == nil { ctx.EventManager().EmitEvent(evt) } + + // Mirror AbortOutbound's monitoring signal for outbounds that arrive + // already aborted, so alerting sees them the same way. + if outbound.OutboundStatus == types.Status_ABORTED { + ctx.EventManager().EmitEvent(sdk.NewEvent( + "outbound_aborted", + sdk.NewAttribute("utx_id", utxId), + sdk.NewAttribute("outbound_id", outbound.Id), + sdk.NewAttribute("abort_reason", outbound.AbortReason), + )) + } } return nil diff --git a/x/uexecutor/keeper/evm.go b/x/uexecutor/keeper/evm.go index 51452f7f0..611dcdc93 100644 --- a/x/uexecutor/keeper/evm.go +++ b/x/uexecutor/keeper/evm.go @@ -7,11 +7,57 @@ import ( "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/pushchain/push-chain-node/x/uexecutor/types" uregistrytypes "github.com/pushchain/push-chain-node/x/uregistry/types" ) +// derivedModuleCall issues one committing EVM call whose sender is the uexecutor +// module account. +// +// It is the only place a module-sender DerivedEVMCall may be made from, because +// it is the only place that maintains the module's nonce (F-2026-18189). The +// nonce is taken from nextModuleSenderNonce and consumed by burnModuleSenderNonce +// whether or not the call succeeded — a reverted attempt commits nothing, so +// giving its nonce back would let a byte-identical retry reproduce a derived tx +// hash that was already emitted. Callers get the EVM error unchanged; the nonce +// bookkeeping is not part of it. +func (k Keeper) derivedModuleCall( + ctx sdk.Context, + contractABI abi.ABI, + moduleAddr, contract common.Address, + value, gasLimit *big.Int, + method string, + args ...interface{}, +) (*evmtypes.MsgEthereumTxResponse, error) { + nonce, err := k.nextModuleSenderNonce(ctx, moduleAddr) + if err != nil { + return nil, err + } + + res, callErr := k.evmKeeper.DerivedEVMCall( + ctx, + contractABI, + moduleAddr, // sender: module account + contract, // destination + value, + gasLimit, + true, // commit = true (real tx, not simulation) + false, // gasless = false (@dev: we need gas to be emitted in the tx receipt) + true, // module sender = true + &nonce, // manual nonce of module + method, + args..., + ) + + if err := k.burnModuleSenderNonce(ctx, moduleAddr, nonce); err != nil { + return nil, err + } + + return res, callErr +} + // CallFactoryToGetUEAAddressForOrigin calls FactoryV1.getUEAForOrigin(...) func (k Keeper) CallFactoryToGetUEAAddressForOrigin( ctx sdk.Context, @@ -175,13 +221,16 @@ func (k Keeper) moduleSenderNonce(ctx sdk.Context, from common.Address) (bool, * if !k.IsUeModuleAddress(ctx, from) { return false, nil, nil } - nonce, err := k.GetModuleAccountNonce(ctx) + // Routed through nextModuleSenderNonce/burnModuleSenderNonce so these call + // sites get the same reconciliation as derivedModuleCall: the handed-out + // nonce is max(counter, account nonce) and the advance is unconditional. + // A plain get-then-increment is what F-2026-18189 reported. + nonce, err := k.nextModuleSenderNonce(ctx, from) if err != nil { return false, nil, errors.Wrap(err, "failed to get module account nonce") } - // increment first (safe for internal modules) — mirrors the other module calls - if _, err := k.IncrementModuleAccountNonce(ctx); err != nil { - return false, nil, errors.Wrap(err, "failed to increment module account nonce") + if err := k.burnModuleSenderNonce(ctx, from, nonce); err != nil { + return false, nil, errors.Wrap(err, "failed to advance module account nonce") } return true, &nonce, nil } @@ -246,47 +295,6 @@ func (k Keeper) CallUEAExecutePayload( return res, nil } -// CallUEAMigrateUEA migrates UEA through existing UEA -func (k Keeper) CallUEAMigrateUEA( - ctx sdk.Context, - from, ueaAddr common.Address, - migration_payload *types.MigrationPayload, - signature []byte, -) (*evmtypes.MsgEthereumTxResponse, error) { - abi, err := types.ParseUeaABI() - if err != nil { - return nil, errors.Wrap(err, "failed to parse UEA ABI") - } - - abiMigrationPayload, err := types.NewAbiMigrationPayload(migration_payload) - if err != nil { - return nil, errors.Wrapf(err, "failed to create universal payload") - } - - // Module-sender migrations must advance the shared module nonce (see - // moduleSenderNonce); non-module senders use their own account sequence. - isModuleSender, moduleNonce, err := k.moduleSenderNonce(ctx, from) - if err != nil { - return nil, err - } - - return k.evmKeeper.DerivedEVMCall( - ctx, - abi, - from, - ueaAddr, - big.NewInt(0), - nil, - true, // commit = true (real tx, not simulation) - false, // gasless = false (@dev: we need gas to be emitted in the tx receipt) - isModuleSender, - moduleNonce, - "migrateUEA", - abiMigrationPayload, - signature, - ) -} - // CallUEADomainSeparator fetches the domainSeparator from the UEA contract func (k Keeper) CallUEADomainSeparator( ctx sdk.Context, @@ -336,22 +344,13 @@ func (k Keeper) CallPRC20Deposit( ueModuleAccAddress, _ := k.GetUeModuleAddress(ctx) - isModuleSender, moduleNonce, err := k.moduleSenderNonce(ctx, ueModuleAccAddress) - if err != nil { - return nil, err - } - - return k.evmKeeper.DerivedEVMCall( + return k.derivedModuleCall( ctx, abi, - ueModuleAccAddress, // sender: module account - handlerAddr, // destination + ueModuleAccAddress, + handlerAddr, big.NewInt(0), nil, - true, // commit = true (real tx, not simulation) - false, // gasless = false (@dev: we need gas to be emitted in the tx receipt) - isModuleSender, - moduleNonce, "depositPRC20Token", prc20Address, amount, @@ -376,22 +375,13 @@ func (k Keeper) CallUniversalCoreSetChainMeta( ueModuleAccAddress, _ := k.GetUeModuleAddress(ctx) - isModuleSender, moduleNonce, err := k.moduleSenderNonce(ctx, ueModuleAccAddress) - if err != nil { - return nil, err - } - - return k.evmKeeper.DerivedEVMCall( + return k.derivedModuleCall( ctx, abi, ueModuleAccAddress, handlerAddr, big.NewInt(0), nil, - true, - false, - isModuleSender, - moduleNonce, "setChainMeta", chainNamespace, price, @@ -790,22 +780,13 @@ func (k Keeper) CallPRC20DepositAutoSwap( ueModuleAccAddress, _ := k.GetUeModuleAddress(ctx) - isModuleSender, moduleNonce, err := k.moduleSenderNonce(ctx, ueModuleAccAddress) - if err != nil { - return nil, err - } - - return k.evmKeeper.DerivedEVMCall( + return k.derivedModuleCall( ctx, abi, - ueModuleAccAddress, // who is sending the transaction - handlerAddr, // destination: Handler contract + ueModuleAccAddress, + handlerAddr, big.NewInt(0), nil, - true, // commit = true (real tx, not simulation) - false, // gasless = false (@dev: we need gas to be emitted in the tx receipt) - isModuleSender, - moduleNonce, "depositPRC20WithAutoSwap", prc20Address, amount, @@ -836,23 +817,14 @@ func (k Keeper) CallUniversalCoreRefundUnusedGas( ueModuleAccAddress, _ := k.GetUeModuleAddress(ctx) - isModuleSender, moduleNonce, err := k.moduleSenderNonce(ctx, ueModuleAccAddress) - if err != nil { - return nil, err - } - // fee is uint24 in Solidity — pass as *big.Int (go-ethereum ABI packs non-standard widths as *big.Int) - return k.evmKeeper.DerivedEVMCall( + return k.derivedModuleCall( ctx, abi, ueModuleAccAddress, handlerAddr, big.NewInt(0), nil, - true, - false, - isModuleSender, - moduleNonce, "refundUnusedGas", gasToken, amount, @@ -882,22 +854,13 @@ func (k Keeper) CallExecuteUniversalTx( ueModuleAccAddress, _ := k.GetUeModuleAddress(ctx) - isModuleSender, moduleNonce, err := k.moduleSenderNonce(ctx, ueModuleAccAddress) - if err != nil { - return nil, err - } - - return k.evmKeeper.DerivedEVMCall( + return k.derivedModuleCall( ctx, recipientABI, ueModuleAccAddress, recipientAddr, big.NewInt(0), nil, - true, - false, - isModuleSender, - moduleNonce, "executeUniversalTx", sourceChain, ceaAddress, diff --git a/x/uexecutor/keeper/execute_inbound_funds.go b/x/uexecutor/keeper/execute_inbound_funds.go index fa3901ef9..5241f6aa8 100644 --- a/x/uexecutor/keeper/execute_inbound_funds.go +++ b/x/uexecutor/keeper/execute_inbound_funds.go @@ -74,7 +74,18 @@ func (k Keeper) ExecuteInboundFunds(ctx context.Context, utx types.UniversalTx) // isCEA failures never create an INBOUND_REVERT outbound // (consistent with execute_inbound_funds_and_payload.go and execute_inbound_gas_and_payload.go) if err != nil && !inbound.IsCEA { - revertOutbound := k.buildRevertOutbound(sdkCtx, inbound) + revertOutbound, buildErr := k.buildRevertOutbound(sdkCtx, inbound) + if buildErr != nil { + // The revert is still attached (recorded ABORTED) so the attempt stays + // auditable and the UTX becomes eligible for rescue. + k.Logger().Error("revert outbound could not be fully built", + "utx_id", utx.Id, + "error", buildErr.Error(), + ) + } + if revertOutbound == nil { + return nil + } if attachErr := k.attachOutboundsToUtx(sdkCtx, utx.Id, []*types.OutboundTx{revertOutbound}, err.Error()); attachErr != nil { if storeErr := k.UpdateUniversalTx(sdkCtx, utx.Id, func(u *types.UniversalTx) error { u.RevertError = attachErr.Error() diff --git a/x/uexecutor/keeper/execute_inbound_funds_and_payload.go b/x/uexecutor/keeper/execute_inbound_funds_and_payload.go index cf8884146..e00f0f888 100644 --- a/x/uexecutor/keeper/execute_inbound_funds_and_payload.go +++ b/x/uexecutor/keeper/execute_inbound_funds_and_payload.go @@ -170,7 +170,18 @@ func (k Keeper) ExecuteInboundFundsAndPayload(ctx context.Context, utx types.Uni // If deposit failed, stop here. if execErr != nil { if shouldRevert { - revertOutbound := k.buildRevertOutbound(sdkCtx, utx.InboundTx) + revertOutbound, buildErr := k.buildRevertOutbound(sdkCtx, utx.InboundTx) + if buildErr != nil { + // The revert is still attached (recorded ABORTED) so the attempt stays + // auditable and the UTX becomes eligible for rescue. + k.Logger().Error("revert outbound could not be fully built", + "utx_id", universalTxKey, + "error", buildErr.Error(), + ) + } + if revertOutbound == nil { + return nil + } if attachErr := k.attachOutboundsToUtx( sdkCtx, universalTxKey, @@ -218,6 +229,7 @@ func (k Keeper) ExecuteInboundFundsAndPayload(ctx context.Context, utx types.Uni var contractReceipt *evmtypes.MsgEthereumTxResponse var contractErr error var feeErr error + var attachErr error if resolveErr != nil { contractErr = resolveErr @@ -251,10 +263,29 @@ func (k Keeper) ExecuteInboundFundsAndPayload(ctx context.Context, utx types.Uni prc20Addr, txId, ) + if contractErr != nil { + // Reverted: cacheCtx is discarded, so bill the gas on the + // parent sdkCtx. + k.ChargeRevertedPayloadGas(ctx, sdkCtx, ueaAddr, contractReceipt, utx.InboundTx.UniversalPayload) + } if contractErr == nil { feeErr = k.DeductGasFeesFromReceipt(cacheCtx, cacheCtx, ueaAddr, contractReceipt, utx.InboundTx.UniversalPayload) if feeErr == nil { - writeCache() + // A successful callback may itself have called + // UniversalGatewayPC, burning PRC20 and emitting + // UniversalTxOutbound. DerivedEVMCall skips + // PostTxProcessing, so nothing else picks those logs up: + // without this attach the supply is burned and no + // OutboundTx / PendingOutbounds row is ever created. + // Attaching inside cacheCtx keeps the burn and the + // outbound rows atomic - if the attach fails the cache + // is discarded, rolling the burn back with it. + if contractReceipt != nil { + attachErr = k.AttachOutboundsToExistingUniversalTx(cacheCtx, contractReceipt, utx) + } + if attachErr == nil { + writeCache() + } } } } @@ -274,6 +305,8 @@ func (k Keeper) ExecuteInboundFundsAndPayload(ctx context.Context, utx types.Uni callPcTx.ErrorMsg = contractErr.Error() case feeErr != nil: callPcTx.ErrorMsg = fmt.Sprintf("gas fee deduction failed: %s", feeErr.Error()) + case attachErr != nil: + callPcTx.ErrorMsg = fmt.Sprintf("outbound attach failed: %s", attachErr.Error()) default: callPcTx.Status = "SUCCESS" } @@ -291,7 +324,7 @@ func (k Keeper) ExecuteInboundFundsAndPayload(ctx context.Context, utx types.Uni // --- Step 3: execute payload via UEA k.Logger().Debug("executing payload via UEA", "utx_key", universalTxKey, "uea", ueaAddr.Hex()) var payloadErr error - receipt, payloadErr = k.ExecutePayloadV2(ctx, ueModuleAddr, ueaAddr, utx.InboundTx.UniversalPayload, utx.InboundTx.VerificationData) + receipt, payloadErr = k.ExecutePayloadV2(ctx, ueModuleAddr, ueaAddr, utx.InboundTx.UniversalPayload, utx.InboundTx.VerificationData, utx) payloadPcTx := types.PCTx{ Sender: ueModuleAddressStr, @@ -317,16 +350,9 @@ func (k Keeper) ExecuteInboundFundsAndPayload(ctx context.Context, utx types.Uni "tx_hash", receipt.Hash, "gas_used", receipt.GasUsed, ) + // Outbounds are attached inside ExecutePayloadV2, atomically with the + // payload execution: reaching here means they are already committed. payloadPcTx.Status = "SUCCESS" - - if attachErr := k.AttachOutboundsToExistingUniversalTx(sdkCtx, receipt, utx); attachErr != nil { - if storeErr := k.UpdateUniversalTx(sdkCtx, universalTxKey, func(u *types.UniversalTx) error { - u.RevertError = attachErr.Error() - return nil - }); storeErr != nil { - return storeErr - } - } } updateErr2 := k.UpdateUniversalTx(ctx, universalTxKey, func(utx *types.UniversalTx) error { diff --git a/x/uexecutor/keeper/execute_inbound_gas.go b/x/uexecutor/keeper/execute_inbound_gas.go index 9a9d194db..132ec126c 100644 --- a/x/uexecutor/keeper/execute_inbound_gas.go +++ b/x/uexecutor/keeper/execute_inbound_gas.go @@ -190,7 +190,18 @@ func (k Keeper) ExecuteInboundGas(ctx context.Context, inbound types.Inbound) er } if execErr != nil && shouldRevert { - revertOutbound := k.buildRevertOutbound(sdkCtx, &inbound) + revertOutbound, buildErr := k.buildRevertOutbound(sdkCtx, &inbound) + if buildErr != nil { + // The revert is still attached (recorded ABORTED) so the attempt stays + // auditable and the UTX becomes eligible for rescue. + k.Logger().Error("revert outbound could not be fully built", + "utx_id", universalTxKey, + "error", buildErr.Error(), + ) + } + if revertOutbound == nil { + return nil + } if attachErr := k.attachOutboundsToUtx( sdkCtx, diff --git a/x/uexecutor/keeper/execute_inbound_gas_and_payload.go b/x/uexecutor/keeper/execute_inbound_gas_and_payload.go index baa7284d3..fde576ef4 100644 --- a/x/uexecutor/keeper/execute_inbound_gas_and_payload.go +++ b/x/uexecutor/keeper/execute_inbound_gas_and_payload.go @@ -189,7 +189,18 @@ func (k Keeper) ExecuteInboundGasAndPayload(ctx context.Context, utx types.Unive // --- create revert ONLY for pre-deposit / deposit failures (non-isCEA path) if execErr != nil && shouldRevert { - revertOutbound := k.buildRevertOutbound(sdkCtx, utx.InboundTx) + revertOutbound, buildErr := k.buildRevertOutbound(sdkCtx, utx.InboundTx) + if buildErr != nil { + // The revert is still attached (recorded ABORTED) so the attempt stays + // auditable and the UTX becomes eligible for rescue. + k.Logger().Error("revert outbound could not be fully built", + "utx_id", universalTxKey, + "error", buildErr.Error(), + ) + } + if revertOutbound == nil { + return nil + } if attachErr := k.attachOutboundsToUtx( sdkCtx, @@ -248,10 +259,27 @@ func (k Keeper) ExecuteInboundGasAndPayload(ctx context.Context, utx types.Unive ) var feeErr error + var attachErr error + if contractErr != nil { + // Reverted: cacheCtx is discarded, so bill on the parent sdkCtx. The + // gas deposit committed before the cache opened, so there is a balance. + k.ChargeRevertedPayloadGas(ctx, sdkCtx, ueaAddr, contractReceipt, utx.InboundTx.UniversalPayload) + } if contractErr == nil && contractReceipt != nil { feeErr = k.DeductGasFeesFromReceipt(cacheCtx, cacheCtx, ueaAddr, contractReceipt, utx.InboundTx.UniversalPayload) if feeErr == nil { - writeCache() + // A successful callback may itself have called + // UniversalGatewayPC, burning PRC20 and emitting + // UniversalTxOutbound. DerivedEVMCall skips PostTxProcessing, + // so nothing else picks those logs up: without this attach the + // supply is burned and no OutboundTx / PendingOutbounds row is + // ever created. Attaching inside cacheCtx keeps the burn and the + // outbound rows atomic - if the attach fails the cache is + // discarded, rolling the burn back with it. + attachErr = k.AttachOutboundsToExistingUniversalTx(cacheCtx, contractReceipt, utx) + if attachErr == nil { + writeCache() + } } } @@ -271,6 +299,8 @@ func (k Keeper) ExecuteInboundGasAndPayload(ctx context.Context, utx types.Unive // EVM call returned nil receipt without error — leave Status FAILED, no message. case feeErr != nil: callPcTx.ErrorMsg = fmt.Sprintf("gas fee deduction failed: %s", feeErr.Error()) + case attachErr != nil: + callPcTx.ErrorMsg = fmt.Sprintf("outbound attach failed: %s", attachErr.Error()) default: callPcTx.Status = "SUCCESS" } @@ -295,6 +325,7 @@ func (k Keeper) ExecuteInboundGasAndPayload(ctx context.Context, utx types.Unive ueaAddr, utx.InboundTx.UniversalPayload, utx.InboundTx.VerificationData, + utx, ) payloadPcTx := types.PCTx{ @@ -321,16 +352,9 @@ func (k Keeper) ExecuteInboundGasAndPayload(ctx context.Context, utx types.Unive "tx_hash", receipt.Hash, "gas_used", receipt.GasUsed, ) + // Outbounds are attached inside ExecutePayloadV2, atomically with the + // payload execution: reaching here means they are already committed. payloadPcTx.Status = "SUCCESS" - - if attachErr := k.AttachOutboundsToExistingUniversalTx(sdkCtx, receipt, utx); attachErr != nil { - if storeErr := k.UpdateUniversalTx(sdkCtx, universalTxKey, func(u *types.UniversalTx) error { - u.RevertError = attachErr.Error() - return nil - }); storeErr != nil { - return storeErr - } - } } updateErr := k.UpdateUniversalTx(ctx, universalTxKey, func(utx *types.UniversalTx) error { diff --git a/x/uexecutor/keeper/execute_payload.go b/x/uexecutor/keeper/execute_payload.go index 58e81adfa..900147280 100644 --- a/x/uexecutor/keeper/execute_payload.go +++ b/x/uexecutor/keeper/execute_payload.go @@ -12,9 +12,10 @@ import ( "github.com/pushchain/push-chain-node/x/uexecutor/types" ) -// ExecutePayloadV2 executes a universal payload through a UEA. +// ExecutePayloadV2 executes a universal payload through a UEA and attaches the +// gateway outbounds it emitted to utx, atomically with the execution itself. // The caller is responsible for resolving and validating ueaAddr before calling this function. -func (k Keeper) ExecutePayloadV2(ctx context.Context, evmFrom common.Address, ueaAddr common.Address, universalPayload *types.UniversalPayload, verificationData string) (*vmtypes.MsgEthereumTxResponse, error) { +func (k Keeper) ExecutePayloadV2(ctx context.Context, evmFrom common.Address, ueaAddr common.Address, universalPayload *types.UniversalPayload, verificationData string, utx types.UniversalTx) (*vmtypes.MsgEthereumTxResponse, error) { sdkCtx := sdk.UnwrapSDKContext(ctx) k.Logger().Debug("execute payload v2", @@ -32,10 +33,10 @@ func (k Keeper) ExecutePayloadV2(ctx context.Context, evmFrom common.Address, ue return nil, errors.Wrapf(err, "invalid verificationData format") } - // Step 2: Wrap EVM execution + fee deduction in a CacheContext so they - // commit/revert together. If fee deduction fails, the EVM state changes - // from CallUEAExecutePayload are discarded — closes the free-execution - // gap when the UEA has no native UPC to cover gas. + // Step 2: Wrap EVM execution + fee deduction + outbound attach in a + // CacheContext so they commit/revert together. If fee deduction fails, the + // EVM state changes from CallUEAExecutePayload are discarded — closes the + // free-execution gap when the UEA has no native UPC to cover gas. cacheCtx, writeCache := sdkCtx.CacheContext() receipt, execErr := k.CallUEAExecutePayload(cacheCtx, evmFrom, ueaAddr, universalPayload, verificationDataVal) @@ -49,10 +50,25 @@ func (k Keeper) ExecutePayloadV2(ctx context.Context, evmFrom common.Address, ue if execErr != nil { // EVM execution failed — cache discarded by not calling writeCache. + // Bill the gas on the parent sdkCtx so the charge survives the discard. + k.ChargeRevertedPayloadGas(ctx, sdkCtx, ueaAddr, receipt, universalPayload) return receipt, execErr } - // Both succeeded — commit EVM state and fee deduction together. + // Step 4: Attach the outbounds the payload emitted, still inside the cache. + // A payload that calls UniversalGatewayPC has already burned the PRC20 by + // the time we get here, and BuildOutboundsFromReceipt is all-or-nothing: + // one invalid leg of a multicall (unregistered PRC20, disabled chain) + // discards every valid outbound alongside it. Attaching here means that + // failure also discards the burn, instead of leaving burned supply with no + // OutboundTx and no PendingOutbounds row to deliver against. + if receipt != nil { + if attachErr := k.AttachOutboundsToExistingUniversalTx(cacheCtx, receipt, utx); attachErr != nil { + return receipt, fmt.Errorf("outbound attach failed: %w", attachErr) + } + } + + // All succeeded — commit EVM state, fee deduction and outbounds together. writeCache() k.Logger().Debug("payload executed via UEA", diff --git a/x/uexecutor/keeper/export_test.go b/x/uexecutor/keeper/export_test.go index 205296bcd..9cb2e23a3 100644 --- a/x/uexecutor/keeper/export_test.go +++ b/x/uexecutor/keeper/export_test.go @@ -9,6 +9,10 @@ func (k Keeper) TestAttachOutboundsToUtx(ctx sdk.Context, utxId string, outbound return k.attachOutboundsToUtx(ctx, utxId, outbounds, revertMsg) } +func (k Keeper) TestBuildRevertOutbound(ctx sdk.Context, inbound *types.Inbound) (*types.OutboundTx, error) { + return k.buildRevertOutbound(ctx, inbound) +} + func (k Keeper) TestBuildOutboundFromEvent(ctx sdk.Context, event *types.UniversalTxOutboundEvent, txHash string, logIndex uint64) (*types.OutboundTx, error) { return k.buildOutboundFromEvent(ctx, event, txHash, logIndex) } diff --git a/x/uexecutor/keeper/fees.go b/x/uexecutor/keeper/fees.go index bee45f344..76ec82640 100644 --- a/x/uexecutor/keeper/fees.go +++ b/x/uexecutor/keeper/fees.go @@ -90,6 +90,67 @@ func (k Keeper) CalculateGasCost( return gasCost, nil } +// ChargeRevertedPayloadGas bills a reverted payload for the gas it burned. It +// charges the parent ctx so the amount survives the caller's discarded EVM cache, +// clamps to the balance held, and never returns an error — callers record a FAILED +// PcTx and carry on. +// +// INBOUND ROUTES ONLY. Never call this from MsgExecutePayload: submission there is +// permissionless and a revert rolls back the UEA nonce, so one captured owner +// signature stays replayable and billing it would let anyone drain the UEA. +func (k Keeper) ChargeRevertedPayloadGas( + ctx context.Context, + sdkCtx sdk.Context, + recipient common.Address, + receipt *evmtypes.MsgEthereumTxResponse, + universalPayload *types.UniversalPayload, +) { + if receipt == nil || receipt.GasUsed == 0 || universalPayload == nil { + return + } + + abiPayload, err := types.NewAbiUniversalPayload(universalPayload) + if err != nil { + return + } + baseFee := k.feemarketKeeper.GetBaseFee(sdkCtx) + if baseFee.IsNil() { + return + } + gasCost, err := k.CalculateGasCost(baseFee, abiPayload.MaxFeePerGas, abiPayload.MaxPriorityFeePerGas, receipt.GasUsed) + if err != nil || gasCost.Sign() <= 0 { + return + } + + recipientAccAddr := sdk.AccAddress(recipient.Bytes()) + available := k.bankKeeper.GetBalance(sdkCtx, recipientAccAddr, pchaintypes.BaseDenom).Amount.BigInt() + + charge := gasCost + if available.Cmp(gasCost) < 0 { + charge = available + } + if charge.Sign() <= 0 { + k.Logger().Info("reverted payload not billed: no balance", + "recipient", recipient.Hex(), "gas_used", receipt.GasUsed, "gas_cost", gasCost.String()) + return + } + + if err := k.DeductAndBurnFees(ctx, recipientAccAddr, charge); err != nil { + // Best effort: never fail the message over the revert-path charge. + k.Logger().Error("failed to bill reverted payload gas", + "recipient", recipient.Hex(), "charge", charge.String(), "error", err) + return + } + + k.Logger().Info("reverted payload gas billed", + "recipient", recipient.Hex(), + "gas_used", receipt.GasUsed, + "gas_cost", gasCost.String(), + "charged", charge.String(), + "partial", charge.Cmp(gasCost) < 0, + ) +} + // DeductGasFeesFromReceipt calculates and deducts gas fees from a recipient address // based on the EVM receipt and universal payload parameters. // Returns nil if receipt is nil (Go-level error, no EVM tx was created). diff --git a/x/uexecutor/keeper/handle_failed_inbound_validation.go b/x/uexecutor/keeper/handle_failed_inbound_validation.go index 0713aaa32..1c00f4702 100644 --- a/x/uexecutor/keeper/handle_failed_inbound_validation.go +++ b/x/uexecutor/keeper/handle_failed_inbound_validation.go @@ -44,7 +44,18 @@ func (k Keeper) handleFailedInboundValidation(sdkCtx sdk.Context, utx types.Univ "source_chain", inbound.SourceChain, "amount", inbound.Amount, ) - revertOutbound := k.buildRevertOutbound(sdkCtx, inbound) + revertOutbound, buildErr := k.buildRevertOutbound(sdkCtx, inbound) + if buildErr != nil { + // The revert is still attached (recorded ABORTED) so the attempt stays + // auditable and the UTX becomes eligible for rescue. + k.Logger().Error("revert outbound could not be fully built", + "utx_key", universalTxKey, + "error", buildErr.Error(), + ) + } + if revertOutbound == nil { + return nil + } if attachErr := k.attachOutboundsToUtx( sdkCtx, diff --git a/x/uexecutor/keeper/keeper.go b/x/uexecutor/keeper/keeper.go index 41881c08e..4b260ee46 100755 --- a/x/uexecutor/keeper/keeper.go +++ b/x/uexecutor/keeper/keeper.go @@ -3,6 +3,7 @@ package keeper import ( "context" "errors" + "fmt" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -364,20 +365,78 @@ func (k Keeper) GetModuleAccountNonce(ctx sdk.Context) (uint64, error) { return nonce, nil } -// IncrementModuleAccountNonce increases the nonce by 1 and stores it back. +// SetModuleAccountNonce allows explicitly setting the nonce (optional, for migration or testing). +// It keeps the module account's EVM nonce in step, so the two can never diverge — +// see nextModuleSenderNonce for why that matters. +func (k Keeper) SetModuleAccountNonce(ctx sdk.Context, nonce uint64) error { + if err := k.ModuleAccountNonce.Set(ctx, nonce); err != nil { + return err + } + + acc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleName) + if acc == nil { + return fmt.Errorf("module account %s not found", types.ModuleName) + } + if acc.GetSequence() == nonce { + return nil + } + if err := acc.SetSequence(nonce); err != nil { + return err + } + k.accountKeeper.SetAccount(ctx, acc) + + return nil +} + +// nextModuleSenderNonce picks the nonce for the module's next DerivedEVMCall. +// +// F-2026-18189. The module account's EVM nonce is the source of truth, but x/vm +// will not maintain it: ApplyMessageWithConfig advances a sender's nonce only in +// its contractCreation branch, and every call the module makes is a plain CALL. +// So the module maintains it itself (see burnModuleSenderNonce), and reads it +// back here so that a nonce the EVM *did* advance — a CREATE from the module, or +// a chain upgraded from a build that left the account nonce behind — is picked up +// instead of being re-issued. +// IncrementModuleAccountNonce hands out the module sender's next nonce and burns +// it, so callers outside the keeper advance it the same way derivedModuleCall +// does. Returns the value the counter now sits at. func (k Keeper) IncrementModuleAccountNonce(ctx sdk.Context) (uint64, error) { - nonce, err := k.GetModuleAccountNonce(ctx) + moduleAddr, _ := k.GetUeModuleAddress(ctx) + nonce, err := k.nextModuleSenderNonce(ctx, moduleAddr) if err != nil { return 0, err } - newNonce := nonce + 1 - if err := k.ModuleAccountNonce.Set(ctx, newNonce); err != nil { + if err := k.burnModuleSenderNonce(ctx, moduleAddr, nonce); err != nil { return 0, err } - return newNonce, nil + return nonce + 1, nil } -// SetModuleAccountNonce allows explicitly setting the nonce (optional, for migration or testing). -func (k Keeper) SetModuleAccountNonce(ctx sdk.Context, nonce uint64) error { - return k.ModuleAccountNonce.Set(ctx, nonce) +func (k Keeper) nextModuleSenderNonce(ctx sdk.Context, moduleAddr common.Address) (uint64, error) { + nonce, err := k.GetModuleAccountNonce(ctx) + if err != nil { + return 0, err + } + if evmNonce := k.evmKeeper.GetNonce(ctx, moduleAddr); evmNonce > nonce { + nonce = evmNonce + } + return nonce, nil +} + +// burnModuleSenderNonce consumes the nonce handed out by nextModuleSenderNonce. +// +// The advance is unconditional: it happens whether the call committed, reverted, +// or never reached the EVM at all. That is deliberate. The derived tx hash is +// ethtypes.NewTx(&DynamicFeeTx{Nonce, GasFeeCap, GasTipCap, Gas, To, Value, +// Data}).Hash(), so the nonce is the only thing separating two byte-identical +// module calls; a failed attempt that gave its nonce back would let the retry +// reproduce a hash already emitted in this block. Because the counter and the +// account nonce move together, advancing on failure can no longer desync them — +// which is what F-2026-18189 reported. +func (k Keeper) burnModuleSenderNonce(ctx sdk.Context, moduleAddr common.Address, nonce uint64) error { + next := nonce + 1 + if evmNonce := k.evmKeeper.GetNonce(ctx, moduleAddr); evmNonce > next { + next = evmNonce + } + return k.SetModuleAccountNonce(ctx, next) } diff --git a/x/uexecutor/keeper/keeper_test.go b/x/uexecutor/keeper/keeper_test.go index f2a1d72d4..dd79ae95a 100755 --- a/x/uexecutor/keeper/keeper_test.go +++ b/x/uexecutor/keeper/keeper_test.go @@ -50,6 +50,9 @@ var maccPerms = map[string][]string{ stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, minttypes.ModuleName: {authtypes.Minter}, govtypes.ModuleName: {authtypes.Burner}, + // The uexecutor module account is resolved by Keeper.GetUeModuleAddress, which + // every UniversalCore call goes through. + types.ModuleName: nil, } type testFixture struct { @@ -119,11 +122,11 @@ func SetupTest(t *testing.T) *testFixture { registerBaseSDKModules(logger, f, encCfg, keys, accountAddressCodec, validatorAddressCodec, consensusAddressCodec) // Setup Keeper. - f.k = keeper.NewKeeper(encCfg.Codec, runtime.NewKVStoreService(keys[types.ModuleName]), logger, f.govModAddr, f.mockEVMKeeper, &feemarketkeeper.Keeper{}, f.mockBankKeeper, authkeeper.AccountKeeper{}, f.mockUregistryKeeper, &uvalidatorKeeper.Keeper{}) + f.k = keeper.NewKeeper(encCfg.Codec, runtime.NewKVStoreService(keys[types.ModuleName]), logger, f.govModAddr, f.mockEVMKeeper, &feemarketkeeper.Keeper{}, f.mockBankKeeper, f.accountkeeper, f.mockUregistryKeeper, &uvalidatorKeeper.Keeper{}) f.k.SetUCallbackKeeper(f.mockUCallbackKeeper) f.msgServer = keeper.NewMsgServerImpl(f.k) f.queryServer = keeper.NewQuerier(f.k) - f.appModule = module.NewAppModule(encCfg.Codec, f.k, f.mockEVMKeeper, &feemarketkeeper.Keeper{}, f.mockBankKeeper, authkeeper.AccountKeeper{}, f.mockUregistryKeeper, &uvalidatorKeeper.Keeper{}) + f.appModule = module.NewAppModule(encCfg.Codec, f.k, f.mockEVMKeeper, &feemarketkeeper.Keeper{}, f.mockBankKeeper, f.accountkeeper, f.mockUregistryKeeper, &uvalidatorKeeper.Keeper{}) return f } @@ -149,8 +152,11 @@ func registerBaseSDKModules( registerModuleInterfaces(encCfg) // Auth Keeper. + // NOTE: keys is built from module names, and authtypes.StoreKey ("acc") is not + // authtypes.ModuleName ("auth") — looking up the wrong one yields a nil store key + // and panics the first time the account keeper is actually touched. f.accountkeeper = authkeeper.NewAccountKeeper( - encCfg.Codec, runtime.NewKVStoreService(keys[authtypes.StoreKey]), + encCfg.Codec, runtime.NewKVStoreService(keys[authtypes.ModuleName]), authtypes.ProtoBaseAccount, maccPerms, ac, app.Bech32PrefixAccAddr, diff --git a/x/uexecutor/keeper/msg_execute_payload.go b/x/uexecutor/keeper/msg_execute_payload.go index 946b1e12a..d12e51c7a 100644 --- a/x/uexecutor/keeper/msg_execute_payload.go +++ b/x/uexecutor/keeper/msg_execute_payload.go @@ -25,7 +25,16 @@ func (k Keeper) ExecutePayload(ctx context.Context, evmFrom common.Address, univ "owner", universalAccountId.Owner, ) - // Step 1: Validate payload and verificationData early (fast-fail before EVM work) + // Step 1: Validate payload and verificationData early (fast-fail before EVM work). + // The size cap is re-applied here rather than left to MsgExecutePayload's + // ValidateBasic so it holds for every caller of this keeper method — the msg + // route is fee exempt, so nothing else prices these bytes. + if err := universalPayload.ValidateSize(); err != nil { + return err + } + if err := types.ValidatePayloadBlobSize("verificationData", verificationData); err != nil { + return err + } if _, err := types.NewAbiUniversalPayload(universalPayload); err != nil { return errors.Wrapf(err, "invalid universal payload") } diff --git a/x/uexecutor/keeper/msg_migrate_uea.go b/x/uexecutor/keeper/msg_migrate_uea.go deleted file mode 100644 index f59d4a301..000000000 --- a/x/uexecutor/keeper/msg_migrate_uea.go +++ /dev/null @@ -1,84 +0,0 @@ -package keeper - -import ( - "context" - "fmt" - - "cosmossdk.io/errors" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - "github.com/pushchain/push-chain-node/utils" - "github.com/pushchain/push-chain-node/x/uexecutor/types" -) - -// updateParams is for updating params collections of the module -func (k Keeper) MigrateUEA(ctx context.Context, evmFrom common.Address, universalAccountId *types.UniversalAccountId, migrationPayload *types.MigrationPayload, signature string) error { - sdkCtx := sdk.UnwrapSDKContext(ctx) - - // Get Caip2Identifier for the universal account - caip2Identifier := universalAccountId.GetCAIP2() - - k.Logger().Info("migrate UEA", - "from", evmFrom.Hex(), - "chain", caip2Identifier, - "owner", universalAccountId.Owner, - ) - - // Step 1: Parse and validate payload and signature - _, err := types.NewAbiMigrationPayload(migrationPayload) - if err != nil { - return errors.Wrapf(err, "invalid migration payload") - } - - // add signature verification - signatureVal, err := utils.HexToBytes(signature) - if err != nil { - return errors.Wrapf(err, "invalid signature format") - } - - chainConfig, err := k.uregistryKeeper.GetChainConfig(sdkCtx, caip2Identifier) - if err != nil { - return errors.Wrapf(err, "failed to get chain config for chain %s", caip2Identifier) - } - - // TODO: Decide later if migration should be disabled if inbound is disabled - if !chainConfig.Enabled.IsInboundEnabled { - k.Logger().Warn("migrate UEA rejected: chain not enabled", "chain", caip2Identifier) - return fmt.Errorf("chain %s is not enabled", caip2Identifier) - } - - factoryAddress := common.HexToAddress(types.FACTORY_PROXY_ADDRESS_HEX) - - // Step 2: Compute smart account address - // Calling factory contract to compute the UEA address - ueaAddr, isDeployed, err := k.CallFactoryToGetUEAAddressForOrigin(sdkCtx, evmFrom, factoryAddress, universalAccountId) - if err != nil { - return err - } - - if !isDeployed { - k.Logger().Warn("migrate UEA rejected: UEA not deployed", "chain", caip2Identifier, "owner", universalAccountId.Owner) - return fmt.Errorf("UEA is not deployed") - } - - k.Logger().Debug("migrating UEA", - "uea", ueaAddr.Hex(), - "chain", caip2Identifier, - "from", evmFrom.Hex(), - ) - - // Step 3: Migrate UEA through UEA - receipt, err := k.CallUEAMigrateUEA(sdkCtx, evmFrom, ueaAddr, migrationPayload, signatureVal) - if err != nil { - return err - } - - k.Logger().Info("UEA migrated", - "chain", caip2Identifier, - "uea", ueaAddr.Hex(), - "tx_hash", receipt.Hash, - "gas_used", receipt.GasUsed, - ) - - return nil -} diff --git a/x/uexecutor/keeper/msg_server.go b/x/uexecutor/keeper/msg_server.go index 4db727697..bfa00958d 100755 --- a/x/uexecutor/keeper/msg_server.go +++ b/x/uexecutor/keeper/msg_server.go @@ -54,21 +54,6 @@ func (ms msgServer) ExecutePayload(ctx context.Context, msg *types.MsgExecutePay return &types.MsgExecutePayloadResponse{}, nil } -// MigrateUEA handles UEA Migration. -func (ms msgServer) MigrateUEA(ctx context.Context, msg *types.MsgMigrateUEA) (*types.MsgMigrateUEAResponse, error) { - _, evmFromAddress, err := utils.GetAddressPair(msg.Signer) - if err != nil { - return nil, errors.Wrapf(err, "failed to parse signer address") - } - - err = ms.k.MigrateUEA(ctx, evmFromAddress, msg.UniversalAccountId, msg.MigrationPayload, msg.Signature) - if err != nil { - return nil, err - } - - return &types.MsgMigrateUEAResponse{}, nil -} - // VoteInbound implements types.MsgServer. func (ms msgServer) VoteInbound(ctx context.Context, msg *types.MsgVoteInbound) (*types.MsgVoteInboundResponse, error) { signerAccAddr, err := sdk.AccAddressFromBech32(msg.Signer) @@ -153,14 +138,6 @@ func (ms msgServer) VoteChainMeta(ctx context.Context, msg *types.MsgVoteChainMe signerValAddr := sdk.ValAddress(signerAccAddr) - isBonded, err := ms.k.uvalidatorKeeper.IsBondedUniversalValidator(ctx, msg.Signer) - if err != nil { - return nil, errors.Wrapf(err, "failed to check bonded status for signer %s", msg.Signer) - } - if !isBonded { - return nil, fmt.Errorf("universal validator for signer %s is not bonded", msg.Signer) - } - isTombstoned, err := ms.k.uvalidatorKeeper.IsTombstonedUniversalValidator(ctx, msg.Signer) if err != nil { return nil, errors.Wrapf(err, "failed to check tombstoned status for signer %s", msg.Signer) @@ -169,6 +146,26 @@ func (ms msgServer) VoteChainMeta(ctx context.Context, msg *types.MsgVoteChainMe return nil, fmt.Errorf("universal validator for signer %s is tombstoned", msg.Signer) } + // Admission is gated on the same eligibility predicate ballot creation uses + // (lifecycle ACTIVE/PENDING_JOIN AND bonded AND not tombstoned) rather than + // the lifecycle-blind IsBondedUniversalValidator. Admin removal moves a + // universal validator to PENDING_LEAVE while its stake can remain bonded, + // and AfterValidatorRemoved prunes its ChainMeta rows but revokes neither + // its AuthZ grant nor its membership in the universal validator set -- so + // under the bonded-only gate the removed hotkey could reinsert votes right + // after the prune. + // + // Tightening admission is safe here, and only here, because ChainMeta is + // median-based rather than ballot-based: there is no CreateBallot, no + // snapshotted EligibleVoters and no frozen VotingThreshold. Every vote + // recomputes the median over whichever votes are currently fresh, so a + // narrower voter set cannot strand anything in flight. The ballot-based + // vote paths (VoteInbound/VoteOutbound above) deliberately keep the looser + // gate: tightening them would make already-frozen thresholds unreachable. + if err := ms.requireEligibleChainMetaVoter(ctx, signerValAddr); err != nil { + return nil, err + } + err = ms.k.VoteChainMeta(ctx, signerValAddr, msg.ObservedChainId, msg.Price, msg.ChainHeight) if err != nil { return nil, err @@ -176,6 +173,30 @@ func (ms msgServer) VoteChainMeta(ctx context.Context, msg *types.MsgVoteChainMe return &types.MsgVoteChainMetaResponse{}, nil } +// requireEligibleChainMetaVoter returns nil only when signerValAddr is present +// in the current eligible-voter set, i.e. it satisfies exactly the same +// predicate uvalidator applies when it snapshots a ballot's voters. Reusing +// GetEligibleVoters rather than re-deriving the checks keeps ChainMeta vote +// admission from drifting away from that definition. +func (ms msgServer) requireEligibleChainMetaVoter(ctx context.Context, signerValAddr sdk.ValAddress) error { + eligible, err := ms.k.uvalidatorKeeper.GetEligibleVoters(ctx) + if err != nil { + return errors.Wrapf(err, "failed to fetch eligible voters for signer %s", signerValAddr.String()) + } + + want := signerValAddr.String() + for _, uv := range eligible { + if uv.IdentifyInfo != nil && uv.IdentifyInfo.CoreValidatorAddress == want { + return nil + } + } + + return fmt.Errorf( + "universal validator %s is not an eligible voter; only ACTIVE or PENDING_JOIN universal validators with bonded, non-tombstoned staking state may vote on chain meta", + want, + ) +} + // RevertStuckInbound is the admin escape hatch — see Keeper.RevertStuckInbound. func (ms msgServer) RevertStuckInbound(ctx context.Context, msg *types.MsgRevertStuckInbound) (*types.MsgRevertStuckInboundResponse, error) { ms.k.Logger().Info("msg: RevertStuckInbound", "signer", msg.Signer) @@ -212,3 +233,77 @@ func (ms msgServer) RevertStuckInbound(ctx context.Context, msg *types.MsgRevert OutboundId: outboundId, }, nil } + +// ExecuteStuckInbound is the admin escape hatch — see Keeper.ExecuteStuckInbound. +func (ms msgServer) ExecuteStuckInbound(ctx context.Context, msg *types.MsgExecuteStuckInbound) (*types.MsgExecuteStuckInboundResponse, error) { + ms.k.Logger().Info("msg: ExecuteStuckInbound", "signer", msg.Signer) + + admin, err := ms.k.uvalidatorKeeper.GetAdmin(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to read uvalidator admin") + } + if admin != msg.Signer { + return nil, errors.Wrapf(govtypes.ErrInvalidSigner, "invalid admin; expected %s, got %s", admin, msg.Signer) + } + + if msg.Inbound == nil { + return nil, errors.Wrap(sdkErrors.ErrInvalidRequest, "inbound is required") + } + + utxId, err := ms.k.ExecuteStuckInbound(ctx, *msg.Inbound) + if err != nil { + return nil, err + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx.EventManager().EmitEvent(sdk.NewEvent( + "inbound_executed_by_admin", + sdk.NewAttribute("admin", msg.Signer), + sdk.NewAttribute("utx_id", utxId), + sdk.NewAttribute("source_chain", msg.Inbound.SourceChain), + sdk.NewAttribute("amount", msg.Inbound.Amount), + )) + + return &types.MsgExecuteStuckInboundResponse{ + UtxId: utxId, + }, nil +} + +// ExecuteStuckOutbound is the admin escape hatch — see Keeper.ExecuteStuckOutbound. +func (ms msgServer) ExecuteStuckOutbound(ctx context.Context, msg *types.MsgExecuteStuckOutbound) (*types.MsgExecuteStuckOutboundResponse, error) { + ms.k.Logger().Info("msg: ExecuteStuckOutbound", "signer", msg.Signer) + + admin, err := ms.k.uvalidatorKeeper.GetAdmin(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to read uvalidator admin") + } + if admin != msg.Signer { + return nil, errors.Wrapf(govtypes.ErrInvalidSigner, "invalid admin; expected %s, got %s", admin, msg.Signer) + } + + if msg.ObservedTx == nil { + return nil, errors.Wrap(sdkErrors.ErrInvalidRequest, "observed_tx is required") + } + + // Normalize IDs: strip 0x prefix, as VoteOutbound does. + utxId := strings.TrimPrefix(msg.UtxId, "0x") + outboundId := strings.TrimPrefix(msg.TxId, "0x") + + settledId, err := ms.k.ExecuteStuckOutbound(ctx, utxId, outboundId, *msg.ObservedTx) + if err != nil { + return nil, err + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx.EventManager().EmitEvent(sdk.NewEvent( + "outbound_executed_by_admin", + sdk.NewAttribute("admin", msg.Signer), + sdk.NewAttribute("utx_id", utxId), + sdk.NewAttribute("outbound_id", settledId), + sdk.NewAttribute("success", fmt.Sprintf("%t", msg.ObservedTx.Success)), + )) + + return &types.MsgExecuteStuckOutboundResponse{ + OutboundId: settledId, + }, nil +} diff --git a/x/uexecutor/keeper/msg_server_test.go b/x/uexecutor/keeper/msg_server_test.go index 4465e1e42..c69634f1f 100755 --- a/x/uexecutor/keeper/msg_server_test.go +++ b/x/uexecutor/keeper/msg_server_test.go @@ -188,80 +188,3 @@ func TestMsgServer_ExecutePayload(t *testing.T) { }) } - -func TestMsgServer_MigrateUEA(t *testing.T) { - f := SetupTest(t) - - validSigner := f.addrs[0] - validUA := &types.UniversalAccountId{ - ChainNamespace: "eip155", - ChainId: "11155111", - Owner: "0x000000000000000000000000000000000000dead", - } - validMP := &types.MigrationPayload{ - Migration: "0x1234567890abcdef1234567890abcdef12345670", - Nonce: "1", - Deadline: "some-deadline", - } - - t.Run("fail; invalid signer address", func(t *testing.T) { - msg := &types.MsgMigrateUEA{ - Signer: "invalid_address", - UniversalAccountId: validUA, - MigrationPayload: validMP, - Signature: "0x", - } - - _, err := f.msgServer.MigrateUEA(f.ctx, msg) - require.ErrorContains(t, err, "failed to parse signer address") - }) - - t.Run("Fail : ChainConfig for Universal Accout not set", func(t *testing.T) { - // You can inject failure in f.app or f.k.utvKeeper if mockable - msg := &types.MsgMigrateUEA{ - Signer: validSigner.String(), - UniversalAccountId: validUA, - MigrationPayload: validMP, - Signature: "0x", - } - - f.mockUregistryKeeper.EXPECT().GetChainConfig(gomock.Any(), "eip155:11155111").Return(uregistrytypes.ChainConfig{}, errors.New("failed to get chain config for chain eip155:11155111")) - - _, err := f.msgServer.MigrateUEA(f.ctx, msg) - require.ErrorContains(t, err, "failed to get chain config") - }) - - t.Run("Fail: CallFactoryToComputeUEAAddress", func(t *testing.T) { - // You can inject failure in f.app or f.k.utvKeeper if mockable - msg := &types.MsgMigrateUEA{ - Signer: validSigner.String(), - UniversalAccountId: validUA, - MigrationPayload: validMP, - Signature: "0x", - } - - chainConfigTest := uregistrytypes.ChainConfig{ - Chain: "eip155:11155111", - VmType: uregistrytypes.VmType_EVM, // replace with appropriate VM_TYPE enum value - PublicRpcUrl: "https://mainnet.infura.io/v3/YOUR_PROJECT_ID", - GatewayAddress: "0x1234567890abcdef1234567890abcdef12345678", - BlockConfirmation: &uregistrytypes.BlockConfirmation{ - FastInbound: 3, - StandardInbound: 10, - }, - GatewayMethods: []*uregistrytypes.GatewayMethods{}, - Enabled: &uregistrytypes.ChainEnabled{ - IsInboundEnabled: true, - IsOutboundEnabled: true, - }, - } - - f.mockUregistryKeeper.EXPECT().GetChainConfig(gomock.Any(), "eip155:11155111").Return(chainConfigTest, nil) - - f.mockEVMKeeper.EXPECT().NewStateDB(gomock.Any()).Return(nil).AnyTimes() - f.mockEVMKeeper.EXPECT().CallEVM(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("CallFactoryToComputeUEAAddress Failed")).AnyTimes() - - _, err := f.msgServer.MigrateUEA(f.ctx, msg) - require.ErrorContains(t, err, "CallFactoryToComputeUEAAddress Failed") - }) -} diff --git a/x/uexecutor/keeper/msg_update_params.go b/x/uexecutor/keeper/msg_update_params.go index f071aeb49..92873a282 100644 --- a/x/uexecutor/keeper/msg_update_params.go +++ b/x/uexecutor/keeper/msg_update_params.go @@ -6,8 +6,17 @@ import ( "github.com/pushchain/push-chain-node/x/uexecutor/types" ) +// GetParams returns the current module parameters. +func (k Keeper) GetParams(ctx context.Context) (types.Params, error) { + return k.Params.Get(ctx) +} + // updateParams is for updating params collections of the module func (k Keeper) UpdateParams(ctx context.Context, params types.Params) error { + if err := params.ValidateBasic(); err != nil { + return err + } + oldParams, err := k.Params.Get(ctx) if err == nil { k.Logger().Info("params updated", diff --git a/x/uexecutor/keeper/msg_vote_inbound.go b/x/uexecutor/keeper/msg_vote_inbound.go index a20a2651d..ea1a012c8 100644 --- a/x/uexecutor/keeper/msg_vote_inbound.go +++ b/x/uexecutor/keeper/msg_vote_inbound.go @@ -16,6 +16,15 @@ import ( // query what happened to their cross-chain tx instead of having funds silently stuck // in the gateway contract. func (k Keeper) VoteInbound(ctx context.Context, universalValidator sdk.ValAddress, inbound types.Inbound) error { + // Bound the payload blobs before anything reads or writes state. The msg + // carrying this vote is fee exempt, so nothing charges the submitter for the + // bytes it puts into state. Repeated here rather than left to + // MsgVoteInbound.ValidateBasic so the cap holds for every caller of this + // keeper method, not just the one msg route. + if err := inbound.ValidateSize(); err != nil { + return err + } + // Canonicalize first so every derived key + the stored inbound use one // representation per logical event. inbound.Canonicalize() @@ -84,6 +93,19 @@ func (k Keeper) VoteInbound(ctx context.Context, universalValidator sdk.ValAddre } // --- Ballot finalized: always create UTX from here on --- + return k.finalizeInboundAndExecute(ctx, inbound, universalTxKey) +} + +// finalizeInboundAndExecute runs the post-finalization pipeline for an inbound +// whose ballot has reached PASSED: normalize, create the UniversalTx, drop the +// pending entry, then validate and execute. +// +// Shared by the normal vote path (VoteInbound) and the admin escape hatch +// (ExecuteStuckInbound) so a finalized inbound resolves identically whichever +// route finalized its ballot. +func (k Keeper) finalizeInboundAndExecute(ctx context.Context, inbound types.Inbound, universalTxKey string) error { + sdkCtx := sdk.UnwrapSDKContext(ctx) + k.Logger().Info("inbound ballot finalized, creating utx", "utx_key", universalTxKey, "source_chain", inbound.SourceChain) // Normalize inbound after finalization: strip irrelevant fields, decode raw_payload. diff --git a/x/uexecutor/keeper/msg_vote_outbound.go b/x/uexecutor/keeper/msg_vote_outbound.go index ae9a5b726..adb7a0429 100644 --- a/x/uexecutor/keeper/msg_vote_outbound.go +++ b/x/uexecutor/keeper/msg_vote_outbound.go @@ -107,6 +107,23 @@ func (k Keeper) VoteOutbound( return nil } + return k.finalizeOutboundAndSettle(ctx, utxId, outboundId, outbound, observedTx) +} + +// finalizeOutboundAndSettle runs the post-finalization pipeline for an outbound +// whose ballot has reached a terminal-and-settled state: record the observation, +// drop the pending entry, then settle (refund if the observation failed). +// +// Shared by the normal vote path (VoteOutbound) and the admin escape hatch +// (ExecuteStuckOutbound) so a settled outbound resolves identically whichever +// route finalized its ballot. +func (k Keeper) finalizeOutboundAndSettle( + ctx context.Context, + utxId string, + outboundId string, + outbound types.OutboundTx, + observedTx types.OutboundObservation, +) error { // Step 5: Update outbound state to OBSERVED outbound.OutboundStatus = types.Status_OBSERVED outbound.ObservedTx = &observedTx diff --git a/x/uexecutor/keeper/query_keys.go b/x/uexecutor/keeper/query_keys.go index 503c22678..0708baadd 100644 --- a/x/uexecutor/keeper/query_keys.go +++ b/x/uexecutor/keeper/query_keys.go @@ -12,6 +12,10 @@ import ( "github.com/pushchain/push-chain-node/x/uexecutor/types" ) +// maxQueryTxHashLen bounds the tx_hash accepted by the unauthenticated key +// derivation queries. Longest real value is an 88-char base58 Solana signature. +const maxQueryTxHashLen = 128 + // InboundKeys derives the canonical UTX id and inbound ballot id for the given // inbound, applying the same canonicalization the vote path uses. Lets off-chain // validators read the keys from the chain instead of re-implementing the rules. @@ -19,6 +23,18 @@ func (k Querier) InboundKeys(goCtx context.Context, req *types.QueryInboundKeysR if req == nil || req.Inbound == nil { return nil, status.Error(codes.InvalidArgument, "inbound is required") } + // This endpoint is unauthenticated, reads no state and so consumes no gas. + // Bound the one field that drives a decode (tx_hash) rather than trusting + // the caller. The limit is far above any real hash — 88 chars for a base58 + // Solana signature, 66 for 0x-prefixed EVM — so it rejects only garbage. + // Deliberately not applied to raw_payload / verification_data, which are + // legitimately long, nor pushed down into utils.Canonicalize*: the vote + // path must stay lenient (a malformed inbound still has to produce a UTX), + // and changing shared canonicalization would alter ballot keys. + if n := len(req.Inbound.TxHash); n > maxQueryTxHashLen { + return nil, status.Errorf(codes.InvalidArgument, + "tx_hash too long: %d chars (max %d)", n, maxQueryTxHashLen) + } inbound := *req.Inbound inbound.Canonicalize() diff --git a/x/uexecutor/keeper/query_keys_test.go b/x/uexecutor/keeper/query_keys_test.go new file mode 100644 index 000000000..51bbf64ce --- /dev/null +++ b/x/uexecutor/keeper/query_keys_test.go @@ -0,0 +1,87 @@ +package keeper_test + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// solanaSig is a real 88-char base58 Solana signature (64 bytes). +const solanaSig = "5j7s6NiJS3JAkvgkoc18WVAsiSaci2pxB2A6ueCJP4tprA2TFg9wSyTLeYouxPBJEMzJinENTkpA52YStRW5Dia7" + +func TestInboundKeys_RejectsOversizedTxHash(t *testing.T) { + // F-2026-18821: InboundKeys is unauthenticated, reads no state and so burns + // no gas. It canonicalizes tx_hash three times (Canonicalize, then the UTX + // and ballot key helpers), and base58 decoding is quadratic — a 1e5-char + // hash cost tens of seconds of CPU per request before the fix. + f := SetupTest(t) + + huge := strings.Repeat("z", 100_000) + + start := time.Now() + _, err := f.queryServer.InboundKeys(f.ctx, &types.QueryInboundKeysRequest{ + Inbound: &types.Inbound{ + SourceChain: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + TxHash: huge, + LogIndex: "0", + TxType: types.TxType_FUNDS, + }, + }) + elapsed := time.Since(start) + + require.Error(t, err) + require.Equal(t, codes.InvalidArgument, status.Code(err)) + require.Contains(t, err.Error(), "tx_hash too long") + require.Less(t, elapsed, time.Second, "oversized tx_hash must fail fast (took %s)", elapsed) +} + +func TestInboundKeys_AcceptsRealSolanaSignature(t *testing.T) { + // The cap must not reject anything real: 88 chars is the longest a base58 + // 64-byte signature can be. + f := SetupTest(t) + + resp, err := f.queryServer.InboundKeys(f.ctx, &types.QueryInboundKeysRequest{ + Inbound: &types.Inbound{ + SourceChain: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + TxHash: solanaSig, + LogIndex: "0", + TxType: types.TxType_FUNDS, + }, + }) + + require.NoError(t, err) + require.NotEmpty(t, resp.UtxId) + require.NotEmpty(t, resp.BallotId) + // Canonicalization folds the base58 signature into 0x-hex. + require.Equal(t, "0x", resp.CanonicalInbound.TxHash[:2]) + require.Len(t, resp.CanonicalInbound.TxHash, 2+128) +} + +func TestInboundKeys_TxHashAtCapIsAccepted(t *testing.T) { + // Boundary: exactly maxQueryTxHashLen (128) is allowed, 129 is not. + f := SetupTest(t) + + newReq := func(n int) *types.QueryInboundKeysRequest { + return &types.QueryInboundKeysRequest{ + Inbound: &types.Inbound{ + SourceChain: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + TxHash: strings.Repeat("z", n), + LogIndex: "0", + TxType: types.TxType_FUNDS, + }, + } + } + + _, err := f.queryServer.InboundKeys(f.ctx, newReq(128)) + require.NoError(t, err, "128-char tx_hash is at the cap and must be accepted") + + _, err = f.queryServer.InboundKeys(f.ctx, newReq(129)) + require.Error(t, err) + require.Equal(t, codes.InvalidArgument, status.Code(err)) +} diff --git a/x/uexecutor/mocks/mock_evmkeeper.go b/x/uexecutor/mocks/mock_evmkeeper.go index f5cd1d64f..ba1aa8333 100644 --- a/x/uexecutor/mocks/mock_evmkeeper.go +++ b/x/uexecutor/mocks/mock_evmkeeper.go @@ -87,6 +87,20 @@ func (mr *MockEVMKeeperMockRecorder) GetCodeHash(ctx, addr interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCodeHash", reflect.TypeOf((*MockEVMKeeper)(nil).GetCodeHash), ctx, addr) } +// GetNonce mocks base method. +func (m *MockEVMKeeper) GetNonce(ctx types.Context, addr common.Address) uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNonce", ctx, addr) + ret0, _ := ret[0].(uint64) + return ret0 +} + +// GetNonce indicates an expected call of GetNonce. +func (mr *MockEVMKeeperMockRecorder) GetNonce(ctx, addr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNonce", reflect.TypeOf((*MockEVMKeeper)(nil).GetNonce), ctx, addr) +} + // DerivedEVMCall mocks base method. func (m *MockEVMKeeper) DerivedEVMCall(ctx types.Context, abi abi.ABI, from, contract common.Address, value, gasLimit *big.Int, commit, gasless, isModuleSender bool, manualNonce *uint64, method string, args ...interface{}) (*types0.MsgEthereumTxResponse, error) { m.ctrl.T.Helper() diff --git a/x/uexecutor/types/abi.go b/x/uexecutor/types/abi.go index 836ea5f64..abe16dff3 100644 --- a/x/uexecutor/types/abi.go +++ b/x/uexecutor/types/abi.go @@ -4,8 +4,6 @@ import ( "math/big" "strings" - "errors" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/pushchain/push-chain-node/utils" @@ -978,23 +976,6 @@ func NewAbiUniversalPayload(proto *UniversalPayload) (AbiUniversalPayload, error }, nil } -type AbiMigrationPayload struct { - Migration common.Address - Nonce *big.Int - Deadline *big.Int -} - -func NewAbiMigrationPayload(proto *MigrationPayload) (AbiMigrationPayload, error) { - if proto.Migration == "" { - return AbiMigrationPayload{}, errors.New("invalid migration payload") - } - return AbiMigrationPayload{ - Migration: common.HexToAddress(proto.Migration), - Nonce: utils.StringToBigInt(proto.Nonce), - Deadline: utils.StringToBigInt(proto.Deadline), - }, nil -} - type AbiUniversalAccountId struct { ChainNamespace string ChainId string diff --git a/x/uexecutor/types/constants.go b/x/uexecutor/types/constants.go index 0d2fbcd30..abef034a2 100644 --- a/x/uexecutor/types/constants.go +++ b/x/uexecutor/types/constants.go @@ -56,6 +56,27 @@ var RescueFundsOnSourceChainEventSig = crypto.Keccak256Hash([]byte( "RescueFundsOnSourceChain(bytes32,address,string,address,uint8,uint256,uint256,uint256)", )).Hex() +// MaxUniversalPayloadBytes hard-caps the size of a universal payload, and of the +// hex blobs that carry one on the wire, at 128 KiB — the same limit the +// usigverifier precompile applies to a raw ed25519 message, so there is one +// payload size limit to reason about. +// +// A flat size cap rather than a gas price on purpose: MsgExecutePayload and +// MsgVoteInbound are fee exempt (app/txpolicy/gasless.go), so nothing charges +// the submitter for the bytes it puts into a block, into consensus state and +// into every node's memory. On a fee-exempt path the only defence that holds is +// a hard limit. It is a flat number rather than one derived from the Solidity +// UniversalPayload struct because that struct is variable length; a flat number +// is auditable and stable. +const MaxUniversalPayloadBytes = 128 * 1024 + +// MaxOutboundPayloadBytes caps the hex-encoded payload an outbound carries to a +// destination chain, so ~64 KiB of calldata. Bounded by what the destination can +// accept: geth's txpool rejects transactions over 128 KB (txMaxSize), so a larger +// payload yields a tx no EVM node accepts — unsendable once TSS has signed it. +// Same value as MaxUniversalPayloadBytes, different reason; do not collapse them. +const MaxOutboundPayloadBytes = 128 * 1024 + // PC20Selector and PRC20Selector are the 4-byte magic selectors the gateway // prepends to a payload so the chain can route PC20 vs PRC20 without a new event // or TxType. Values are the 0x-stripped, lower-hex ASCII encodings and MUST stay diff --git a/x/uexecutor/types/expected_keepers.go b/x/uexecutor/types/expected_keepers.go index e439713e1..526097b17 100644 --- a/x/uexecutor/types/expected_keepers.go +++ b/x/uexecutor/types/expected_keepers.go @@ -57,6 +57,8 @@ type EVMKeeper interface { args ...interface{}, ) (*types.MsgEthereumTxResponse, error) GetCodeHash(ctx sdk.Context, addr common.Address) common.Hash + // GetNonce returns the account nonce (auth sequence) the EVM sees for addr. + GetNonce(ctx sdk.Context, addr common.Address) uint64 } // FeeMarketKeeper defines the expected interface for the fee market module. @@ -98,6 +100,9 @@ type BankKeeper interface { // AccountKeeper defines the expected interface for the auth module type AccountKeeper interface { GetModuleAccount(ctx context.Context, moduleName string) sdk.ModuleAccountI + // SetAccount persists an account. Used to keep the uexecutor module + // account's EVM nonce in step with the nonce handed to DerivedEVMCall. + SetAccount(ctx context.Context, acc sdk.AccountI) } type UValidatorKeeper interface { @@ -120,6 +125,10 @@ type UValidatorKeeper interface { GetEligibleVoters(ctx context.Context) ([]uvalidatortypes.UniversalValidator, error) GetBallot(ctx context.Context, id string) (uvalidatortypes.Ballot, error) GetAdmin(ctx context.Context) (string, error) + // MarkBallotFinalized drives a ballot to PASSED/REJECTED. Needed by the + // ExecuteStuckInbound escape hatch, which finalizes a ballot the vote flow + // can no longer finalize on its own. + MarkBallotFinalized(ctx context.Context, id string, status uvalidatortypes.BallotStatus) error } // UCallbackKeeper ingests read requests from derived-call receipts, which the diff --git a/x/uexecutor/types/genesis_test.go b/x/uexecutor/types/genesis_test.go index bdfc922a1..9c478f107 100755 --- a/x/uexecutor/types/genesis_test.go +++ b/x/uexecutor/types/genesis_test.go @@ -20,9 +20,12 @@ func TestGenesisState_Validate(t *testing.T) { valid: true, }, { - desc: "valid genesis state", + // An empty Params leaves max_gasless_tx_gas at 0, which would + // reject every gasless tx and stop the universal validators from + // voting. Fail at genesis rather than silently. + desc: "empty params are rejected", genState: &types.GenesisState{}, - valid: true, + valid: false, }, } for _, tc := range tests { diff --git a/x/uexecutor/types/inbound.go b/x/uexecutor/types/inbound.go index 2a8eca6e4..a52f471b8 100644 --- a/x/uexecutor/types/inbound.go +++ b/x/uexecutor/types/inbound.go @@ -3,7 +3,6 @@ package types import ( "encoding/json" "fmt" - "math/big" "strings" "cosmossdk.io/errors" @@ -84,6 +83,29 @@ func (p *Inbound) NormalizeForTxType() error { return nil } +// ValidateSize enforces MaxUniversalPayloadBytes on every variable-length +// payload field an inbound carries. raw_payload is the wire form of the +// universal payload and universal_payload is what a validator submits before +// the core decodes raw_payload itself; both land in PendingInbounds state on +// the first vote, on a fee-exempt msg, so both are bounded here. +// +// Split out of ValidateBasic so the keeper can apply the cap on its own: a +// universal validator submits votes wrapped in authz.MsgExec +// (universalClient/pushsigner/pushsigner.go wrapWithAuthZ), which baseapp does +// not validate at CheckTx, so the cap must not depend on one call site. +func (p *Inbound) ValidateSize() error { + if p == nil { + return nil + } + if err := ValidatePayloadBlobSize("raw_payload", p.RawPayload); err != nil { + return err + } + if err := ValidatePayloadBlobSize("verification_data", p.VerificationData); err != nil { + return err + } + return p.UniversalPayload.ValidateSize() +} + // Stringer method for Params. func (p Inbound) String() string { bz, err := json.Marshal(p) @@ -101,6 +123,13 @@ func (p Inbound) String() string { // (with a failed PCTx / revert) instead of silently dropping the vote and leaving // user funds stuck in the gateway. func (p Inbound) ValidateBasic() error { + // Reject oversized payload blobs before anything else: unlike the + // execution-level checks below, this one is a resource bound, and the bytes + // are already in the block by the time execution validation runs. + if err := p.ValidateSize(); err != nil { + return err + } + // Validate source_chain (must follow CAIP-2 format) — needed for UTX key chain := strings.TrimSpace(p.SourceChain) if chain == "" { @@ -141,9 +170,10 @@ func (p Inbound) ValidateForExecution() error { if strings.TrimSpace(p.Amount) == "" { return errors.Wrap(sdkerrors.ErrInvalidRequest, "amount cannot be empty") } - bi, ok := new(big.Int).SetString(p.Amount, 10) - if !ok || bi.Sign() < 0 { - return errors.Wrap(sdkerrors.ErrInvalidRequest, "amount must be a valid non-negative uint256") + // Length-capped, range-checked uint256 parse — see F-2026-18798. + bi, err := ValidateUint256String(p.Amount, "amount must be a valid non-negative uint256") + if err != nil { + return err } // Only GAS_AND_PAYLOAD and FUNDS_AND_PAYLOAD allow zero amount (skip deposit, still execute payload) if bi.Sign() == 0 && p.TxType != TxType_GAS_AND_PAYLOAD && p.TxType != TxType_FUNDS_AND_PAYLOAD { diff --git a/x/uexecutor/types/migration_payload.go b/x/uexecutor/types/migration_payload.go deleted file mode 100644 index bd5533f4c..000000000 --- a/x/uexecutor/types/migration_payload.go +++ /dev/null @@ -1,49 +0,0 @@ -package types - -import ( - "encoding/json" - "math/big" - "strings" - - "cosmossdk.io/errors" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/pushchain/push-chain-node/utils" -) - -// Stringer method for Params. -func (p MigrationPayload) String() string { - bz, err := json.Marshal(p) - if err != nil { - panic(err) - } - - return string(bz) -} - -// ValidateBasic does the sanity check on the UniversalPayload fields. -func (p MigrationPayload) ValidateBasic() error { - // Validate 'migration' address - if strings.TrimSpace(p.Migration) == "" { - return errors.Wrap(sdkerrors.ErrInvalidAddress, "migration address cannot be empty") - } - if !utils.IsValidAddress(p.Migration, utils.HEX) { - return errors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid migration contract address format: %s", p.Migration) - } - - // Validate all numeric string fields as uint256 - uintFields := map[string]string{ - "nonce": p.Nonce, - "deadline": p.Deadline, - } - - for fieldName, value := range uintFields { - if value != "" { - bi, ok := new(big.Int).SetString(value, 10) - if !ok || bi.Sign() < 0 { - return errors.Wrapf(sdkerrors.ErrInvalidRequest, "%s must be a valid unsigned integer", fieldName) - } - } - } - - return nil -} diff --git a/x/uexecutor/types/migration_payload_test.go b/x/uexecutor/types/migration_payload_test.go deleted file mode 100644 index fe33c71b2..000000000 --- a/x/uexecutor/types/migration_payload_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package types_test - -import ( - "testing" - - "github.com/pushchain/push-chain-node/x/uexecutor/types" - "github.com/stretchr/testify/require" -) - -func TestMsgMigrationPayload_ValidateBasic(t *testing.T) { - validSigner := "push1fgaewhyd9fkwtqaj9c233letwcuey6dgly9gv9" - invalidSigner := "invalid_bech32" - validUA := &types.UniversalAccountId{ - ChainNamespace: "eip155", - ChainId: "11155111", - Owner: "0x000000000000000000000000000000000000dead", - } - - validPayload := &types.MigrationPayload{ - Migration: "0x000000000000000000000000000000000000dead", - } - - invalidPayload := &types.MigrationPayload{ - Migration: "invalid_address", - } - - validSig := "abcdef0123456789" - // invalidSig := "zzzzzz" - - tests := []struct { - name string - msg *types.MsgMigrateUEA - expectErr bool - }{ - { - name: "valid msg", - msg: &types.MsgMigrateUEA{ - Signer: validSigner, - UniversalAccountId: validUA, - MigrationPayload: validPayload, - Signature: validSig, - }, - expectErr: false, - }, - { - name: "invalid signer", - msg: &types.MsgMigrateUEA{ - Signer: invalidSigner, - UniversalAccountId: validUA, - MigrationPayload: validPayload, - Signature: validSig, - }, - expectErr: true, - }, - { - name: "nil universal account", - msg: &types.MsgMigrateUEA{ - Signer: validSigner, - UniversalAccountId: nil, - MigrationPayload: validPayload, - Signature: validSig, - }, - expectErr: true, - }, - { - name: "nil universal payload", - msg: &types.MsgMigrateUEA{ - Signer: validSigner, - UniversalAccountId: validUA, - MigrationPayload: nil, - Signature: validSig, - }, - expectErr: true, - }, - { - name: "empty Signature", - msg: &types.MsgMigrateUEA{ - Signer: validSigner, - UniversalAccountId: validUA, - MigrationPayload: validPayload, - Signature: "", - }, - expectErr: true, - }, - { - name: "invalid universal payload data", - msg: &types.MsgMigrateUEA{ - Signer: validSigner, - UniversalAccountId: validUA, - MigrationPayload: invalidPayload, - Signature: validSig, - }, - expectErr: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - err := tc.msg.ValidateBasic() - if tc.expectErr { - require.Error(t, err, "expected error but got none") - } else { - require.NoError(t, err, "expected no error but got: %v", err) - } - }) - } -} diff --git a/x/uexecutor/types/msg_execute_payload.go b/x/uexecutor/types/msg_execute_payload.go index 656499f7d..e49936818 100644 --- a/x/uexecutor/types/msg_execute_payload.go +++ b/x/uexecutor/types/msg_execute_payload.go @@ -7,6 +7,7 @@ import ( "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" ) var ( @@ -47,10 +48,20 @@ func (msg *MsgExecutePayload) GetSigners() []sdk.AccAddress { // ValidateBasic does a sanity check on the provided data. func (msg *MsgExecutePayload) ValidateBasic() error { - // Validate signer - if _, err := sdk.AccAddressFromBech32(msg.Signer); err != nil { + // Validate signer. + // The length check is deliberate: bech32 account addresses may carry up to + // 255 bytes, and this signer is later converted to a 20-byte EVM address + // that keeps only the rightmost bytes. A longer signer would therefore + // collapse onto an unrelated EVM address, including module addresses that + // the UEA trusts. Reject it here, at CheckTx, before the ante chain runs. + signerBz, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { return errors.Wrap(err, "invalid signer address") } + if len(signerBz) != common.AddressLength { + return errors.Wrapf(sdkerrors.ErrInvalidAddress, + "invalid signer address length: got %d bytes, want %d", len(signerBz), common.AddressLength) + } // Validate universalAccountId if msg.UniversalAccountId == nil { @@ -66,6 +77,9 @@ func (msg *MsgExecutePayload) ValidateBasic() error { if len(msg.VerificationData) == 0 { return errors.Wrap(sdkerrors.ErrInvalidRequest, "verificationData cannot be empty") } + if err := ValidatePayloadBlobSize("verificationData", msg.VerificationData); err != nil { + return err + } if _, err := hex.DecodeString(strings.TrimPrefix(msg.VerificationData, "0x")); err != nil { return errors.Wrap(sdkerrors.ErrInvalidRequest, "invalid verificationData hex") } diff --git a/x/uexecutor/types/msg_execute_stuck_outbound.go b/x/uexecutor/types/msg_execute_stuck_outbound.go new file mode 100644 index 000000000..c981acf6e --- /dev/null +++ b/x/uexecutor/types/msg_execute_stuck_outbound.go @@ -0,0 +1,35 @@ +package types + +import ( + "strings" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +var ( + _ sdk.Msg = &MsgExecuteStuckOutbound{} +) + +// ValidateBasic mirrors MsgVoteOutbound: the admin must supply exactly the +// observation the validators voted on. +func (msg *MsgExecuteStuckOutbound) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.Signer); err != nil { + return errors.Wrap(err, "invalid signer address") + } + + if strings.TrimSpace(msg.TxId) == "" { + return errors.Wrap(sdkerrors.ErrInvalidRequest, "tx_id cannot be empty") + } + + if strings.TrimSpace(msg.UtxId) == "" { + return errors.Wrap(sdkerrors.ErrInvalidRequest, "utx_id cannot be empty") + } + + if msg.ObservedTx == nil { + return errors.Wrap(sdkerrors.ErrInvalidRequest, "observed_tx cannot be nil") + } + + return msg.ObservedTx.ValidateBasic() +} diff --git a/x/uexecutor/types/msg_execute_stuck_outbound_test.go b/x/uexecutor/types/msg_execute_stuck_outbound_test.go new file mode 100644 index 000000000..ef74c95ed --- /dev/null +++ b/x/uexecutor/types/msg_execute_stuck_outbound_test.go @@ -0,0 +1,130 @@ +package types_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +const stuckOutboundSigner = "push1fgaewhyd9fkwtqaj9c233letwcuey6dgly9gv9" + +func newMsgExecuteStuckOutbound(obs *types.OutboundObservation) *types.MsgExecuteStuckOutbound { + return &types.MsgExecuteStuckOutbound{ + Signer: stuckOutboundSigner, + TxId: "outbound-1", + UtxId: "utx-1", + ObservedTx: obs, + } +} + +func successObservation(gasFeeUsed string) *types.OutboundObservation { + return &types.OutboundObservation{ + Success: true, + TxHash: "0x" + strings.Repeat("ab", 32), + BlockHeight: 42, + GasFeeUsed: gasFeeUsed, + } +} + +// gas_fee_used feeds both the outbound ballot key and the refund arithmetic, so +// MsgExecuteStuckOutbound has to admit exactly what MsgVoteOutbound admits — +// including the length cap and uint256 range check from F-2026-18798. +func TestMsgExecuteStuckOutbound_ValidateBasic_GasFeeUsed(t *testing.T) { + cases := []struct { + name string + gasFeeUsed string + expectErr string + }{ + {name: "valid", gasFeeUsed: "1000"}, + {name: "zero is valid", gasFeeUsed: "0"}, + {name: "empty", gasFeeUsed: "", expectErr: "observed_tx.gas_fee_used is required"}, + {name: "non-numeric", gasFeeUsed: "not-a-number", expectErr: "observed_tx.gas_fee_used must be a valid uint256"}, + {name: "negative", gasFeeUsed: "-1", expectErr: "observed_tx.gas_fee_used must be a valid uint256"}, + {name: "over uint256 range", gasFeeUsed: strings.Repeat("9", 78), expectErr: "value exceeds the uint256 range"}, + {name: "over length cap", gasFeeUsed: strings.Repeat("1", 81), expectErr: "exceeds the maximum of 80 characters"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := newMsgExecuteStuckOutbound(successObservation(tc.gasFeeUsed)).ValidateBasic() + if tc.expectErr == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectErr) + }) + } +} + +func TestMsgExecuteStuckOutbound_ValidateBasic_RequiredFields(t *testing.T) { + require.NoError(t, newMsgExecuteStuckOutbound(successObservation("100")).ValidateBasic()) + + bad := newMsgExecuteStuckOutbound(successObservation("100")) + bad.Signer = "not-bech32" + require.ErrorContains(t, bad.ValidateBasic(), "invalid signer address") + + bad = newMsgExecuteStuckOutbound(successObservation("100")) + bad.TxId = " " + require.ErrorContains(t, bad.ValidateBasic(), "tx_id cannot be empty") + + bad = newMsgExecuteStuckOutbound(successObservation("100")) + bad.UtxId = "" + require.ErrorContains(t, bad.ValidateBasic(), "utx_id cannot be empty") + + require.ErrorContains(t, newMsgExecuteStuckOutbound(nil).ValidateBasic(), "observed_tx cannot be nil") + + // Success requires a tx hash and a block height. + bad = newMsgExecuteStuckOutbound(successObservation("100")) + bad.ObservedTx.TxHash = "" + require.ErrorContains(t, bad.ValidateBasic(), "observed_tx.tx_hash required when success=true") + + bad = newMsgExecuteStuckOutbound(successObservation("100")) + bad.ObservedTx.BlockHeight = 0 + require.ErrorContains(t, bad.ValidateBasic(), "observed_tx.block_height must be > 0 when success=true") + + // A failed observation may carry no tx hash — but if it does, it needs a height. + require.NoError(t, newMsgExecuteStuckOutbound(&types.OutboundObservation{ + Success: false, ErrorMsg: "reverted", GasFeeUsed: "100", + }).ValidateBasic()) + + require.ErrorContains(t, newMsgExecuteStuckOutbound(&types.OutboundObservation{ + Success: false, ErrorMsg: "reverted", GasFeeUsed: "100", TxHash: "0xdead", + }).ValidateBasic(), "observed_tx.block_height must be > 0 when tx_hash is provided") +} + +// The admin hatch and the validator vote path must admit the same observations: +// anything the vote path refuses can never have produced a ballot for the hatch +// to settle against. +func TestMsgExecuteStuckOutbound_MatchesVoteOutboundAdmission(t *testing.T) { + observations := []*types.OutboundObservation{ + successObservation("100"), + successObservation(""), + successObservation("not-a-number"), + successObservation(strings.Repeat("9", 78)), + {Success: false, ErrorMsg: "reverted", GasFeeUsed: "0"}, + {Success: false, GasFeeUsed: "1", TxHash: "0xdead"}, + {Success: true, GasFeeUsed: "1", BlockHeight: 0, TxHash: "0xdead"}, + } + + for i, obs := range observations { + voteMsg := &types.MsgVoteOutbound{ + Signer: stuckOutboundSigner, + TxId: "outbound-1", + UtxId: "utx-1", + ObservedTx: obs, + } + voteErr := voteMsg.ValidateBasic() + hatchErr := newMsgExecuteStuckOutbound(obs).ValidateBasic() + + if voteErr == nil { + require.NoError(t, hatchErr, "observation %d accepted by the vote path must be accepted by the hatch", i) + continue + } + require.Error(t, hatchErr, "observation %d refused by the vote path must be refused by the hatch", i) + require.Equal(t, voteErr.Error(), hatchErr.Error(), "observation %d must be refused for the same reason", i) + } +} diff --git a/x/uexecutor/types/msg_migrate_uea.go b/x/uexecutor/types/msg_migrate_uea.go deleted file mode 100644 index 45178c6a7..000000000 --- a/x/uexecutor/types/msg_migrate_uea.go +++ /dev/null @@ -1,78 +0,0 @@ -package types - -import ( - "cosmossdk.io/errors" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" -) - -var ( - _ sdk.Msg = &MsgMigrateUEA{} -) - -// NewMsgMigrateUEA creates new instance of MsgMigrateUEA -func NewMsgMigrateUEA( - sender sdk.Address, - universalAccountId *UniversalAccountId, - migrationPayload *MigrationPayload, - signature string, -) *MsgMigrateUEA { - return &MsgMigrateUEA{ - Signer: sender.String(), - UniversalAccountId: universalAccountId, - MigrationPayload: migrationPayload, - Signature: signature, - } -} - -// Route returns the name of the module -func (msg MsgMigrateUEA) Route() string { return ModuleName } - -// Type returns the action -func (msg MsgMigrateUEA) Type() string { return "migrate_uea" } - -// GetSignBytes implements the LegacyMsg interface. -func (msg MsgMigrateUEA) GetSignBytes() []byte { - return sdk.MustSortJSON(AminoCdc.MustMarshalJSON(&msg)) -} - -// GetSigners returns the expected signers for a MsgExecutePayload message. -func (msg *MsgMigrateUEA) GetSigners() []sdk.AccAddress { - addr, _ := sdk.AccAddressFromBech32(msg.Signer) - return []sdk.AccAddress{addr} -} - -// ValidateBasic does a sanity check on the provided data. -func (msg *MsgMigrateUEA) ValidateBasic() error { - // Validate signer - if _, err := sdk.AccAddressFromBech32(msg.Signer); err != nil { - return errors.Wrap(err, "invalid signer address") - } - - // Validate universalAccountId - if msg.UniversalAccountId == nil { - return errors.Wrap(sdkerrors.ErrInvalidRequest, "universal account cannot be nil") - } - - // Validate migration payload - if msg.MigrationPayload == nil { - return errors.Wrap(sdkerrors.ErrInvalidRequest, "migration payload cannot be nil") - } - - // Validate Signature - if len(msg.Signature) == 0 { - return errors.Wrap(sdkerrors.ErrInvalidRequest, "signature cannot be empty") - } - - // Validate universalAccountId structure - if err := msg.UniversalAccountId.ValidateBasic(); err != nil { - return errors.Wrap(err, "invalid universalAccountId") - } - - // Validate migration payload structure - if err := msg.MigrationPayload.ValidateBasic(); err != nil { - return errors.Wrap(err, "invalid migration payload") - } - - return nil -} diff --git a/x/uexecutor/types/msg_migrate_uea_test.go b/x/uexecutor/types/msg_migrate_uea_test.go deleted file mode 100644 index d566fd72f..000000000 --- a/x/uexecutor/types/msg_migrate_uea_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package types_test - -import ( - "testing" - - "github.com/pushchain/push-chain-node/x/uexecutor/types" - "github.com/stretchr/testify/require" -) - -func TestMsgMigrateUEA_ValidateBasic(t *testing.T) { - validSigner := "push1fgaewhyd9fkwtqaj9c233letwcuey6dgly9gv9" - - validUA := &types.UniversalAccountId{ - ChainNamespace: "eip155", - ChainId: "11155111", - Owner: "0x000000000000000000000000000000000000dead", - } - - validMigrationPayload := &types.MigrationPayload{ - Migration: "0x000000000000000000000000000000000000dead", - Nonce: "1", - Deadline: "9999999999", - } - - invalidMigrationPayload := &types.MigrationPayload{ - Migration: "bad_address", - Nonce: "1", - Deadline: "1", - } - - tests := []struct { - name string - msg *types.MsgMigrateUEA - expectErr bool - }{ - { - name: "valid msg", - msg: &types.MsgMigrateUEA{ - Signer: validSigner, - UniversalAccountId: validUA, - MigrationPayload: validMigrationPayload, - Signature: "0xabcdef", - }, - expectErr: false, - }, - { - name: "fails when migration payload validation fails (delegation)", - msg: &types.MsgMigrateUEA{ - Signer: validSigner, - UniversalAccountId: validUA, - MigrationPayload: invalidMigrationPayload, - Signature: "0xabcdef", - }, - expectErr: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - err := tc.msg.ValidateBasic() - if tc.expectErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/x/uexecutor/types/msg_signer_length_test.go b/x/uexecutor/types/msg_signer_length_test.go new file mode 100644 index 000000000..f81ca06b4 --- /dev/null +++ b/x/uexecutor/types/msg_signer_length_test.go @@ -0,0 +1,81 @@ +package types_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// uexecutorModuleEVMAddr is sha256("uexecutor")[:20] rendered as an EVM address. +// The UEA contract trusts calls from it unconditionally. +const uexecutorModuleEVMAddr = "0x14191Ea54B4c176fCf86f51b0FAc7CB1E71Df7d7" + +// aliasedModuleSigner returns a bech32 signer of the given byte length whose +// rightmost 20 bytes are the uexecutor module account, so that the downstream +// conversion to a 20-byte EVM address collapses onto the module itself. +func aliasedModuleSigner(t *testing.T, length int) string { + t.Helper() + moduleAddr := authtypes.NewModuleAddress(types.ModuleName) + require.Len(t, moduleAddr, common.AddressLength) + require.Equal(t, uexecutorModuleEVMAddr, common.BytesToAddress(moduleAddr).Hex()) + + prefix := make([]byte, length-common.AddressLength) + prefix[0] = 0x01 + addr := sdk.AccAddress(append(prefix, moduleAddr...)) + require.Equal(t, uexecutorModuleEVMAddr, common.BytesToAddress(addr).Hex()) + return addr.String() +} + +// TestGaslessMsgs_RejectOverlongSigner is the CheckTx-time guard for +// F-2026-18200: a gasless message must reject a signer that does not decode to +// exactly 20 bytes, before the ante chain ever runs. +func TestGaslessMsgs_RejectOverlongSigner(t *testing.T) { + validUA := &types.UniversalAccountId{ + ChainNamespace: "eip155", + ChainId: "11155111", + Owner: "0x000000000000000000000000000000000000dead", + } + + for _, length := range []int{21, 22, 32} { + signer := aliasedModuleSigner(t, length) + + execMsg := &types.MsgExecutePayload{ + Signer: signer, + UniversalAccountId: validUA, + UniversalPayload: &types.UniversalPayload{ + To: "0x000000000000000000000000000000000000dead", + Data: "0xabcdef", + }, + VerificationData: "abcdef", + } + err := execMsg.ValidateBasic() + require.Error(t, err, "MsgExecutePayload must reject a %d-byte signer", length) + require.Contains(t, err.Error(), "invalid signer address length") + } +} + +// TestGaslessMsgs_Accept20ByteSigner is the positive control. +func TestGaslessMsgs_Accept20ByteSigner(t *testing.T) { + signer := sdk.AccAddress(make([]byte, common.AddressLength)).String() + validUA := &types.UniversalAccountId{ + ChainNamespace: "eip155", + ChainId: "11155111", + Owner: "0x000000000000000000000000000000000000dead", + } + + execMsg := &types.MsgExecutePayload{ + Signer: signer, + UniversalAccountId: validUA, + UniversalPayload: &types.UniversalPayload{ + To: "0x000000000000000000000000000000000000dead", + Data: "0xabcdef", + }, + VerificationData: "abcdef", + } + require.NoError(t, execMsg.ValidateBasic()) +} diff --git a/x/uexecutor/types/msg_vote_chain_meta.go b/x/uexecutor/types/msg_vote_chain_meta.go index f4052aa6a..fc3dec952 100644 --- a/x/uexecutor/types/msg_vote_chain_meta.go +++ b/x/uexecutor/types/msg_vote_chain_meta.go @@ -10,6 +10,16 @@ var ( _ sdk.Msg = &MsgVoteChainMeta{} ) +// MaxObservedChainIdLen caps the CAIP-2 chain id carried by a chain-meta vote. +// +// F-2026-18803: the id is used verbatim as the ChainMetas map key +// (collections.StringKey), so an uncapped id is an attacker-controlled IAVL key +// of arbitrary size. CAIP-2 itself allows at most 8 (namespace) + 1 + 32 +// (reference) = 41 characters, and the longest id we actually register is +// "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" (41). 128 leaves generous headroom +// for future namespaces while keeping the key bounded. +const MaxObservedChainIdLen = 128 + // NewMsgVoteChainMeta creates new instance of MsgVoteChainMeta func NewMsgVoteChainMeta( sender sdk.Address, @@ -49,6 +59,19 @@ func (msg *MsgVoteChainMeta) ValidateBasic() error { if msg.ObservedChainId == "" { return errors.Wrap(sdkerrors.ErrInvalidRequest, "observed_chain_id cannot be empty") } + // F-2026-18803 (stateless half): ValidateBasic has no keeper, so it cannot + // ask whether the chain is registered — Keeper.VoteChainMeta does that. What + // it can do for free at CheckTx time is bound the id's size and shape, so an + // absurd id is dropped at mempool admission rather than after a block + // commits it as a ChainMetas key. + if len(msg.ObservedChainId) > MaxObservedChainIdLen { + return errors.Wrapf(sdkerrors.ErrInvalidRequest, + "observed_chain_id exceeds %d characters (got %d)", MaxObservedChainIdLen, len(msg.ObservedChainId)) + } + if _, _, err := ParseCAIP2(msg.ObservedChainId); err != nil { + return errors.Wrap(sdkerrors.ErrInvalidRequest, + "observed_chain_id must be in CAIP-2 format :") + } if msg.Price == 0 { return errors.Wrap(sdkerrors.ErrInvalidRequest, "price must be greater than 0") } diff --git a/x/uexecutor/types/msg_vote_chain_meta_test.go b/x/uexecutor/types/msg_vote_chain_meta_test.go new file mode 100644 index 000000000..8c234c346 --- /dev/null +++ b/x/uexecutor/types/msg_vote_chain_meta_test.go @@ -0,0 +1,101 @@ +package types_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// F-2026-18803 (stateless half): observed_chain_id becomes the ChainMetas map +// key verbatim, so ValidateBasic bounds its size and shape at CheckTx time. +func TestMsgVoteChainMeta_ValidateBasic(t *testing.T) { + const validSigner = "push1fgaewhyd9fkwtqaj9c233letwcuey6dgly9gv9" + + newMsg := func(chainID string) *types.MsgVoteChainMeta { + return &types.MsgVoteChainMeta{ + Signer: validSigner, + ObservedChainId: chainID, + Price: 100_000_000_000, + ChainHeight: 12345, + } + } + + tests := []struct { + name string + msg *types.MsgVoteChainMeta + expectErr string + }{ + { + name: "valid evm chain id", + msg: newMsg("eip155:11155111"), + }, + { + name: "valid solana chain id", + msg: newMsg("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"), + }, + { + name: "chain id exactly at the cap is accepted", + msg: newMsg("eip155:" + strings.Repeat("9", types.MaxObservedChainIdLen-len("eip155:"))), + }, + { + name: "chain id one byte over the cap is rejected", + msg: newMsg("eip155:" + strings.Repeat("9", types.MaxObservedChainIdLen-len("eip155:")+1)), + expectErr: "exceeds 128 characters", + }, + { + name: "oversized chain id is rejected", + msg: newMsg("eip155:" + strings.Repeat("9", 100_000)), + expectErr: "exceeds 128 characters", + }, + { + name: "non-CAIP-2 chain id is rejected", + msg: newMsg("ethereum"), + expectErr: "CAIP-2 format", + }, + { + name: "empty namespace is rejected", + msg: newMsg(":11155111"), + expectErr: "CAIP-2 format", + }, + { + name: "empty reference is rejected", + msg: newMsg("eip155:"), + expectErr: "CAIP-2 format", + }, + { + name: "empty chain id is rejected", + msg: newMsg(""), + expectErr: "observed_chain_id cannot be empty", + }, + { + name: "invalid signer is rejected", + msg: &types.MsgVoteChainMeta{Signer: "not-bech32", ObservedChainId: "eip155:1", Price: 1, ChainHeight: 1}, + expectErr: "invalid signer address", + }, + { + name: "zero price is rejected", + msg: &types.MsgVoteChainMeta{Signer: validSigner, ObservedChainId: "eip155:1", Price: 0, ChainHeight: 1}, + expectErr: "price must be greater than 0", + }, + { + name: "zero chain height is rejected", + msg: &types.MsgVoteChainMeta{Signer: validSigner, ObservedChainId: "eip155:1", Price: 1, ChainHeight: 0}, + expectErr: "chain_height must be greater than 0", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.expectErr == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectErr) + }) + } +} diff --git a/x/uexecutor/types/msg_vote_outbound.go b/x/uexecutor/types/msg_vote_outbound.go index 8b1c8d9a7..6056722bd 100644 --- a/x/uexecutor/types/msg_vote_outbound.go +++ b/x/uexecutor/types/msg_vote_outbound.go @@ -66,33 +66,5 @@ func (msg *MsgVoteOutbound) ValidateBasic() error { } // Validate observed_tx content - obs := msg.ObservedTx - - // gas_fee_used is always required — the external chain consumes gas regardless - // of success or failure, and excess gas must be refundable in both cases. - if strings.TrimSpace(obs.GasFeeUsed) == "" { - return errors.Wrap(sdkerrors.ErrInvalidRequest, - "observed_tx.gas_fee_used is required") - } - - if obs.Success { - // Success additionally requires tx_hash and block_height. - if strings.TrimSpace(obs.TxHash) == "" { - return errors.Wrap(sdkerrors.ErrInvalidRequest, - "observed_tx.tx_hash required when success=true") - } - if obs.BlockHeight == 0 { - return errors.Wrap(sdkerrors.ErrInvalidRequest, - "observed_tx.block_height must be > 0 when success=true") - } - } else { - // Failure case: tx_hash MAY be empty. - // BUT if tx_hash is present, block_height must be > 0. - if strings.TrimSpace(obs.TxHash) != "" && obs.BlockHeight == 0 { - return errors.Wrap(sdkerrors.ErrInvalidRequest, - "observed_tx.block_height must be > 0 when tx_hash is provided") - } - } - - return nil + return msg.ObservedTx.ValidateBasic() } diff --git a/x/uexecutor/types/outbound_observation.go b/x/uexecutor/types/outbound_observation.go new file mode 100644 index 000000000..d987737f6 --- /dev/null +++ b/x/uexecutor/types/outbound_observation.go @@ -0,0 +1,45 @@ +package types + +import ( + "strings" + + "cosmossdk.io/errors" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// ValidateBasic sanity-checks a destination-chain observation. Shared by +// MsgVoteOutbound and MsgExecuteStuckOutbound so the two cannot drift. +func (obs *OutboundObservation) ValidateBasic() error { + // gas_fee_used is always required — the external chain consumes gas regardless + // of success or failure, and excess gas must be refundable in both cases. + if strings.TrimSpace(obs.GasFeeUsed) == "" { + return errors.Wrap(sdkerrors.ErrInvalidRequest, + "observed_tx.gas_fee_used is required") + } + // Length-capped, range-checked uint256 parse — see F-2026-18798. The value + // also feeds the outbound ballot key, so a malformed one must never be voted. + if _, err := ValidateUint256String(obs.GasFeeUsed, "observed_tx.gas_fee_used must be a valid uint256"); err != nil { + return err + } + + if obs.Success { + // Success additionally requires tx_hash and block_height. + if strings.TrimSpace(obs.TxHash) == "" { + return errors.Wrap(sdkerrors.ErrInvalidRequest, + "observed_tx.tx_hash required when success=true") + } + if obs.BlockHeight == 0 { + return errors.Wrap(sdkerrors.ErrInvalidRequest, + "observed_tx.block_height must be > 0 when success=true") + } + } else { + // Failure case: tx_hash MAY be empty. + // BUT if tx_hash is present, block_height must be > 0. + if strings.TrimSpace(obs.TxHash) != "" && obs.BlockHeight == 0 { + return errors.Wrap(sdkerrors.ErrInvalidRequest, + "observed_tx.block_height must be > 0 when tx_hash is provided") + } + } + + return nil +} diff --git a/x/uexecutor/types/outbound_payload_size_test.go b/x/uexecutor/types/outbound_payload_size_test.go new file mode 100644 index 000000000..f77f48826 --- /dev/null +++ b/x/uexecutor/types/outbound_payload_size_test.go @@ -0,0 +1,56 @@ +package types_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// F-2026-18146: an outbound payload comes from an attacker-controlled gateway +// event and lands in state, so it is capped at admission. +func TestValidateOutboundPayloadBlobSize(t *testing.T) { + max := types.MaxOutboundPayloadBytes + require.Equal(t, 128*1024, max) + + for _, tc := range []struct { + name string + blob string + wantOK bool + }{ + {"empty", "", true}, + {"small", "0xdeadbeef", true}, + {"at the cap", strings.Repeat("a", max), true}, + {"one over", strings.Repeat("a", max+1), false}, + {"the published ~2 MiB payload", strings.Repeat("a", 2*1024*1024), false}, + } { + t.Run(tc.name, func(t *testing.T) { + err := types.ValidateOutboundPayloadBlobSize("payload", tc.blob) + if tc.wantOK { + require.NoError(t, err) + return + } + require.Error(t, err) + require.Contains(t, err.Error(), "payload too large") + require.Contains(t, err.Error(), "131072") + }) + } +} + +func TestOutboundTx_ValidateSize(t *testing.T) { + require.NoError(t, (*types.OutboundTx)(nil).ValidateSize()) + require.NoError(t, (&types.OutboundTx{Payload: "0xdeadbeef"}).ValidateSize()) + + err := (&types.OutboundTx{Payload: strings.Repeat("a", types.MaxOutboundPayloadBytes+1)}).ValidateSize() + require.Error(t, err) + require.Contains(t, err.Error(), "payload too large") +} + +// The outbound cap is derived from geth's txpool limit, not from the inbound +// cap. Equal today, but they must stay independently changeable. +func TestOutboundCapIsIndependentOfInboundCap(t *testing.T) { + require.Equal(t, 128*1024, types.MaxOutboundPayloadBytes) + require.Equal(t, 128*1024, types.MaxUniversalPayloadBytes) +} diff --git a/x/uexecutor/types/outbound_tx.go b/x/uexecutor/types/outbound_tx.go index 4541989a4..e2e3cfbed 100644 --- a/x/uexecutor/types/outbound_tx.go +++ b/x/uexecutor/types/outbound_tx.go @@ -2,7 +2,6 @@ package types import ( "encoding/json" - "math/big" "strings" "cosmossdk.io/errors" @@ -21,6 +20,15 @@ func (p OutboundTx) String() string { } // ValidateBasic does the sanity check on the OutboundTx fields. +// ValidateSize caps the payload. Split out so the keeper can apply it to an +// event-sourced outbound before the row is built. +func (p *OutboundTx) ValidateSize() error { + if p == nil { + return nil + } + return ValidateOutboundPayloadBlobSize("payload", p.Payload) +} + func (p OutboundTx) ValidateBasic() error { // Validate destination_chain (must follow CAIP-2 format) chain := strings.TrimSpace(p.DestinationChain) @@ -57,7 +65,12 @@ func (p OutboundTx) ValidateBasic() error { if strings.TrimSpace(p.Amount) == "" { return errors.Wrap(sdkerrors.ErrInvalidRequest, "amount cannot be empty for funds tx") } - if bi, ok := new(big.Int).SetString(p.Amount, 10); !ok || bi.Sign() <= 0 { + // Length-capped, range-checked uint256 parse — see F-2026-18798. + bi, err := ValidateUint256String(p.Amount, "amount must be a valid positive uint256") + if err != nil { + return err + } + if bi.Sign() <= 0 { return errors.Wrap(sdkerrors.ErrInvalidRequest, "amount must be a valid positive uint256") } } @@ -92,8 +105,9 @@ func (p OutboundTx) ValidateBasic() error { // gas_limit (uint) if strings.TrimSpace(p.GasLimit) != "" { - if _, ok := new(big.Int).SetString(p.GasLimit, 10); !ok { - return errors.Wrap(sdkerrors.ErrInvalidRequest, "gas_limit must be a valid uint") + // Length-capped, range-checked uint256 parse — see F-2026-18798. + if _, err := ValidateUint256String(p.GasLimit, "gas_limit must be a valid uint"); err != nil { + return err } } diff --git a/x/uexecutor/types/params.go b/x/uexecutor/types/params.go index 6dadfa5a9..489649921 100755 --- a/x/uexecutor/types/params.go +++ b/x/uexecutor/types/params.go @@ -2,13 +2,26 @@ package types import ( "encoding/json" + "fmt" ) +// DefaultMaxGaslessTxGas is the default cap on the gas limit a fee-exempt +// (gasless) transaction may declare. +// +// Fee-paying transactions are self-limiting: the required fee is +// ceil(minGasPrice * gasLimit), so declaring a large gas limit costs real +// tokens. Gasless transactions pay nothing, so nothing bounds the gas they +// declare, and the declared gas is what accumulates into the block's +// cumulative gas wanted. 100,000,000 is roughly 20x the largest gas actually +// consumed by a gasless transaction observed on the network. +const DefaultMaxGaslessTxGas uint64 = 100_000_000 + // DefaultParams returns default module parameters. func DefaultParams() Params { // TODO: return Params{ - SomeValue: true, + SomeValue: true, + MaxGaslessTxGas: DefaultMaxGaslessTxGas, } } @@ -24,6 +37,12 @@ func (p Params) String() string { // Validate does the sanity check on the params. func (p Params) ValidateBasic() error { - // TODO: + // A zero cap would reject every gasless transaction, which would stop the + // universal validators from voting. Governance must always set a usable + // value. + if p.MaxGaslessTxGas == 0 { + return fmt.Errorf("max_gasless_tx_gas must be greater than 0") + } + return nil } diff --git a/x/uexecutor/types/params_test.go b/x/uexecutor/types/params_test.go new file mode 100644 index 000000000..111175f50 --- /dev/null +++ b/x/uexecutor/types/params_test.go @@ -0,0 +1,42 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// TestDefaultParams_MaxGaslessTxGas pins the shipped default. Gasless txs pay +// no fee, so this cap is the only bound on the gas one of them can declare. +func TestDefaultParams_MaxGaslessTxGas(t *testing.T) { + params := types.DefaultParams() + + require.Equal(t, uint64(100_000_000), params.MaxGaslessTxGas) + require.Equal(t, types.DefaultMaxGaslessTxGas, params.MaxGaslessTxGas) + require.NoError(t, params.ValidateBasic()) +} + +// TestParams_ValidateBasic_RejectsZeroCap: a zero cap would reject every +// gasless tx, which would stop the universal validators from voting. +func TestParams_ValidateBasic_RejectsZeroCap(t *testing.T) { + params := types.DefaultParams() + params.MaxGaslessTxGas = 0 + + err := params.ValidateBasic() + + require.Error(t, err) + require.Contains(t, err.Error(), "max_gasless_tx_gas") +} + +// TestParams_ValidateBasic_AcceptsGovernanceChosenCaps: the cap has to be +// movable in both directions by proposal. +func TestParams_ValidateBasic_AcceptsGovernanceChosenCaps(t *testing.T) { + for _, cap := range []uint64{1, 21_000, 30_000_000, 100_000_000, 500_000_000} { + params := types.DefaultParams() + params.MaxGaslessTxGas = cap + + require.NoError(t, params.ValidateBasic(), "cap %d must be settable", cap) + } +} diff --git a/x/uexecutor/types/payload_size_test.go b/x/uexecutor/types/payload_size_test.go new file mode 100644 index 000000000..3d9b59b56 --- /dev/null +++ b/x/uexecutor/types/payload_size_test.go @@ -0,0 +1,188 @@ +package types_test + +import ( + "strings" + "testing" + + "github.com/pushchain/push-chain-node/x/uexecutor/types" + "github.com/stretchr/testify/require" +) + +// payloadWithSize builds a valid UniversalPayload whose serialized size is +// exactly want bytes, by sizing the (hex) data field to fill the remainder. +func payloadWithSize(t *testing.T, want int) types.UniversalPayload { + t.Helper() + + // The data field costs one tag byte plus a varint length plus the bytes + // themselves; every size this test uses falls in the 3-byte varint band. + const dataOverhead = 1 + 3 + + // "0x" plus an even number of hex characters, so the length of the data + // field is always even — the nonce absorbs the odd byte when needed. + for _, nonce := range []string{"1", "11"} { + p := types.UniversalPayload{To: mockHexAddress(), Nonce: nonce} + dataLen := want - p.Size() - dataOverhead + if dataLen < 2 || dataLen%2 != 0 { + continue + } + p.Data = "0x" + strings.Repeat("ab", (dataLen-2)/2) + if p.Size() == want { + return p + } + } + + t.Fatalf("could not build a universal payload of exactly %d bytes", want) + return types.UniversalPayload{} +} + +func TestUniversalPayload_SizeCap(t *testing.T) { + t.Run("at the cap is accepted", func(t *testing.T) { + p := payloadWithSize(t, types.MaxUniversalPayloadBytes) + require.Equal(t, types.MaxUniversalPayloadBytes, p.Size()) + require.NoError(t, p.ValidateSize()) + require.NoError(t, p.ValidateBasic()) + }) + + t.Run("one byte over the cap is rejected", func(t *testing.T) { + p := payloadWithSize(t, types.MaxUniversalPayloadBytes+1) + require.Equal(t, types.MaxUniversalPayloadBytes+1, p.Size()) + + sizeErr := p.ValidateSize() + basicErr := p.ValidateBasic() + + require.Error(t, sizeErr) + require.Contains(t, sizeErr.Error(), "universal payload too large") + require.Contains(t, sizeErr.Error(), "131073 bytes exceeds the 131072 byte limit") + + // ValidateBasic must reject it too — the payload is otherwise valid, so + // the size check is the only thing that can fail it. + require.Error(t, basicErr) + require.Contains(t, basicErr.Error(), "universal payload too large") + }) + + t.Run("nil payload has no size", func(t *testing.T) { + var p *types.UniversalPayload + require.NoError(t, p.ValidateSize()) + }) +} + +func TestValidatePayloadBlobSize(t *testing.T) { + atCap := strings.Repeat("a", types.MaxUniversalPayloadBytes) + overCap := atCap + "a" + + require.NoError(t, types.ValidatePayloadBlobSize("raw_payload", atCap)) + + err := types.ValidatePayloadBlobSize("raw_payload", overCap) + require.Error(t, err) + require.Contains(t, err.Error(), "raw_payload too large") + require.Contains(t, err.Error(), "131073 bytes exceeds the 131072 byte limit") +} + +func TestInbound_SizeCap(t *testing.T) { + base := func() types.Inbound { + return types.Inbound{ + SourceChain: "eip155:11155111", + TxHash: "0x" + strings.Repeat("11", 32), + Sender: mockHexAddress(), + LogIndex: "1", + TxType: types.TxType_FUNDS_AND_PAYLOAD, + } + } + + atCap := strings.Repeat("a", types.MaxUniversalPayloadBytes) + overCap := atCap + "a" + + t.Run("raw_payload at the cap is accepted", func(t *testing.T) { + in := base() + in.RawPayload = atCap + require.Len(t, in.RawPayload, types.MaxUniversalPayloadBytes) + require.NoError(t, in.ValidateSize()) + require.NoError(t, in.ValidateBasic()) + }) + + t.Run("raw_payload one byte over the cap is rejected", func(t *testing.T) { + in := base() + in.RawPayload = overCap + require.Len(t, in.RawPayload, types.MaxUniversalPayloadBytes+1) + + sizeErr := in.ValidateSize() + basicErr := in.ValidateBasic() + + require.Error(t, sizeErr) + require.Contains(t, sizeErr.Error(), "raw_payload too large") + require.Error(t, basicErr) + require.Contains(t, basicErr.Error(), "raw_payload too large") + }) + + t.Run("verification_data over the cap is rejected", func(t *testing.T) { + in := base() + in.VerificationData = overCap + + err := in.ValidateBasic() + require.Error(t, err) + require.Contains(t, err.Error(), "verification_data too large") + }) + + t.Run("embedded universal_payload over the cap is rejected", func(t *testing.T) { + in := base() + p := payloadWithSize(t, types.MaxUniversalPayloadBytes+1) + in.UniversalPayload = &p + + err := in.ValidateBasic() + require.Error(t, err) + require.Contains(t, err.Error(), "universal payload too large") + }) + + t.Run("nil inbound has no size", func(t *testing.T) { + var in *types.Inbound + require.NoError(t, in.ValidateSize()) + }) +} + +func TestMsgExecutePayload_SizeCap(t *testing.T) { + const signer = "push1fgaewhyd9fkwtqaj9c233letwcuey6dgly9gv9" + ua := &types.UniversalAccountId{ + ChainNamespace: "eip155", + ChainId: "11155111", + Owner: "0x000000000000000000000000000000000000dead", + } + + t.Run("payload at the cap is accepted", func(t *testing.T) { + p := payloadWithSize(t, types.MaxUniversalPayloadBytes) + msg := &types.MsgExecutePayload{ + Signer: signer, + UniversalAccountId: ua, + UniversalPayload: &p, + VerificationData: "abcdef0123456789", + } + require.NoError(t, msg.ValidateBasic()) + }) + + t.Run("payload over the cap is rejected", func(t *testing.T) { + p := payloadWithSize(t, types.MaxUniversalPayloadBytes+1) + msg := &types.MsgExecutePayload{ + Signer: signer, + UniversalAccountId: ua, + UniversalPayload: &p, + VerificationData: "abcdef0123456789", + } + + err := msg.ValidateBasic() + require.Error(t, err) + require.Contains(t, err.Error(), "universal payload too large") + }) + + t.Run("verificationData over the cap is rejected", func(t *testing.T) { + p := types.UniversalPayload{To: mockHexAddress(), Data: "0xabcdef"} + msg := &types.MsgExecutePayload{ + Signer: signer, + UniversalAccountId: ua, + UniversalPayload: &p, + VerificationData: strings.Repeat("ab", types.MaxUniversalPayloadBytes), + } + + err := msg.ValidateBasic() + require.Error(t, err) + require.Contains(t, err.Error(), "verificationData too large") + }) +} diff --git a/x/uexecutor/types/tx.pb.go b/x/uexecutor/types/tx.pb.go index 6a642042b..52d8dca10 100644 --- a/x/uexecutor/types/tx.pb.go +++ b/x/uexecutor/types/tx.pb.go @@ -240,116 +240,6 @@ func (m *MsgExecutePayloadResponse) XXX_DiscardUnknown() { var xxx_messageInfo_MsgExecutePayloadResponse proto.InternalMessageInfo -// MsgMigrateUEA defines a message for migarting Universal Executor Account (UEA) -type MsgMigrateUEA struct { - // signer is the Cosmos address initiating the tx (used for tx signing) - Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"` - // universal_account_id is the identifier of the owner account - UniversalAccountId *UniversalAccountId `protobuf:"bytes,2,opt,name=universal_account_id,json=universalAccountId,proto3" json:"universal_account_id,omitempty"` - // payload is the migration payload to be executed - MigrationPayload *MigrationPayload `protobuf:"bytes,3,opt,name=migration_payload,json=migrationPayload,proto3" json:"migration_payload,omitempty"` - // signature is the bytes passed as verifier data for the given payload. - Signature string `protobuf:"bytes,4,opt,name=signature,proto3" json:"signature,omitempty"` -} - -func (m *MsgMigrateUEA) Reset() { *m = MsgMigrateUEA{} } -func (m *MsgMigrateUEA) String() string { return proto.CompactTextString(m) } -func (*MsgMigrateUEA) ProtoMessage() {} -func (*MsgMigrateUEA) Descriptor() ([]byte, []int) { - return fileDescriptor_88d6216044506365, []int{4} -} -func (m *MsgMigrateUEA) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *MsgMigrateUEA) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_MsgMigrateUEA.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *MsgMigrateUEA) XXX_Merge(src proto.Message) { - xxx_messageInfo_MsgMigrateUEA.Merge(m, src) -} -func (m *MsgMigrateUEA) XXX_Size() int { - return m.Size() -} -func (m *MsgMigrateUEA) XXX_DiscardUnknown() { - xxx_messageInfo_MsgMigrateUEA.DiscardUnknown(m) -} - -var xxx_messageInfo_MsgMigrateUEA proto.InternalMessageInfo - -func (m *MsgMigrateUEA) GetSigner() string { - if m != nil { - return m.Signer - } - return "" -} - -func (m *MsgMigrateUEA) GetUniversalAccountId() *UniversalAccountId { - if m != nil { - return m.UniversalAccountId - } - return nil -} - -func (m *MsgMigrateUEA) GetMigrationPayload() *MigrationPayload { - if m != nil { - return m.MigrationPayload - } - return nil -} - -func (m *MsgMigrateUEA) GetSignature() string { - if m != nil { - return m.Signature - } - return "" -} - -// MsgMigrateUEAResponse defines the response for MsgMigrateUEA. -type MsgMigrateUEAResponse struct { -} - -func (m *MsgMigrateUEAResponse) Reset() { *m = MsgMigrateUEAResponse{} } -func (m *MsgMigrateUEAResponse) String() string { return proto.CompactTextString(m) } -func (*MsgMigrateUEAResponse) ProtoMessage() {} -func (*MsgMigrateUEAResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_88d6216044506365, []int{5} -} -func (m *MsgMigrateUEAResponse) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *MsgMigrateUEAResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_MsgMigrateUEAResponse.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *MsgMigrateUEAResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_MsgMigrateUEAResponse.Merge(m, src) -} -func (m *MsgMigrateUEAResponse) XXX_Size() int { - return m.Size() -} -func (m *MsgMigrateUEAResponse) XXX_DiscardUnknown() { - xxx_messageInfo_MsgMigrateUEAResponse.DiscardUnknown(m) -} - -var xxx_messageInfo_MsgMigrateUEAResponse proto.InternalMessageInfo - // MsgVoteInbound allows a universal validator to vote on an inbound transfer. type MsgVoteInbound struct { // signer is the Cosmos address initiating the tx (used for tx signing) @@ -361,7 +251,7 @@ func (m *MsgVoteInbound) Reset() { *m = MsgVoteInbound{} } func (m *MsgVoteInbound) String() string { return proto.CompactTextString(m) } func (*MsgVoteInbound) ProtoMessage() {} func (*MsgVoteInbound) Descriptor() ([]byte, []int) { - return fileDescriptor_88d6216044506365, []int{6} + return fileDescriptor_88d6216044506365, []int{4} } func (m *MsgVoteInbound) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -412,7 +302,7 @@ func (m *MsgVoteInboundResponse) Reset() { *m = MsgVoteInboundResponse{} func (m *MsgVoteInboundResponse) String() string { return proto.CompactTextString(m) } func (*MsgVoteInboundResponse) ProtoMessage() {} func (*MsgVoteInboundResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_88d6216044506365, []int{7} + return fileDescriptor_88d6216044506365, []int{5} } func (m *MsgVoteInboundResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -454,7 +344,7 @@ func (m *MsgVoteOutbound) Reset() { *m = MsgVoteOutbound{} } func (m *MsgVoteOutbound) String() string { return proto.CompactTextString(m) } func (*MsgVoteOutbound) ProtoMessage() {} func (*MsgVoteOutbound) Descriptor() ([]byte, []int) { - return fileDescriptor_88d6216044506365, []int{8} + return fileDescriptor_88d6216044506365, []int{6} } func (m *MsgVoteOutbound) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -519,7 +409,7 @@ func (m *MsgVoteOutboundResponse) Reset() { *m = MsgVoteOutboundResponse func (m *MsgVoteOutboundResponse) String() string { return proto.CompactTextString(m) } func (*MsgVoteOutboundResponse) ProtoMessage() {} func (*MsgVoteOutboundResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_88d6216044506365, []int{9} + return fileDescriptor_88d6216044506365, []int{7} } func (m *MsgVoteOutboundResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -560,7 +450,7 @@ func (m *MsgVoteChainMeta) Reset() { *m = MsgVoteChainMeta{} } func (m *MsgVoteChainMeta) String() string { return proto.CompactTextString(m) } func (*MsgVoteChainMeta) ProtoMessage() {} func (*MsgVoteChainMeta) Descriptor() ([]byte, []int) { - return fileDescriptor_88d6216044506365, []int{10} + return fileDescriptor_88d6216044506365, []int{8} } func (m *MsgVoteChainMeta) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -625,7 +515,7 @@ func (m *MsgVoteChainMetaResponse) Reset() { *m = MsgVoteChainMetaRespon func (m *MsgVoteChainMetaResponse) String() string { return proto.CompactTextString(m) } func (*MsgVoteChainMetaResponse) ProtoMessage() {} func (*MsgVoteChainMetaResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_88d6216044506365, []int{11} + return fileDescriptor_88d6216044506365, []int{9} } func (m *MsgVoteChainMetaResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -670,7 +560,7 @@ func (m *MsgRevertStuckInbound) Reset() { *m = MsgRevertStuckInbound{} } func (m *MsgRevertStuckInbound) String() string { return proto.CompactTextString(m) } func (*MsgRevertStuckInbound) ProtoMessage() {} func (*MsgRevertStuckInbound) Descriptor() ([]byte, []int) { - return fileDescriptor_88d6216044506365, []int{12} + return fileDescriptor_88d6216044506365, []int{10} } func (m *MsgRevertStuckInbound) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -722,7 +612,7 @@ func (m *MsgRevertStuckInboundResponse) Reset() { *m = MsgRevertStuckInb func (m *MsgRevertStuckInboundResponse) String() string { return proto.CompactTextString(m) } func (*MsgRevertStuckInboundResponse) ProtoMessage() {} func (*MsgRevertStuckInboundResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_88d6216044506365, []int{13} + return fileDescriptor_88d6216044506365, []int{11} } func (m *MsgRevertStuckInboundResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -765,13 +655,240 @@ func (m *MsgRevertStuckInboundResponse) GetOutboundId() string { return "" } +// MsgExecuteStuckInbound is an admin escape hatch and the sibling of +// MsgRevertStuckInbound. For an inbound whose ballot is stored PENDING but can +// never finalize on its own, and whose YES votes already meet the recomputed +// threshold, this marks the ballot PASSED and runs the same post-finalization +// pipeline a finalizing vote would have run - so the user receives the bridged +// funds on Push instead of a source-chain refund. +type MsgExecuteStuckInbound struct { + // signer must equal uvalidator Params.Admin + Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"` + // inbound is the original payload the stuck ballot was voting on. Admin + // supplies this from off-chain UV observation logs since the chain does not + // persist ballot payloads. + Inbound *Inbound `protobuf:"bytes,2,opt,name=inbound,proto3" json:"inbound,omitempty"` +} + +func (m *MsgExecuteStuckInbound) Reset() { *m = MsgExecuteStuckInbound{} } +func (m *MsgExecuteStuckInbound) String() string { return proto.CompactTextString(m) } +func (*MsgExecuteStuckInbound) ProtoMessage() {} +func (*MsgExecuteStuckInbound) Descriptor() ([]byte, []int) { + return fileDescriptor_88d6216044506365, []int{12} +} +func (m *MsgExecuteStuckInbound) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgExecuteStuckInbound) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgExecuteStuckInbound.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgExecuteStuckInbound) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgExecuteStuckInbound.Merge(m, src) +} +func (m *MsgExecuteStuckInbound) XXX_Size() int { + return m.Size() +} +func (m *MsgExecuteStuckInbound) XXX_DiscardUnknown() { + xxx_messageInfo_MsgExecuteStuckInbound.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgExecuteStuckInbound proto.InternalMessageInfo + +func (m *MsgExecuteStuckInbound) GetSigner() string { + if m != nil { + return m.Signer + } + return "" +} + +func (m *MsgExecuteStuckInbound) GetInbound() *Inbound { + if m != nil { + return m.Inbound + } + return nil +} + +type MsgExecuteStuckInboundResponse struct { + UtxId string `protobuf:"bytes,1,opt,name=utx_id,json=utxId,proto3" json:"utx_id,omitempty"` +} + +func (m *MsgExecuteStuckInboundResponse) Reset() { *m = MsgExecuteStuckInboundResponse{} } +func (m *MsgExecuteStuckInboundResponse) String() string { return proto.CompactTextString(m) } +func (*MsgExecuteStuckInboundResponse) ProtoMessage() {} +func (*MsgExecuteStuckInboundResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_88d6216044506365, []int{13} +} +func (m *MsgExecuteStuckInboundResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgExecuteStuckInboundResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgExecuteStuckInboundResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgExecuteStuckInboundResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgExecuteStuckInboundResponse.Merge(m, src) +} +func (m *MsgExecuteStuckInboundResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgExecuteStuckInboundResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgExecuteStuckInboundResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgExecuteStuckInboundResponse proto.InternalMessageInfo + +func (m *MsgExecuteStuckInboundResponse) GetUtxId() string { + if m != nil { + return m.UtxId + } + return "" +} + +// MsgExecuteStuckOutbound is an admin escape hatch for an outbound whose ballot +// can no longer reach a terminal-and-settled state — EXPIRED, or PENDING with +// every eligible voter already voted and the YES votes at the stored threshold. +// It runs the same settlement pipeline a finalizing vote would have run, so the +// outcome follows observed_tx.success: a success settles, a failure mints the +// bridged tokens back to the revert recipient and refunds the excess gas. +type MsgExecuteStuckOutbound struct { + // signer must equal uvalidator Params.Admin + Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"` + TxId string `protobuf:"bytes,2,opt,name=tx_id,json=txId,proto3" json:"tx_id,omitempty"` + UtxId string `protobuf:"bytes,3,opt,name=utx_id,json=utxId,proto3" json:"utx_id,omitempty"` + // observed_tx is the destination-chain observation the stuck ballot was voting + // on. Admin supplies this from off-chain UV observation logs since the chain + // does not persist ballot payloads; it must match field-for-field or the + // derived ballot key finds no ballot. + ObservedTx *OutboundObservation `protobuf:"bytes,4,opt,name=observed_tx,json=observedTx,proto3" json:"observed_tx,omitempty"` +} + +func (m *MsgExecuteStuckOutbound) Reset() { *m = MsgExecuteStuckOutbound{} } +func (m *MsgExecuteStuckOutbound) String() string { return proto.CompactTextString(m) } +func (*MsgExecuteStuckOutbound) ProtoMessage() {} +func (*MsgExecuteStuckOutbound) Descriptor() ([]byte, []int) { + return fileDescriptor_88d6216044506365, []int{14} +} +func (m *MsgExecuteStuckOutbound) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgExecuteStuckOutbound) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgExecuteStuckOutbound.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgExecuteStuckOutbound) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgExecuteStuckOutbound.Merge(m, src) +} +func (m *MsgExecuteStuckOutbound) XXX_Size() int { + return m.Size() +} +func (m *MsgExecuteStuckOutbound) XXX_DiscardUnknown() { + xxx_messageInfo_MsgExecuteStuckOutbound.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgExecuteStuckOutbound proto.InternalMessageInfo + +func (m *MsgExecuteStuckOutbound) GetSigner() string { + if m != nil { + return m.Signer + } + return "" +} + +func (m *MsgExecuteStuckOutbound) GetTxId() string { + if m != nil { + return m.TxId + } + return "" +} + +func (m *MsgExecuteStuckOutbound) GetUtxId() string { + if m != nil { + return m.UtxId + } + return "" +} + +func (m *MsgExecuteStuckOutbound) GetObservedTx() *OutboundObservation { + if m != nil { + return m.ObservedTx + } + return nil +} + +type MsgExecuteStuckOutboundResponse struct { + OutboundId string `protobuf:"bytes,1,opt,name=outbound_id,json=outboundId,proto3" json:"outbound_id,omitempty"` +} + +func (m *MsgExecuteStuckOutboundResponse) Reset() { *m = MsgExecuteStuckOutboundResponse{} } +func (m *MsgExecuteStuckOutboundResponse) String() string { return proto.CompactTextString(m) } +func (*MsgExecuteStuckOutboundResponse) ProtoMessage() {} +func (*MsgExecuteStuckOutboundResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_88d6216044506365, []int{15} +} +func (m *MsgExecuteStuckOutboundResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgExecuteStuckOutboundResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgExecuteStuckOutboundResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgExecuteStuckOutboundResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgExecuteStuckOutboundResponse.Merge(m, src) +} +func (m *MsgExecuteStuckOutboundResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgExecuteStuckOutboundResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgExecuteStuckOutboundResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgExecuteStuckOutboundResponse proto.InternalMessageInfo + +func (m *MsgExecuteStuckOutboundResponse) GetOutboundId() string { + if m != nil { + return m.OutboundId + } + return "" +} + func init() { proto.RegisterType((*MsgUpdateParams)(nil), "uexecutor.v1.MsgUpdateParams") proto.RegisterType((*MsgUpdateParamsResponse)(nil), "uexecutor.v1.MsgUpdateParamsResponse") proto.RegisterType((*MsgExecutePayload)(nil), "uexecutor.v1.MsgExecutePayload") proto.RegisterType((*MsgExecutePayloadResponse)(nil), "uexecutor.v1.MsgExecutePayloadResponse") - proto.RegisterType((*MsgMigrateUEA)(nil), "uexecutor.v1.MsgMigrateUEA") - proto.RegisterType((*MsgMigrateUEAResponse)(nil), "uexecutor.v1.MsgMigrateUEAResponse") proto.RegisterType((*MsgVoteInbound)(nil), "uexecutor.v1.MsgVoteInbound") proto.RegisterType((*MsgVoteInboundResponse)(nil), "uexecutor.v1.MsgVoteInboundResponse") proto.RegisterType((*MsgVoteOutbound)(nil), "uexecutor.v1.MsgVoteOutbound") @@ -780,70 +897,76 @@ func init() { proto.RegisterType((*MsgVoteChainMetaResponse)(nil), "uexecutor.v1.MsgVoteChainMetaResponse") proto.RegisterType((*MsgRevertStuckInbound)(nil), "uexecutor.v1.MsgRevertStuckInbound") proto.RegisterType((*MsgRevertStuckInboundResponse)(nil), "uexecutor.v1.MsgRevertStuckInboundResponse") + proto.RegisterType((*MsgExecuteStuckInbound)(nil), "uexecutor.v1.MsgExecuteStuckInbound") + proto.RegisterType((*MsgExecuteStuckInboundResponse)(nil), "uexecutor.v1.MsgExecuteStuckInboundResponse") + proto.RegisterType((*MsgExecuteStuckOutbound)(nil), "uexecutor.v1.MsgExecuteStuckOutbound") + proto.RegisterType((*MsgExecuteStuckOutboundResponse)(nil), "uexecutor.v1.MsgExecuteStuckOutboundResponse") } func init() { proto.RegisterFile("uexecutor/v1/tx.proto", fileDescriptor_88d6216044506365) } var fileDescriptor_88d6216044506365 = []byte{ - // 928 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x56, 0xbf, 0x6f, 0xdb, 0x46, - 0x14, 0x36, 0x6d, 0xd9, 0x85, 0x9f, 0x9c, 0xc4, 0x62, 0xe5, 0x5a, 0xa6, 0x63, 0xd9, 0x56, 0x7f, - 0x24, 0x95, 0x6b, 0xb1, 0x71, 0x81, 0x0c, 0xda, 0xec, 0x36, 0x40, 0x85, 0x42, 0x8d, 0xcb, 0xd8, - 0x0d, 0x90, 0x45, 0x38, 0x91, 0x17, 0x8a, 0x68, 0xc8, 0x23, 0x78, 0x47, 0x41, 0xde, 0x8a, 0x8e, - 0x9d, 0x3a, 0xf5, 0x7f, 0x28, 0xb2, 0x78, 0xe8, 0x1f, 0xd0, 0x31, 0x63, 0x50, 0xa0, 0x40, 0xa7, - 0xa0, 0xb0, 0x07, 0xff, 0x0b, 0x1d, 0x0b, 0x1e, 0xc9, 0x23, 0x8f, 0x54, 0xe5, 0xc2, 0x43, 0x91, - 0x45, 0x38, 0x7d, 0xdf, 0x7b, 0x8f, 0xef, 0xfb, 0xee, 0xee, 0x91, 0xb0, 0x16, 0xe2, 0x09, 0x36, - 0x43, 0x46, 0x02, 0x7d, 0xfc, 0x40, 0x67, 0x93, 0x8e, 0x1f, 0x10, 0x46, 0xd4, 0x15, 0x01, 0x77, - 0xc6, 0x0f, 0xb4, 0x1a, 0x72, 0x1d, 0x8f, 0xe8, 0xfc, 0x37, 0x0e, 0xd0, 0xd6, 0x4d, 0x42, 0x5d, - 0x42, 0x75, 0x97, 0xda, 0x51, 0xa2, 0x4b, 0xed, 0x84, 0x68, 0xc8, 0x05, 0xcf, 0x7c, 0x4c, 0x13, - 0x66, 0x4b, 0x62, 0xcc, 0x11, 0x72, 0xbc, 0x81, 0x8b, 0x19, 0x4a, 0xe8, 0xba, 0x4d, 0x6c, 0xc2, - 0x97, 0x7a, 0xb4, 0x4a, 0xd0, 0x8d, 0xf8, 0x39, 0x83, 0x98, 0x88, 0xff, 0xc4, 0x54, 0xeb, 0xa5, - 0x02, 0x77, 0xfa, 0xd4, 0x3e, 0xf5, 0x2d, 0xc4, 0xf0, 0x31, 0x0a, 0x90, 0x4b, 0xd5, 0x87, 0xb0, - 0x8c, 0x42, 0x36, 0x22, 0x81, 0xc3, 0xce, 0x1a, 0xca, 0x8e, 0x72, 0x7f, 0xf9, 0xa8, 0xf1, 0xfb, - 0xaf, 0xfb, 0xf5, 0x24, 0xf1, 0xd0, 0xb2, 0x02, 0x4c, 0xe9, 0x13, 0x16, 0x38, 0x9e, 0x6d, 0x64, - 0xa1, 0xea, 0x01, 0x2c, 0xf9, 0xbc, 0x42, 0x63, 0x7e, 0x47, 0xb9, 0x5f, 0x3d, 0xa8, 0x77, 0xf2, - 0x06, 0x74, 0xe2, 0xea, 0x47, 0x95, 0x57, 0x6f, 0xb6, 0xe7, 0x8c, 0x24, 0xb2, 0xfb, 0xc9, 0x0f, - 0x57, 0xe7, 0xed, 0xac, 0xc6, 0x8f, 0x57, 0xe7, 0xed, 0x8d, 0x4c, 0x62, 0xa1, 0xb3, 0xd6, 0x06, - 0xac, 0x17, 0x20, 0x03, 0x53, 0x9f, 0x78, 0x14, 0xb7, 0x7e, 0x9b, 0x87, 0x5a, 0x9f, 0xda, 0x8f, - 0x78, 0x2a, 0x3e, 0x46, 0x67, 0x2f, 0x08, 0xb2, 0xd4, 0x4f, 0x61, 0x89, 0x3a, 0xb6, 0x87, 0x83, - 0x6b, 0x75, 0x24, 0x71, 0xaa, 0x01, 0xf5, 0xd0, 0x73, 0xc6, 0x38, 0xa0, 0xe8, 0xc5, 0x00, 0x99, - 0x26, 0x09, 0x3d, 0x36, 0x70, 0xac, 0x44, 0xd2, 0x8e, 0x2c, 0xe9, 0x34, 0x8d, 0x3c, 0x8c, 0x03, - 0x7b, 0x96, 0xa1, 0x86, 0x25, 0x4c, 0xfd, 0x0a, 0x6a, 0x59, 0x4d, 0x3f, 0x6e, 0xad, 0xb1, 0xc0, - 0x0b, 0x36, 0xff, 0xa5, 0x60, 0x22, 0xc0, 0x58, 0x0d, 0x0b, 0x88, 0xba, 0x07, 0xb5, 0x31, 0x0e, - 0x9c, 0xe7, 0x8e, 0x89, 0x98, 0x43, 0xbc, 0x81, 0x85, 0x18, 0x6a, 0x54, 0x22, 0x75, 0xc6, 0x6a, - 0x9e, 0xf8, 0x02, 0x31, 0xd4, 0xdd, 0x8b, 0xec, 0x4d, 0xa4, 0x45, 0xde, 0x6e, 0x4a, 0xde, 0xca, - 0x66, 0xb5, 0x36, 0x61, 0xa3, 0x04, 0x0a, 0x7f, 0x7f, 0x99, 0x87, 0x5b, 0x7d, 0x6a, 0xf7, 0x1d, - 0x3b, 0x40, 0x0c, 0x9f, 0x3e, 0x3a, 0x7c, 0x7b, 0xbc, 0x75, 0x79, 0x4f, 0x91, 0x17, 0x33, 0xbd, - 0xed, 0xa7, 0x61, 0xc2, 0x5b, 0xb7, 0x80, 0xa8, 0x77, 0x61, 0x39, 0x6a, 0x15, 0xb1, 0x30, 0xc0, - 0x89, 0xa7, 0x19, 0xd0, 0xbd, 0x57, 0x30, 0x73, 0x5d, 0x32, 0x33, 0x73, 0xa6, 0xb5, 0x0e, 0x6b, - 0x12, 0x20, 0x4c, 0xfc, 0x59, 0x81, 0xdb, 0x7d, 0x6a, 0x7f, 0x4b, 0x18, 0xee, 0x79, 0x43, 0x12, - 0x7a, 0x37, 0x39, 0xa1, 0x3a, 0xbc, 0xe3, 0xc4, 0xc9, 0x89, 0x71, 0x6b, 0xb2, 0xce, 0xa4, 0xb2, - 0x91, 0x46, 0x75, 0x77, 0x0b, 0x7d, 0xd7, 0x42, 0xac, 0xcb, 0x5d, 0xb4, 0x1a, 0xf0, 0x9e, 0x8c, - 0x88, 0x96, 0xdf, 0xc4, 0x03, 0x22, 0xa2, 0x1e, 0x87, 0xec, 0xa6, 0x3d, 0xbf, 0x0b, 0x8b, 0x6c, - 0x92, 0x6e, 0xf5, 0xb2, 0x51, 0x61, 0x93, 0x9e, 0xa5, 0xae, 0xc1, 0x52, 0x18, 0xa3, 0x0b, 0x1c, - 0x5d, 0x0c, 0x39, 0x7c, 0x04, 0x55, 0x32, 0xa4, 0x38, 0x18, 0x63, 0x6b, 0xc0, 0x26, 0x7c, 0x1b, - 0xaa, 0x07, 0xbb, 0xb2, 0xc6, 0xb4, 0x95, 0xc7, 0x3c, 0x90, 0xef, 0xa1, 0x01, 0x69, 0xd6, 0xc9, - 0xa4, 0xfb, 0x71, 0x41, 0xb2, 0x3c, 0x53, 0xf2, 0x62, 0x92, 0x99, 0x92, 0x87, 0x84, 0xf6, 0x3f, - 0x14, 0x58, 0x4d, 0xb8, 0xcf, 0xa3, 0x49, 0xdb, 0xc7, 0x0c, 0xdd, 0x40, 0x7c, 0x1b, 0x6a, 0x42, - 0x50, 0x3c, 0xb1, 0x85, 0x11, 0x77, 0x52, 0x82, 0xd7, 0xef, 0x59, 0x6a, 0x1d, 0x16, 0xfd, 0xc0, - 0x31, 0x31, 0xb7, 0xa4, 0x62, 0xc4, 0x7f, 0xd4, 0x5d, 0x58, 0x89, 0x13, 0x47, 0xd8, 0xb1, 0x47, - 0x8c, 0x7b, 0x52, 0x31, 0xaa, 0x1c, 0xfb, 0x92, 0x43, 0xdd, 0x76, 0x41, 0xb1, 0x56, 0x52, 0x2c, - 0x24, 0xb4, 0x34, 0x68, 0x14, 0x31, 0xa1, 0xf9, 0xa5, 0xc2, 0x0f, 0xaf, 0x81, 0xc7, 0x38, 0x60, - 0x4f, 0x58, 0x68, 0x7e, 0xf7, 0x3f, 0x9e, 0x54, 0xbd, 0x20, 0x62, 0x5b, 0x12, 0x51, 0xee, 0xa9, - 0xf5, 0x14, 0xb6, 0xa6, 0x12, 0xa9, 0x9c, 0xdc, 0x19, 0x53, 0xf2, 0x67, 0x6c, 0x1b, 0xaa, 0x24, - 0xd9, 0xed, 0x6c, 0x33, 0x20, 0x85, 0x7a, 0xd6, 0xc1, 0xdf, 0x15, 0x58, 0xe8, 0x53, 0x5b, 0x3d, - 0x81, 0x15, 0xe9, 0xdd, 0xb8, 0x55, 0x98, 0x29, 0xf2, 0xdb, 0x48, 0xfb, 0x70, 0x26, 0x2d, 0xba, - 0x7a, 0x06, 0xb7, 0x0b, 0x2f, 0xaa, 0xed, 0x52, 0xa2, 0x1c, 0xa0, 0xdd, 0xbb, 0x26, 0x40, 0xd4, - 0xfe, 0x1a, 0x20, 0x37, 0xa4, 0x37, 0x4b, 0x69, 0x19, 0xa9, 0xbd, 0x3f, 0x83, 0x14, 0xf5, 0xbe, - 0x81, 0x6a, 0x7e, 0x5e, 0xdd, 0x2d, 0xe5, 0xe4, 0x58, 0xed, 0x83, 0x59, 0xac, 0x28, 0x79, 0x02, - 0x2b, 0xd2, 0x3c, 0xd9, 0x9a, 0x9a, 0x95, 0xd2, 0x53, 0x4c, 0x9d, 0x76, 0x5b, 0xd5, 0xa7, 0x70, - 0x4b, 0xbe, 0xa9, 0xcd, 0xa9, 0x79, 0x82, 0xd7, 0x3e, 0x9a, 0xcd, 0x8b, 0xc2, 0xcf, 0x41, 0x9d, - 0x72, 0x1d, 0xca, 0xe6, 0x95, 0x83, 0xb4, 0xbd, 0xff, 0x10, 0x94, 0x3e, 0x47, 0x5b, 0xfc, 0xfe, - 0xea, 0xbc, 0xad, 0x1c, 0x1d, 0xbf, 0xba, 0x68, 0x2a, 0xaf, 0x2f, 0x9a, 0xca, 0x5f, 0x17, 0x4d, - 0xe5, 0xa7, 0xcb, 0xe6, 0xdc, 0xeb, 0xcb, 0xe6, 0xdc, 0x9f, 0x97, 0xcd, 0xb9, 0x67, 0x0f, 0x6d, - 0x87, 0x8d, 0xc2, 0x61, 0xc7, 0x24, 0xae, 0xee, 0x87, 0x74, 0xc4, 0xef, 0x3f, 0x5f, 0xed, 0xf3, - 0xe5, 0xbe, 0x47, 0x2c, 0xac, 0x4f, 0xf4, 0xec, 0xd6, 0xf0, 0x4f, 0xc7, 0xe1, 0x12, 0xff, 0xd6, - 0xfb, 0xec, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x5b, 0x71, 0xf9, 0x2d, 0xa8, 0x0a, 0x00, 0x00, + // 946 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x56, 0xcd, 0x6e, 0xdb, 0x46, + 0x10, 0x36, 0x63, 0x59, 0x85, 0x47, 0x6e, 0x62, 0x31, 0x72, 0x23, 0x33, 0x35, 0x65, 0xb3, 0x49, + 0x9b, 0xca, 0xb1, 0x18, 0xbb, 0x40, 0x0a, 0xe8, 0x16, 0xb5, 0x05, 0x2a, 0x14, 0x42, 0x5c, 0xc6, + 0x69, 0x80, 0x5c, 0x84, 0x35, 0xb9, 0xa1, 0x88, 0x5a, 0x5c, 0x81, 0xbb, 0x14, 0xe4, 0x5b, 0xdb, + 0x63, 0x4f, 0x3d, 0xf5, 0x25, 0x82, 0x02, 0x3e, 0xf4, 0x01, 0x7a, 0xcc, 0x31, 0x28, 0x50, 0xa0, + 0xa7, 0xa0, 0xb0, 0x0f, 0xbe, 0xf5, 0x19, 0x0a, 0x2d, 0xc9, 0x15, 0x97, 0xa4, 0x7f, 0xe0, 0x83, + 0x81, 0x5e, 0x84, 0xd5, 0x7c, 0x33, 0xb3, 0xf3, 0x7d, 0x3b, 0xb3, 0x4b, 0x58, 0x09, 0xf1, 0x04, + 0xdb, 0x21, 0x23, 0x81, 0x39, 0xde, 0x36, 0xd9, 0xa4, 0x35, 0x0a, 0x08, 0x23, 0xea, 0x92, 0x30, + 0xb7, 0xc6, 0xdb, 0x5a, 0x15, 0x0d, 0x3d, 0x9f, 0x98, 0xfc, 0x37, 0x72, 0xd0, 0xee, 0xd8, 0x84, + 0x0e, 0x09, 0x35, 0x87, 0xd4, 0x9d, 0x06, 0x0e, 0xa9, 0x1b, 0x03, 0x75, 0x39, 0xe1, 0xe1, 0x08, + 0xd3, 0x18, 0x59, 0x93, 0x10, 0x7b, 0x80, 0x3c, 0xbf, 0x3f, 0xc4, 0x0c, 0xc5, 0x70, 0xcd, 0x25, + 0x2e, 0xe1, 0x4b, 0x73, 0xba, 0x8a, 0xad, 0xab, 0xd1, 0x3e, 0xfd, 0x08, 0x88, 0xfe, 0x44, 0x90, + 0xf1, 0x5a, 0x81, 0x5b, 0x3d, 0xea, 0x3e, 0x1f, 0x39, 0x88, 0xe1, 0x5d, 0x14, 0xa0, 0x21, 0x55, + 0x1f, 0xc3, 0x22, 0x0a, 0xd9, 0x80, 0x04, 0x1e, 0x3b, 0xac, 0x2b, 0xeb, 0xca, 0x83, 0xc5, 0x4e, + 0xfd, 0xcf, 0xdf, 0xb7, 0x6a, 0x71, 0xe0, 0x13, 0xc7, 0x09, 0x30, 0xa5, 0xcf, 0x58, 0xe0, 0xf9, + 0xae, 0x35, 0x73, 0x55, 0x77, 0xa0, 0x3c, 0xe2, 0x19, 0xea, 0x37, 0xd6, 0x95, 0x07, 0x95, 0x9d, + 0x5a, 0x2b, 0x2d, 0x40, 0x2b, 0xca, 0xde, 0x29, 0xbd, 0x79, 0xd7, 0x98, 0xb3, 0x62, 0xcf, 0xf6, + 0xc3, 0x9f, 0x4e, 0x8f, 0x9a, 0xb3, 0x1c, 0x3f, 0x9f, 0x1e, 0x35, 0x57, 0x67, 0x14, 0x33, 0x95, + 0x19, 0xab, 0x70, 0x27, 0x63, 0xb2, 0x30, 0x1d, 0x11, 0x9f, 0x62, 0xe3, 0x8f, 0x1b, 0x50, 0xed, + 0x51, 0xf7, 0x2b, 0x1e, 0x8a, 0x77, 0xd1, 0xe1, 0x01, 0x41, 0x8e, 0xfa, 0x08, 0xca, 0xd4, 0x73, + 0x7d, 0x1c, 0x5c, 0xc8, 0x23, 0xf6, 0x53, 0x2d, 0xa8, 0x85, 0xbe, 0x37, 0xc6, 0x01, 0x45, 0x07, + 0x7d, 0x64, 0xdb, 0x24, 0xf4, 0x59, 0xdf, 0x73, 0x62, 0x4a, 0xeb, 0x32, 0xa5, 0xe7, 0x89, 0xe7, + 0x93, 0xc8, 0xb1, 0xeb, 0x58, 0x6a, 0x98, 0xb3, 0xa9, 0xdf, 0x40, 0x75, 0x96, 0x73, 0x14, 0x95, + 0x56, 0x9f, 0xe7, 0x09, 0xf5, 0x33, 0x12, 0xc6, 0x04, 0xac, 0xe5, 0x30, 0x63, 0x51, 0x37, 0xa1, + 0x3a, 0xc6, 0x81, 0xf7, 0xca, 0xb3, 0x11, 0xf3, 0x88, 0xdf, 0x77, 0x10, 0x43, 0xf5, 0xd2, 0x94, + 0x9d, 0xb5, 0x9c, 0x06, 0xbe, 0x44, 0x0c, 0xb5, 0x37, 0xa7, 0xf2, 0xc6, 0xd4, 0xa6, 0xda, 0xde, + 0x95, 0xb4, 0x95, 0xc5, 0x32, 0xee, 0xc2, 0x6a, 0xce, 0x28, 0xf4, 0xfd, 0x55, 0x81, 0x9b, 0x3d, + 0xea, 0x7e, 0x47, 0x18, 0xee, 0xfa, 0xfb, 0x24, 0xf4, 0xaf, 0x22, 0xae, 0x09, 0xef, 0x79, 0x51, + 0x70, 0xac, 0xe7, 0x8a, 0x4c, 0x3f, 0xce, 0x6c, 0x25, 0x5e, 0xed, 0x8d, 0x4c, 0xfd, 0xd5, 0x10, + 0x9b, 0x72, 0x15, 0x46, 0x1d, 0x3e, 0x90, 0x2d, 0xa2, 0xe4, 0x77, 0x51, 0x6f, 0x4f, 0xa1, 0xa7, + 0x21, 0xbb, 0x6a, 0xcd, 0xb7, 0x61, 0x81, 0x4d, 0x92, 0x0e, 0x58, 0xb4, 0x4a, 0x6c, 0xd2, 0x75, + 0xd4, 0x15, 0x28, 0x87, 0x91, 0x75, 0x9e, 0x5b, 0x17, 0x42, 0x6e, 0xee, 0x40, 0x85, 0xec, 0x53, + 0x1c, 0x8c, 0xb1, 0xd3, 0x67, 0x13, 0x7e, 0x2a, 0x95, 0x9d, 0x0d, 0x99, 0x63, 0x52, 0xca, 0x53, + 0xee, 0xc8, 0x8f, 0xca, 0x82, 0x24, 0x6a, 0x6f, 0xd2, 0xfe, 0x34, 0x43, 0x59, 0x1e, 0x87, 0x34, + 0x99, 0x78, 0x1c, 0xd2, 0x26, 0xc1, 0xfd, 0x2f, 0x05, 0x96, 0x63, 0xec, 0x8b, 0xe9, 0x25, 0xd1, + 0xc3, 0x0c, 0x5d, 0x81, 0x7c, 0x13, 0xaa, 0x82, 0x50, 0x74, 0xd9, 0x08, 0x21, 0x6e, 0x25, 0x00, + 0xcf, 0xdf, 0x75, 0xd4, 0x1a, 0x2c, 0x8c, 0x02, 0xcf, 0xc6, 0x5c, 0x92, 0x92, 0x15, 0xfd, 0x51, + 0x37, 0x60, 0x29, 0x0a, 0x1c, 0x60, 0xcf, 0x1d, 0x30, 0xae, 0x49, 0xc9, 0xaa, 0x70, 0xdb, 0xd7, + 0xdc, 0xd4, 0x6e, 0x66, 0x18, 0x6b, 0x39, 0xc6, 0x82, 0x82, 0xa1, 0x41, 0x3d, 0x6b, 0x13, 0x9c, + 0x5f, 0x2b, 0xb0, 0xd2, 0xa3, 0xae, 0x85, 0xc7, 0x38, 0x60, 0xcf, 0x58, 0x68, 0x7f, 0x7f, 0x8d, + 0x9d, 0x6a, 0x66, 0x48, 0x34, 0x24, 0x12, 0xf9, 0x9a, 0x8c, 0x17, 0xb0, 0x56, 0x08, 0x24, 0x74, + 0x52, 0x3d, 0xa6, 0xa4, 0x7b, 0xac, 0x01, 0x15, 0x12, 0x9f, 0xf6, 0xec, 0x30, 0x20, 0x31, 0x75, + 0x1d, 0xe3, 0x37, 0x85, 0x4f, 0x44, 0x3c, 0xc7, 0xd7, 0xad, 0xc3, 0xa3, 0x8c, 0x0e, 0xeb, 0x45, + 0x37, 0x8e, 0x24, 0xc4, 0xe7, 0xa0, 0x17, 0x23, 0x17, 0x28, 0x61, 0xfc, 0xab, 0xf0, 0xfe, 0x4f, + 0x47, 0xfe, 0x8f, 0xe6, 0x7c, 0x3b, 0x23, 0xd4, 0xc6, 0x99, 0x42, 0x89, 0x79, 0xef, 0x40, 0xe3, + 0x0c, 0x48, 0x48, 0x95, 0xe9, 0x0e, 0x25, 0xdb, 0x1d, 0x3b, 0x3f, 0x96, 0x61, 0xbe, 0x47, 0x5d, + 0x75, 0x0f, 0x96, 0xa4, 0x47, 0x7f, 0x4d, 0xae, 0x3e, 0xf3, 0xcc, 0x6a, 0xf7, 0xcf, 0x85, 0xc5, + 0xf6, 0x2f, 0xe1, 0x66, 0xe6, 0x05, 0x6e, 0xe4, 0x02, 0x65, 0x07, 0xed, 0x93, 0x0b, 0x1c, 0x44, + 0xee, 0x6f, 0xa1, 0x92, 0x7e, 0x7d, 0x3e, 0xcc, 0xc5, 0xa5, 0x50, 0xed, 0xde, 0x79, 0xa8, 0x48, + 0xb9, 0x07, 0x4b, 0xd2, 0xeb, 0xb0, 0x56, 0x18, 0x95, 0xc0, 0x05, 0x22, 0x14, 0xdd, 0xbd, 0xea, + 0x0b, 0x78, 0x5f, 0xbe, 0x77, 0xf5, 0xc2, 0x38, 0x81, 0x6b, 0x1f, 0x9f, 0x8f, 0x8b, 0xc4, 0xaf, + 0x40, 0x2d, 0xb8, 0xdc, 0x3e, 0xca, 0x45, 0xe7, 0x9d, 0xb4, 0xcd, 0x4b, 0x38, 0x89, 0x7d, 0x3c, + 0xb8, 0x5d, 0x74, 0x7b, 0xdc, 0x3b, 0xeb, 0xa4, 0xa4, 0x9d, 0x1e, 0x5e, 0xc6, 0x4b, 0x6c, 0x75, + 0x00, 0xb5, 0xc2, 0xf9, 0xbd, 0x7f, 0x6e, 0x16, 0x71, 0x22, 0x5b, 0x97, 0x72, 0x4b, 0x76, 0xd3, + 0x16, 0x7e, 0x38, 0x3d, 0x6a, 0x2a, 0x9d, 0xdd, 0x37, 0xc7, 0xba, 0xf2, 0xf6, 0x58, 0x57, 0xfe, + 0x39, 0xd6, 0x95, 0x5f, 0x4e, 0xf4, 0xb9, 0xb7, 0x27, 0xfa, 0xdc, 0xdf, 0x27, 0xfa, 0xdc, 0xcb, + 0xc7, 0xae, 0xc7, 0x06, 0xe1, 0x7e, 0xcb, 0x26, 0x43, 0x73, 0x14, 0xd2, 0x01, 0x7f, 0xa6, 0xf8, + 0x6a, 0x8b, 0x2f, 0xb7, 0x7c, 0xe2, 0x60, 0x73, 0x62, 0xce, 0x66, 0x95, 0x7f, 0x9c, 0xef, 0x97, + 0xf9, 0xd7, 0xf4, 0x67, 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, 0x40, 0x4b, 0x06, 0x6e, 0x0a, 0x0c, + 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -864,8 +987,6 @@ type MsgClient interface { UpdateParams(ctx context.Context, in *MsgUpdateParams, opts ...grpc.CallOption) (*MsgUpdateParamsResponse, error) // ExecutePayload defines a message for executing a universal payload ExecutePayload(ctx context.Context, in *MsgExecutePayload, opts ...grpc.CallOption) (*MsgExecutePayloadResponse, error) - // MigrateUEA defines a message for migrating UEA - MigrateUEA(ctx context.Context, in *MsgMigrateUEA, opts ...grpc.CallOption) (*MsgMigrateUEAResponse, error) // VoteInbound defines a message for voting on synthetic assets bridging from external chain to PC VoteInbound(ctx context.Context, in *MsgVoteInbound, opts ...grpc.CallOption) (*MsgVoteInboundResponse, error) // VoteOutbound defines a message for voting on a observed outbound tx on external chain @@ -876,6 +997,15 @@ type MsgClient interface { // ballot has expired without finalizing, refunding the user on the source // chain via the normal revert/outbound flow. Admin-only escape hatch. RevertStuckInbound(ctx context.Context, in *MsgRevertStuckInbound, opts ...grpc.CallOption) (*MsgRevertStuckInboundResponse, error) + // ExecuteStuckInbound finalizes an inbound ballot that is provably unable to + // finalize on its own yet already carries enough YES votes, then runs the + // normal post-finalization pipeline so the user receives funds on Push. + // Admin-only escape hatch, sibling of RevertStuckInbound. + ExecuteStuckInbound(ctx context.Context, in *MsgExecuteStuckInbound, opts ...grpc.CallOption) (*MsgExecuteStuckInboundResponse, error) + // ExecuteStuckOutbound settles an outbound whose ballot can no longer reach a + // terminal-and-settled state, running the same post-finalization pipeline a + // finalizing vote would have run. Admin-only escape hatch. + ExecuteStuckOutbound(ctx context.Context, in *MsgExecuteStuckOutbound, opts ...grpc.CallOption) (*MsgExecuteStuckOutboundResponse, error) } type msgClient struct { @@ -904,15 +1034,6 @@ func (c *msgClient) ExecutePayload(ctx context.Context, in *MsgExecutePayload, o return out, nil } -func (c *msgClient) MigrateUEA(ctx context.Context, in *MsgMigrateUEA, opts ...grpc.CallOption) (*MsgMigrateUEAResponse, error) { - out := new(MsgMigrateUEAResponse) - err := c.cc.Invoke(ctx, "/uexecutor.v1.Msg/MigrateUEA", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *msgClient) VoteInbound(ctx context.Context, in *MsgVoteInbound, opts ...grpc.CallOption) (*MsgVoteInboundResponse, error) { out := new(MsgVoteInboundResponse) err := c.cc.Invoke(ctx, "/uexecutor.v1.Msg/VoteInbound", in, out, opts...) @@ -949,6 +1070,24 @@ func (c *msgClient) RevertStuckInbound(ctx context.Context, in *MsgRevertStuckIn return out, nil } +func (c *msgClient) ExecuteStuckInbound(ctx context.Context, in *MsgExecuteStuckInbound, opts ...grpc.CallOption) (*MsgExecuteStuckInboundResponse, error) { + out := new(MsgExecuteStuckInboundResponse) + err := c.cc.Invoke(ctx, "/uexecutor.v1.Msg/ExecuteStuckInbound", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *msgClient) ExecuteStuckOutbound(ctx context.Context, in *MsgExecuteStuckOutbound, opts ...grpc.CallOption) (*MsgExecuteStuckOutboundResponse, error) { + out := new(MsgExecuteStuckOutboundResponse) + err := c.cc.Invoke(ctx, "/uexecutor.v1.Msg/ExecuteStuckOutbound", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // MsgServer is the server API for Msg service. type MsgServer interface { // UpdateParams defines a governance operation for updating the parameters. @@ -957,8 +1096,6 @@ type MsgServer interface { UpdateParams(context.Context, *MsgUpdateParams) (*MsgUpdateParamsResponse, error) // ExecutePayload defines a message for executing a universal payload ExecutePayload(context.Context, *MsgExecutePayload) (*MsgExecutePayloadResponse, error) - // MigrateUEA defines a message for migrating UEA - MigrateUEA(context.Context, *MsgMigrateUEA) (*MsgMigrateUEAResponse, error) // VoteInbound defines a message for voting on synthetic assets bridging from external chain to PC VoteInbound(context.Context, *MsgVoteInbound) (*MsgVoteInboundResponse, error) // VoteOutbound defines a message for voting on a observed outbound tx on external chain @@ -969,6 +1106,15 @@ type MsgServer interface { // ballot has expired without finalizing, refunding the user on the source // chain via the normal revert/outbound flow. Admin-only escape hatch. RevertStuckInbound(context.Context, *MsgRevertStuckInbound) (*MsgRevertStuckInboundResponse, error) + // ExecuteStuckInbound finalizes an inbound ballot that is provably unable to + // finalize on its own yet already carries enough YES votes, then runs the + // normal post-finalization pipeline so the user receives funds on Push. + // Admin-only escape hatch, sibling of RevertStuckInbound. + ExecuteStuckInbound(context.Context, *MsgExecuteStuckInbound) (*MsgExecuteStuckInboundResponse, error) + // ExecuteStuckOutbound settles an outbound whose ballot can no longer reach a + // terminal-and-settled state, running the same post-finalization pipeline a + // finalizing vote would have run. Admin-only escape hatch. + ExecuteStuckOutbound(context.Context, *MsgExecuteStuckOutbound) (*MsgExecuteStuckOutboundResponse, error) } // UnimplementedMsgServer can be embedded to have forward compatible implementations. @@ -981,9 +1127,6 @@ func (*UnimplementedMsgServer) UpdateParams(ctx context.Context, req *MsgUpdateP func (*UnimplementedMsgServer) ExecutePayload(ctx context.Context, req *MsgExecutePayload) (*MsgExecutePayloadResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ExecutePayload not implemented") } -func (*UnimplementedMsgServer) MigrateUEA(ctx context.Context, req *MsgMigrateUEA) (*MsgMigrateUEAResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method MigrateUEA not implemented") -} func (*UnimplementedMsgServer) VoteInbound(ctx context.Context, req *MsgVoteInbound) (*MsgVoteInboundResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method VoteInbound not implemented") } @@ -996,6 +1139,12 @@ func (*UnimplementedMsgServer) VoteChainMeta(ctx context.Context, req *MsgVoteCh func (*UnimplementedMsgServer) RevertStuckInbound(ctx context.Context, req *MsgRevertStuckInbound) (*MsgRevertStuckInboundResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RevertStuckInbound not implemented") } +func (*UnimplementedMsgServer) ExecuteStuckInbound(ctx context.Context, req *MsgExecuteStuckInbound) (*MsgExecuteStuckInboundResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ExecuteStuckInbound not implemented") +} +func (*UnimplementedMsgServer) ExecuteStuckOutbound(ctx context.Context, req *MsgExecuteStuckOutbound) (*MsgExecuteStuckOutboundResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ExecuteStuckOutbound not implemented") +} func RegisterMsgServer(s grpc1.Server, srv MsgServer) { s.RegisterService(&_Msg_serviceDesc, srv) @@ -1037,38 +1186,20 @@ func _Msg_ExecutePayload_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } -func _Msg_MigrateUEA_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(MsgMigrateUEA) +func _Msg_VoteInbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgVoteInbound) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(MsgServer).MigrateUEA(ctx, in) + return srv.(MsgServer).VoteInbound(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/uexecutor.v1.Msg/MigrateUEA", + FullMethod: "/uexecutor.v1.Msg/VoteInbound", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(MsgServer).MigrateUEA(ctx, req.(*MsgMigrateUEA)) - } - return interceptor(ctx, in, info, handler) -} - -func _Msg_VoteInbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(MsgVoteInbound) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(MsgServer).VoteInbound(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/uexecutor.v1.Msg/VoteInbound", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(MsgServer).VoteInbound(ctx, req.(*MsgVoteInbound)) + return srv.(MsgServer).VoteInbound(ctx, req.(*MsgVoteInbound)) } return interceptor(ctx, in, info, handler) } @@ -1127,6 +1258,42 @@ func _Msg_RevertStuckInbound_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _Msg_ExecuteStuckInbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgExecuteStuckInbound) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServer).ExecuteStuckInbound(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/uexecutor.v1.Msg/ExecuteStuckInbound", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServer).ExecuteStuckInbound(ctx, req.(*MsgExecuteStuckInbound)) + } + return interceptor(ctx, in, info, handler) +} + +func _Msg_ExecuteStuckOutbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgExecuteStuckOutbound) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServer).ExecuteStuckOutbound(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/uexecutor.v1.Msg/ExecuteStuckOutbound", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServer).ExecuteStuckOutbound(ctx, req.(*MsgExecuteStuckOutbound)) + } + return interceptor(ctx, in, info, handler) +} + var _Msg_serviceDesc = grpc.ServiceDesc{ ServiceName: "uexecutor.v1.Msg", HandlerType: (*MsgServer)(nil), @@ -1139,10 +1306,6 @@ var _Msg_serviceDesc = grpc.ServiceDesc{ MethodName: "ExecutePayload", Handler: _Msg_ExecutePayload_Handler, }, - { - MethodName: "MigrateUEA", - Handler: _Msg_MigrateUEA_Handler, - }, { MethodName: "VoteInbound", Handler: _Msg_VoteInbound_Handler, @@ -1159,6 +1322,14 @@ var _Msg_serviceDesc = grpc.ServiceDesc{ MethodName: "RevertStuckInbound", Handler: _Msg_RevertStuckInbound_Handler, }, + { + MethodName: "ExecuteStuckInbound", + Handler: _Msg_ExecuteStuckInbound_Handler, + }, + { + MethodName: "ExecuteStuckOutbound", + Handler: _Msg_ExecuteStuckOutbound_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "uexecutor/v1/tx.proto", @@ -1311,90 +1482,6 @@ func (m *MsgExecutePayloadResponse) MarshalToSizedBuffer(dAtA []byte) (int, erro return len(dAtA) - i, nil } -func (m *MsgMigrateUEA) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *MsgMigrateUEA) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *MsgMigrateUEA) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.Signature) > 0 { - i -= len(m.Signature) - copy(dAtA[i:], m.Signature) - i = encodeVarintTx(dAtA, i, uint64(len(m.Signature))) - i-- - dAtA[i] = 0x22 - } - if m.MigrationPayload != nil { - { - size, err := m.MigrationPayload.MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintTx(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0x1a - } - if m.UniversalAccountId != nil { - { - size, err := m.UniversalAccountId.MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintTx(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0x12 - } - if len(m.Signer) > 0 { - i -= len(m.Signer) - copy(dAtA[i:], m.Signer) - i = encodeVarintTx(dAtA, i, uint64(len(m.Signer))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *MsgMigrateUEAResponse) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *MsgMigrateUEAResponse) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *MsgMigrateUEAResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - return len(dAtA) - i, nil -} - func (m *MsgVoteInbound) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -1688,115 +1775,239 @@ func (m *MsgRevertStuckInboundResponse) MarshalToSizedBuffer(dAtA []byte) (int, return len(dAtA) - i, nil } -func encodeVarintTx(dAtA []byte, offset int, v uint64) int { - offset -= sovTx(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *MsgUpdateParams) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Authority) - if l > 0 { - n += 1 + l + sovTx(uint64(l)) +func (m *MsgExecuteStuckInbound) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err } - l = m.Params.Size() - n += 1 + l + sovTx(uint64(l)) - return n + return dAtA[:n], nil } -func (m *MsgUpdateParamsResponse) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - return n +func (m *MsgExecuteStuckInbound) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) } -func (m *MsgExecutePayload) Size() (n int) { - if m == nil { - return 0 - } +func (m *MsgExecuteStuckInbound) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i var l int _ = l - l = len(m.Signer) - if l > 0 { - n += 1 + l + sovTx(uint64(l)) - } - if m.UniversalAccountId != nil { - l = m.UniversalAccountId.Size() - n += 1 + l + sovTx(uint64(l)) - } - if m.UniversalPayload != nil { - l = m.UniversalPayload.Size() - n += 1 + l + sovTx(uint64(l)) + if m.Inbound != nil { + { + size, err := m.Inbound.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 } - l = len(m.VerificationData) - if l > 0 { - n += 1 + l + sovTx(uint64(l)) + if len(m.Signer) > 0 { + i -= len(m.Signer) + copy(dAtA[i:], m.Signer) + i = encodeVarintTx(dAtA, i, uint64(len(m.Signer))) + i-- + dAtA[i] = 0xa } - return n + return len(dAtA) - i, nil } -func (m *MsgExecutePayloadResponse) Size() (n int) { - if m == nil { - return 0 +func (m *MsgExecuteStuckInboundResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err } - var l int - _ = l - return n + return dAtA[:n], nil } -func (m *MsgMigrateUEA) Size() (n int) { - if m == nil { - return 0 - } +func (m *MsgExecuteStuckInboundResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgExecuteStuckInboundResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i var l int _ = l - l = len(m.Signer) - if l > 0 { - n += 1 + l + sovTx(uint64(l)) - } - if m.UniversalAccountId != nil { - l = m.UniversalAccountId.Size() - n += 1 + l + sovTx(uint64(l)) - } - if m.MigrationPayload != nil { - l = m.MigrationPayload.Size() - n += 1 + l + sovTx(uint64(l)) - } - l = len(m.Signature) - if l > 0 { - n += 1 + l + sovTx(uint64(l)) + if len(m.UtxId) > 0 { + i -= len(m.UtxId) + copy(dAtA[i:], m.UtxId) + i = encodeVarintTx(dAtA, i, uint64(len(m.UtxId))) + i-- + dAtA[i] = 0xa } - return n + return len(dAtA) - i, nil } -func (m *MsgMigrateUEAResponse) Size() (n int) { - if m == nil { - return 0 +func (m *MsgExecuteStuckOutbound) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err } - var l int - _ = l - return n + return dAtA[:n], nil } -func (m *MsgVoteInbound) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l +func (m *MsgExecuteStuckOutbound) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgExecuteStuckOutbound) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.ObservedTx != nil { + { + size, err := m.ObservedTx.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + if len(m.UtxId) > 0 { + i -= len(m.UtxId) + copy(dAtA[i:], m.UtxId) + i = encodeVarintTx(dAtA, i, uint64(len(m.UtxId))) + i-- + dAtA[i] = 0x1a + } + if len(m.TxId) > 0 { + i -= len(m.TxId) + copy(dAtA[i:], m.TxId) + i = encodeVarintTx(dAtA, i, uint64(len(m.TxId))) + i-- + dAtA[i] = 0x12 + } + if len(m.Signer) > 0 { + i -= len(m.Signer) + copy(dAtA[i:], m.Signer) + i = encodeVarintTx(dAtA, i, uint64(len(m.Signer))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *MsgExecuteStuckOutboundResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgExecuteStuckOutboundResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgExecuteStuckOutboundResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.OutboundId) > 0 { + i -= len(m.OutboundId) + copy(dAtA[i:], m.OutboundId) + i = encodeVarintTx(dAtA, i, uint64(len(m.OutboundId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintTx(dAtA []byte, offset int, v uint64) int { + offset -= sovTx(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *MsgUpdateParams) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Authority) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = m.Params.Size() + n += 1 + l + sovTx(uint64(l)) + return n +} + +func (m *MsgUpdateParamsResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *MsgExecutePayload) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Signer) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + if m.UniversalAccountId != nil { + l = m.UniversalAccountId.Size() + n += 1 + l + sovTx(uint64(l)) + } + if m.UniversalPayload != nil { + l = m.UniversalPayload.Size() + n += 1 + l + sovTx(uint64(l)) + } + l = len(m.VerificationData) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + return n +} + +func (m *MsgExecutePayloadResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *MsgVoteInbound) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l l = len(m.Signer) if l > 0 { n += 1 + l + sovTx(uint64(l)) @@ -1917,6 +2128,74 @@ func (m *MsgRevertStuckInboundResponse) Size() (n int) { return n } +func (m *MsgExecuteStuckInbound) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Signer) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + if m.Inbound != nil { + l = m.Inbound.Size() + n += 1 + l + sovTx(uint64(l)) + } + return n +} + +func (m *MsgExecuteStuckInboundResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.UtxId) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + return n +} + +func (m *MsgExecuteStuckOutbound) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Signer) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = len(m.TxId) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = len(m.UtxId) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + if m.ObservedTx != nil { + l = m.ObservedTx.Size() + n += 1 + l + sovTx(uint64(l)) + } + return n +} + +func (m *MsgExecuteStuckOutboundResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.OutboundId) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + return n +} + func sovTx(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -2324,7 +2603,7 @@ func (m *MsgExecutePayloadResponse) Unmarshal(dAtA []byte) error { } return nil } -func (m *MsgMigrateUEA) Unmarshal(dAtA []byte) error { +func (m *MsgVoteInbound) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -2347,10 +2626,10 @@ func (m *MsgMigrateUEA) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: MsgMigrateUEA: wiretype end group for non-group") + return fmt.Errorf("proto: MsgVoteInbound: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: MsgMigrateUEA: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: MsgVoteInbound: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: @@ -2387,7 +2666,7 @@ func (m *MsgMigrateUEA) Unmarshal(dAtA []byte) error { iNdEx = postIndex case 2: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field UniversalAccountId", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Inbound", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -2414,259 +2693,23 @@ func (m *MsgMigrateUEA) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - if m.UniversalAccountId == nil { - m.UniversalAccountId = &UniversalAccountId{} + if m.Inbound == nil { + m.Inbound = &Inbound{} } - if err := m.UniversalAccountId.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + if err := m.Inbound.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field MigrationPayload", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTx - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthTx + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err } - postIndex := iNdEx + msglen - if postIndex < 0 { + if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthTx } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.MigrationPayload == nil { - m.MigrationPayload = &MigrationPayload{} - } - if err := m.MigrationPayload.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Signature", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTx - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthTx - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthTx - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Signature = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipTx(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthTx - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *MsgMigrateUEAResponse) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTx - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: MsgMigrateUEAResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: MsgMigrateUEAResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - default: - iNdEx = preIndex - skippy, err := skipTx(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthTx - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *MsgVoteInbound) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTx - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: MsgVoteInbound: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: MsgVoteInbound: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Signer", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTx - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthTx - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthTx - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Signer = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Inbound", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTx - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthTx - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthTx - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Inbound == nil { - m.Inbound = &Inbound{} - } - if err := m.Inbound.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipTx(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthTx - } - if (iNdEx + skippy) > l { + if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy @@ -3394,6 +3437,470 @@ func (m *MsgRevertStuckInboundResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *MsgExecuteStuckInbound) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgExecuteStuckInbound: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgExecuteStuckInbound: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Signer", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Signer = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Inbound", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Inbound == nil { + m.Inbound = &Inbound{} + } + if err := m.Inbound.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgExecuteStuckInboundResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgExecuteStuckInboundResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgExecuteStuckInboundResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field UtxId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.UtxId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgExecuteStuckOutbound) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgExecuteStuckOutbound: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgExecuteStuckOutbound: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Signer", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Signer = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TxId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.TxId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field UtxId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.UtxId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ObservedTx", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.ObservedTx == nil { + m.ObservedTx = &OutboundObservation{} + } + if err := m.ObservedTx.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgExecuteStuckOutboundResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgExecuteStuckOutboundResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgExecuteStuckOutboundResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field OutboundId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.OutboundId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipTx(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/uexecutor/types/types.pb.go b/x/uexecutor/types/types.pb.go index 4de72bb25..b5b40edcd 100644 --- a/x/uexecutor/types/types.pb.go +++ b/x/uexecutor/types/types.pb.go @@ -214,6 +214,12 @@ func (InboundTxTypeLegacy) EnumDescriptor() ([]byte, []int) { // Params defines the set of module parameters. type Params struct { SomeValue bool `protobuf:"varint,2,opt,name=some_value,json=someValue,proto3" json:"some_value,omitempty"` + // max_gasless_tx_gas is the maximum gas limit a fee-exempt (gasless) + // transaction is allowed to declare. Gasless transactions pay no fee, so + // their declared gas is not bounded by anything the sender has to spend; + // this cap is the only bound on how much a single gasless transaction can + // contribute to the block's cumulative gas wanted. + MaxGaslessTxGas uint64 `protobuf:"varint,3,opt,name=max_gasless_tx_gas,json=maxGaslessTxGas,proto3" json:"max_gasless_tx_gas,omitempty"` } func (m *Params) Reset() { *m = Params{} } @@ -255,6 +261,13 @@ func (m *Params) GetSomeValue() bool { return false } +func (m *Params) GetMaxGaslessTxGas() uint64 { + if m != nil { + return m.MaxGaslessTxGas + } + return 0 +} + // UniversalPayload mirrors the Solidity struct type UniversalPayload struct { To string `protobuf:"bytes,1,opt,name=to,proto3" json:"to,omitempty"` @@ -363,66 +376,6 @@ func (m *UniversalPayload) GetVType() VerificationType { return VerificationType_signedVerification } -// MigrationPayload mirrors the Solidity struct -type MigrationPayload struct { - Migration string `protobuf:"bytes,1,opt,name=migration,proto3" json:"migration,omitempty"` - Nonce string `protobuf:"bytes,2,opt,name=nonce,proto3" json:"nonce,omitempty"` - Deadline string `protobuf:"bytes,3,opt,name=deadline,proto3" json:"deadline,omitempty"` -} - -func (m *MigrationPayload) Reset() { *m = MigrationPayload{} } -func (*MigrationPayload) ProtoMessage() {} -func (*MigrationPayload) Descriptor() ([]byte, []int) { - return fileDescriptor_fab6d3ca71d1e2a5, []int{2} -} -func (m *MigrationPayload) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *MigrationPayload) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_MigrationPayload.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *MigrationPayload) XXX_Merge(src proto.Message) { - xxx_messageInfo_MigrationPayload.Merge(m, src) -} -func (m *MigrationPayload) XXX_Size() int { - return m.Size() -} -func (m *MigrationPayload) XXX_DiscardUnknown() { - xxx_messageInfo_MigrationPayload.DiscardUnknown(m) -} - -var xxx_messageInfo_MigrationPayload proto.InternalMessageInfo - -func (m *MigrationPayload) GetMigration() string { - if m != nil { - return m.Migration - } - return "" -} - -func (m *MigrationPayload) GetNonce() string { - if m != nil { - return m.Nonce - } - return "" -} - -func (m *MigrationPayload) GetDeadline() string { - if m != nil { - return m.Deadline - } - return "" -} - // UniversalAccountId is the identifier of a owner account type UniversalAccountId struct { ChainNamespace string `protobuf:"bytes,1,opt,name=chain_namespace,json=chainNamespace,proto3" json:"chain_namespace,omitempty"` @@ -433,7 +386,7 @@ type UniversalAccountId struct { func (m *UniversalAccountId) Reset() { *m = UniversalAccountId{} } func (*UniversalAccountId) ProtoMessage() {} func (*UniversalAccountId) Descriptor() ([]byte, []int) { - return fileDescriptor_fab6d3ca71d1e2a5, []int{3} + return fileDescriptor_fab6d3ca71d1e2a5, []int{2} } func (m *UniversalAccountId) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -491,7 +444,7 @@ func (m *RevertInstructions) Reset() { *m = RevertInstructions{} } func (m *RevertInstructions) String() string { return proto.CompactTextString(m) } func (*RevertInstructions) ProtoMessage() {} func (*RevertInstructions) Descriptor() ([]byte, []int) { - return fileDescriptor_fab6d3ca71d1e2a5, []int{4} + return fileDescriptor_fab6d3ca71d1e2a5, []int{3} } func (m *RevertInstructions) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -547,7 +500,7 @@ type Inbound struct { func (m *Inbound) Reset() { *m = Inbound{} } func (*Inbound) ProtoMessage() {} func (*Inbound) Descriptor() ([]byte, []int) { - return fileDescriptor_fab6d3ca71d1e2a5, []int{5} + return fileDescriptor_fab6d3ca71d1e2a5, []int{4} } func (m *Inbound) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -686,7 +639,7 @@ type PCTx struct { func (m *PCTx) Reset() { *m = PCTx{} } func (*PCTx) ProtoMessage() {} func (*PCTx) Descriptor() ([]byte, []int) { - return fileDescriptor_fab6d3ca71d1e2a5, []int{6} + return fileDescriptor_fab6d3ca71d1e2a5, []int{5} } func (m *PCTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -770,7 +723,7 @@ func (m *OutboundObservation) Reset() { *m = OutboundObservation{} } func (m *OutboundObservation) String() string { return proto.CompactTextString(m) } func (*OutboundObservation) ProtoMessage() {} func (*OutboundObservation) Descriptor() ([]byte, []int) { - return fileDescriptor_fab6d3ca71d1e2a5, []int{7} + return fileDescriptor_fab6d3ca71d1e2a5, []int{6} } func (m *OutboundObservation) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -850,7 +803,7 @@ func (m *OriginatingPcTx) Reset() { *m = OriginatingPcTx{} } func (m *OriginatingPcTx) String() string { return proto.CompactTextString(m) } func (*OriginatingPcTx) ProtoMessage() {} func (*OriginatingPcTx) Descriptor() ([]byte, []int) { - return fileDescriptor_fab6d3ca71d1e2a5, []int{8} + return fileDescriptor_fab6d3ca71d1e2a5, []int{7} } func (m *OriginatingPcTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -922,7 +875,7 @@ type OutboundTx struct { func (m *OutboundTx) Reset() { *m = OutboundTx{} } func (*OutboundTx) ProtoMessage() {} func (*OutboundTx) Descriptor() ([]byte, []int) { - return fileDescriptor_fab6d3ca71d1e2a5, []int{9} + return fileDescriptor_fab6d3ca71d1e2a5, []int{8} } func (m *OutboundTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1123,7 +1076,7 @@ type UniversalTx struct { func (m *UniversalTx) Reset() { *m = UniversalTx{} } func (*UniversalTx) ProtoMessage() {} func (*UniversalTx) Descriptor() ([]byte, []int) { - return fileDescriptor_fab6d3ca71d1e2a5, []int{10} + return fileDescriptor_fab6d3ca71d1e2a5, []int{9} } func (m *UniversalTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1204,7 +1157,7 @@ func (m *InboundLegacy) Reset() { *m = InboundLegacy{} } func (m *InboundLegacy) String() string { return proto.CompactTextString(m) } func (*InboundLegacy) ProtoMessage() {} func (*InboundLegacy) Descriptor() ([]byte, []int) { - return fileDescriptor_fab6d3ca71d1e2a5, []int{11} + return fileDescriptor_fab6d3ca71d1e2a5, []int{10} } func (m *InboundLegacy) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1315,7 +1268,7 @@ func (m *OutboundTxLegacy) Reset() { *m = OutboundTxLegacy{} } func (m *OutboundTxLegacy) String() string { return proto.CompactTextString(m) } func (*OutboundTxLegacy) ProtoMessage() {} func (*OutboundTxLegacy) Descriptor() ([]byte, []int) { - return fileDescriptor_fab6d3ca71d1e2a5, []int{12} + return fileDescriptor_fab6d3ca71d1e2a5, []int{11} } func (m *OutboundTxLegacy) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1390,7 +1343,7 @@ func (m *UniversalTxLegacy) Reset() { *m = UniversalTxLegacy{} } func (m *UniversalTxLegacy) String() string { return proto.CompactTextString(m) } func (*UniversalTxLegacy) ProtoMessage() {} func (*UniversalTxLegacy) Descriptor() ([]byte, []int) { - return fileDescriptor_fab6d3ca71d1e2a5, []int{13} + return fileDescriptor_fab6d3ca71d1e2a5, []int{12} } func (m *UniversalTxLegacy) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1455,7 +1408,6 @@ func init() { proto.RegisterEnum("uexecutor.v1.InboundTxTypeLegacy", InboundTxTypeLegacy_name, InboundTxTypeLegacy_value) proto.RegisterType((*Params)(nil), "uexecutor.v1.Params") proto.RegisterType((*UniversalPayload)(nil), "uexecutor.v1.UniversalPayload") - proto.RegisterType((*MigrationPayload)(nil), "uexecutor.v1.MigrationPayload") proto.RegisterType((*UniversalAccountId)(nil), "uexecutor.v1.UniversalAccountId") proto.RegisterType((*RevertInstructions)(nil), "uexecutor.v1.RevertInstructions") proto.RegisterType((*Inbound)(nil), "uexecutor.v1.Inbound") @@ -1472,129 +1424,128 @@ func init() { func init() { proto.RegisterFile("uexecutor/v1/types.proto", fileDescriptor_fab6d3ca71d1e2a5) } var fileDescriptor_fab6d3ca71d1e2a5 = []byte{ - // 1945 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x58, 0xcd, 0x6f, 0x23, 0x49, - 0x15, 0x4f, 0xfb, 0xdb, 0xcf, 0x99, 0xb8, 0x5d, 0x76, 0x92, 0x9e, 0x99, 0x8d, 0x93, 0x78, 0x77, - 0x99, 0x28, 0x68, 0x92, 0xd9, 0x00, 0x2b, 0x61, 0x09, 0x21, 0xc7, 0xe9, 0x64, 0x0d, 0x59, 0xdb, - 0xb4, 0xed, 0x30, 0xcb, 0xa5, 0x55, 0xe9, 0xae, 0x71, 0x5a, 0x1b, 0x77, 0xb7, 0xba, 0xdb, 0x49, - 0xe7, 0x8c, 0xb8, 0x20, 0x24, 0x38, 0xce, 0x71, 0x2e, 0x5c, 0xe0, 0xc2, 0x81, 0x3f, 0x62, 0x8f, - 0xcb, 0x0d, 0x89, 0x0b, 0x9a, 0x39, 0xc0, 0x7f, 0x01, 0xaa, 0xaa, 0xfe, 0x74, 0x9c, 0xd9, 0x45, - 0x1c, 0x90, 0xb8, 0x4c, 0xfa, 0xbd, 0x57, 0xf5, 0xea, 0x7d, 0xfc, 0xde, 0xaf, 0x6a, 0x0c, 0xd2, - 0x9c, 0xf8, 0x44, 0x9b, 0x7b, 0x96, 0x73, 0x78, 0xf3, 0xc9, 0xa1, 0x77, 0x67, 0x13, 0xf7, 0xc0, - 0x76, 0x2c, 0xcf, 0x42, 0xab, 0x91, 0xe5, 0xe0, 0xe6, 0x93, 0x27, 0x8d, 0xa9, 0x35, 0xb5, 0x98, - 0xe1, 0x90, 0x7e, 0xf1, 0x35, 0x4f, 0x6a, 0x78, 0x66, 0x98, 0xd6, 0x21, 0xfb, 0x97, 0xab, 0x5a, - 0xa7, 0x50, 0x18, 0x62, 0x07, 0xcf, 0x5c, 0xb4, 0x05, 0xe0, 0x5a, 0x33, 0xa2, 0xde, 0xe0, 0xeb, - 0x39, 0x91, 0x32, 0x3b, 0xc2, 0x5e, 0x49, 0x29, 0x53, 0xcd, 0x05, 0x55, 0xb4, 0xb7, 0x5e, 0xbf, - 0xd9, 0x5e, 0xf9, 0xe7, 0x9b, 0x6d, 0xe1, 0xd7, 0xff, 0xf8, 0xd3, 0xbe, 0x18, 0x87, 0x61, 0xb3, - 0xdd, 0xad, 0xbf, 0x65, 0x40, 0x9c, 0x98, 0xc6, 0x0d, 0x71, 0x5c, 0x7c, 0x3d, 0xc4, 0x77, 0xd7, - 0x16, 0xd6, 0xd1, 0x1a, 0x64, 0x3c, 0x4b, 0x12, 0x76, 0x84, 0xbd, 0xb2, 0x92, 0xf1, 0x2c, 0xd4, - 0x80, 0x7c, 0xec, 0xbd, 0xac, 0x70, 0x01, 0x21, 0xc8, 0xe9, 0xd8, 0xc3, 0x52, 0x96, 0x29, 0xd9, - 0x37, 0x7a, 0x0a, 0xe5, 0x29, 0x76, 0xd5, 0x6b, 0x63, 0x66, 0x78, 0x52, 0x8e, 0x19, 0x4a, 0x53, - 0xec, 0x9e, 0x53, 0x19, 0x7d, 0x0c, 0xd5, 0x19, 0xf6, 0xd5, 0x57, 0x84, 0xa8, 0x36, 0x71, 0xd4, - 0x29, 0x76, 0xa5, 0x3c, 0x5b, 0xb2, 0x3a, 0xc3, 0xfe, 0x29, 0x21, 0x43, 0xe2, 0x9c, 0x61, 0x17, - 0x7d, 0x0a, 0x12, 0x5d, 0x66, 0x3b, 0x86, 0xe5, 0x18, 0xde, 0x5d, 0x6a, 0x7d, 0x81, 0xad, 0x6f, - 0xcc, 0xb0, 0x3f, 0x0c, 0xcc, 0xf1, 0xbe, 0x06, 0xe4, 0x4d, 0xcb, 0xd4, 0x88, 0x54, 0xe4, 0x51, - 0x32, 0x01, 0x3d, 0x81, 0x92, 0x4e, 0xb0, 0x7e, 0x6d, 0x98, 0x44, 0x2a, 0xf1, 0x80, 0x42, 0x19, - 0xfd, 0x00, 0x0a, 0x37, 0x2a, 0x6d, 0x86, 0x54, 0xde, 0x11, 0xf6, 0xd6, 0x8e, 0x9a, 0x07, 0xc9, - 0x66, 0x1c, 0x5c, 0x10, 0xc7, 0x78, 0x65, 0x68, 0xd8, 0x33, 0x2c, 0x73, 0x7c, 0x67, 0x13, 0x25, - 0x7f, 0x43, 0xff, 0xb4, 0xf7, 0x92, 0x25, 0x7d, 0x1a, 0x97, 0x74, 0x1e, 0xd6, 0x51, 0xb5, 0x79, - 0x21, 0x5b, 0xbf, 0x11, 0x40, 0xfc, 0xdc, 0x98, 0x3a, 0xcc, 0x45, 0x58, 0xdd, 0x0f, 0xa0, 0x3c, - 0x0b, 0x75, 0x41, 0x91, 0x63, 0x45, 0x9c, 0x45, 0xe6, 0xa1, 0x2c, 0xb2, 0xe9, 0x2c, 0x1e, 0x0c, - 0x27, 0xf2, 0x19, 0x85, 0xf3, 0x5a, 0x00, 0x14, 0x35, 0xbb, 0xa3, 0x69, 0xd6, 0xdc, 0xf4, 0x7a, - 0x3a, 0x7a, 0x06, 0x55, 0xed, 0x0a, 0x1b, 0xa6, 0x6a, 0xe2, 0x19, 0x71, 0x6d, 0xac, 0x91, 0x20, - 0xac, 0x35, 0xa6, 0xee, 0x87, 0x5a, 0xf4, 0x18, 0x4a, 0x7c, 0xa1, 0xa1, 0x07, 0xe1, 0x15, 0x99, - 0xdc, 0xd3, 0x69, 0xd8, 0xd6, 0xad, 0x49, 0x9c, 0x20, 0x3a, 0x2e, 0x7c, 0x8b, 0x4a, 0x61, 0x1e, - 0x45, 0x4b, 0x03, 0xa4, 0x90, 0x1b, 0xe2, 0x78, 0x3d, 0xd3, 0xf5, 0x9c, 0xb9, 0x46, 0xe3, 0x76, - 0xd1, 0xc7, 0xb0, 0xf6, 0x6a, 0x6e, 0xea, 0xaa, 0x43, 0x34, 0xc3, 0x36, 0x88, 0xe9, 0x05, 0x81, - 0x3d, 0xa2, 0x5a, 0x25, 0x54, 0xb6, 0xbf, 0x13, 0x1e, 0xb1, 0x15, 0x1f, 0xe1, 0x30, 0x6f, 0xaa, - 0x91, 0x70, 0xd7, 0xfa, 0x7d, 0x0e, 0x8a, 0x3d, 0xf3, 0xd2, 0x9a, 0x9b, 0x3a, 0xda, 0x85, 0x55, - 0xd7, 0x9a, 0x3b, 0x1a, 0x51, 0x59, 0x0a, 0x81, 0xe3, 0x0a, 0xd7, 0x75, 0xa9, 0x0a, 0x6d, 0x42, - 0xd1, 0xf3, 0xd5, 0x2b, 0xec, 0x5e, 0x05, 0xd9, 0x16, 0x3c, 0xff, 0x33, 0xec, 0x5e, 0xa1, 0x0d, - 0x28, 0xb8, 0xc4, 0xd4, 0xa3, 0x6c, 0x03, 0x89, 0x76, 0x36, 0x8e, 0x94, 0xa3, 0x3f, 0x56, 0xd0, - 0x5d, 0x78, 0x46, 0x93, 0x0d, 0x50, 0x1f, 0x48, 0x74, 0x80, 0xb1, 0xeb, 0x12, 0x4f, 0xc5, 0xba, - 0xee, 0x04, 0x08, 0x2f, 0x33, 0x4d, 0x47, 0xd7, 0x1d, 0x3a, 0x52, 0xd7, 0xd6, 0x54, 0x35, 0x4c, - 0x9d, 0xf8, 0x01, 0xb4, 0x4b, 0xd7, 0xd6, 0xb4, 0x47, 0x65, 0xf4, 0x9c, 0x85, 0xc8, 0x20, 0x5c, - 0x62, 0x10, 0x6e, 0xa4, 0x21, 0x3c, 0xf6, 0x19, 0x70, 0x0b, 0x1e, 0xfb, 0x8b, 0x7e, 0x0a, 0xb5, - 0x7b, 0x20, 0x65, 0xd8, 0xaf, 0x2c, 0x62, 0x7f, 0x91, 0x13, 0x14, 0x71, 0xbe, 0xc8, 0x12, 0xdf, - 0x85, 0xda, 0x4d, 0x62, 0x42, 0x54, 0x46, 0x06, 0xc0, 0x02, 0x14, 0x93, 0x86, 0x13, 0x4a, 0x0c, - 0x3f, 0x83, 0xfa, 0x92, 0x8e, 0x48, 0x15, 0x76, 0xf6, 0x4e, 0xfa, 0xec, 0xfb, 0x40, 0x50, 0x90, - 0x73, 0x1f, 0x1c, 0x0d, 0xc8, 0x1b, 0x6e, 0x57, 0xee, 0x48, 0xab, 0x8c, 0xf3, 0xb8, 0x80, 0xb6, - 0xa1, 0xe2, 0xe0, 0xdb, 0x28, 0xb9, 0x47, 0x2c, 0x1e, 0x70, 0xf0, 0x6d, 0x18, 0xf6, 0x26, 0x14, - 0x0d, 0x57, 0xb5, 0xb5, 0xa3, 0x17, 0xd2, 0x1a, 0xdb, 0x58, 0x30, 0xdc, 0xa1, 0x76, 0xf4, 0xa2, - 0x5d, 0x0a, 0xc1, 0xda, 0xfa, 0x4a, 0x80, 0xdc, 0xb0, 0x3b, 0xf6, 0x93, 0x08, 0x10, 0x1e, 0x40, - 0x40, 0x26, 0x85, 0x80, 0xc7, 0x40, 0xe9, 0x4e, 0x9d, 0xbb, 0x44, 0x67, 0xd8, 0xc8, 0x29, 0xc5, - 0x29, 0x76, 0x27, 0x2e, 0x61, 0x80, 0xbb, 0xbc, 0xb6, 0xb4, 0x2f, 0xd5, 0x2b, 0x62, 0x4c, 0xaf, - 0x38, 0x3e, 0x72, 0x4a, 0x85, 0xe9, 0x3e, 0x63, 0x2a, 0xe6, 0xd5, 0xc3, 0xde, 0x3c, 0xe4, 0xb9, - 0x40, 0xa2, 0x10, 0x20, 0x8e, 0x63, 0x39, 0xea, 0xcc, 0x9d, 0x86, 0x10, 0x60, 0x8a, 0xcf, 0xdd, - 0x69, 0xfb, 0x83, 0xe4, 0x8c, 0x55, 0x13, 0x04, 0xaf, 0xa9, 0x9e, 0xdf, 0xfa, 0x55, 0x06, 0xea, - 0x83, 0xb9, 0xc7, 0x30, 0x3f, 0xb8, 0x74, 0x89, 0x73, 0xc3, 0x69, 0x46, 0x82, 0xa2, 0x3b, 0xd7, - 0x34, 0xe2, 0xba, 0x2c, 0xb3, 0x92, 0x12, 0x8a, 0xf7, 0xe2, 0xcc, 0xdc, 0x8f, 0x33, 0x51, 0x96, - 0x6c, 0xaa, 0x2c, 0xa9, 0x40, 0x73, 0xe9, 0x40, 0xd1, 0x0e, 0xac, 0xd2, 0xda, 0x50, 0x3a, 0x67, - 0xf5, 0xe1, 0x53, 0x00, 0x53, 0xec, 0x9e, 0x12, 0xc2, 0x4a, 0xf4, 0x02, 0x1a, 0xb4, 0x2f, 0xea, - 0xad, 0x83, 0x6d, 0xca, 0xf8, 0x74, 0x20, 0x68, 0x84, 0xbc, 0x1a, 0x88, 0xda, 0x7e, 0xce, 0x4d, - 0x1d, 0x6e, 0x69, 0x3f, 0x0b, 0x13, 0x6f, 0xc6, 0x89, 0x5b, 0x41, 0xba, 0xaa, 0x15, 0xe7, 0xdb, - 0x9a, 0x41, 0x75, 0xe0, 0x18, 0x53, 0xc3, 0xc4, 0x9e, 0x61, 0x4e, 0x87, 0xda, 0xfb, 0x9a, 0x9b, - 0x9a, 0xb8, 0x4c, 0x7a, 0xe2, 0xda, 0x1f, 0x2d, 0xa1, 0x33, 0x2b, 0xf6, 0xac, 0xf2, 0xb2, 0xbf, - 0x2d, 0x02, 0x84, 0x65, 0x1f, 0xfb, 0x74, 0x54, 0x74, 0xe2, 0x7a, 0x6c, 0x8d, 0x65, 0xa6, 0x18, - 0x47, 0x4c, 0x18, 0x38, 0xed, 0xa4, 0x58, 0x24, 0xf3, 0x30, 0x8b, 0x64, 0x53, 0x2c, 0x72, 0x00, - 0x75, 0xe2, 0x7b, 0xc4, 0x31, 0x29, 0xa9, 0xc6, 0x74, 0xc2, 0x9b, 0x50, 0x0b, 0x4d, 0x9d, 0x88, - 0x56, 0xf6, 0x40, 0xb4, 0x1d, 0x5a, 0xec, 0xc4, 0x62, 0xde, 0x91, 0x35, 0xa6, 0x8f, 0x57, 0xc6, - 0x58, 0x2f, 0xa4, 0xb0, 0x2e, 0x41, 0x31, 0x9c, 0x32, 0x8e, 0xc9, 0x50, 0x4c, 0xbf, 0x02, 0x4a, - 0x0b, 0xaf, 0x80, 0x04, 0x65, 0x95, 0xbf, 0x05, 0x65, 0x1d, 0x41, 0x9e, 0x95, 0x94, 0x31, 0x4b, - 0xe5, 0x68, 0x2b, 0xbd, 0x78, 0xa1, 0xa7, 0x4a, 0xce, 0xa6, 0x9d, 0x3d, 0x86, 0x0a, 0xef, 0x3d, - 0xd1, 0xe9, 0x4e, 0x4e, 0x32, 0xbb, 0x0b, 0x3b, 0xef, 0x0f, 0x85, 0x02, 0xe1, 0xae, 0xb1, 0x4f, - 0xdf, 0x40, 0x86, 0xce, 0xa8, 0xa5, 0xac, 0x64, 0x0c, 0x1d, 0xfd, 0x08, 0xaa, 0x11, 0xb0, 0x82, - 0x21, 0x7d, 0xb4, 0x2c, 0xfc, 0x11, 0xb3, 0x29, 0x6b, 0xe1, 0x62, 0x2e, 0x3f, 0xc4, 0x7f, 0x6b, - 0xff, 0x05, 0xff, 0x1d, 0x43, 0xdd, 0xd6, 0xd4, 0xc0, 0x2b, 0xdf, 0x4f, 0x5f, 0x14, 0x55, 0xe6, - 0x12, 0xa5, 0x5d, 0x52, 0x36, 0x53, 0x6a, 0xb6, 0xc6, 0x5d, 0xcb, 0xe1, 0xe2, 0xb0, 0x53, 0xb6, - 0x63, 0x68, 0x44, 0x12, 0xa3, 0x4e, 0x0d, 0xa9, 0x4c, 0x07, 0x24, 0x18, 0x58, 0xa9, 0xc6, 0x3b, - 0xcf, 0x67, 0x35, 0x3a, 0x99, 0xdd, 0xcd, 0xf1, 0xc9, 0xe8, 0xfd, 0x27, 0xd3, 0xd5, 0xf1, 0xc9, - 0xfb, 0x50, 0x0b, 0x1c, 0xb8, 0xb7, 0xd8, 0x56, 0x19, 0x4b, 0x48, 0x75, 0x76, 0x4c, 0x95, 0x1b, - 0x46, 0xb7, 0xd8, 0x96, 0xa9, 0x3a, 0x8c, 0xd2, 0xb3, 0xbe, 0x24, 0xa6, 0xd4, 0x88, 0xa2, 0x1c, - 0x53, 0x99, 0xf2, 0x15, 0xbe, 0xb4, 0x1c, 0x4f, 0x75, 0x08, 0x76, 0x2d, 0x53, 0x5a, 0xe7, 0x17, - 0x39, 0xd3, 0x29, 0x4c, 0x95, 0xa4, 0xfc, 0x8d, 0x24, 0xe5, 0xa3, 0x23, 0x58, 0x67, 0x84, 0xa3, - 0x59, 0xa6, 0xe7, 0x60, 0xcd, 0x8b, 0x18, 0x67, 0x93, 0x39, 0xa9, 0x53, 0x63, 0x37, 0xb0, 0x85, - 0x94, 0x13, 0x5f, 0x13, 0xff, 0x12, 0xa0, 0x12, 0xdd, 0x93, 0x11, 0x64, 0x84, 0x08, 0x32, 0xdf, - 0x07, 0x30, 0xf8, 0x6b, 0x83, 0xa2, 0x30, 0xc3, 0xaa, 0xb3, 0x9e, 0xae, 0x4e, 0xf0, 0x1a, 0x51, - 0xca, 0xc1, 0xc2, 0xb1, 0x8f, 0x9e, 0x85, 0x80, 0xcf, 0xee, 0x64, 0x1f, 0x28, 0x27, 0x47, 0xf9, - 0x0f, 0xa1, 0x12, 0x21, 0xd2, 0xf3, 0xa5, 0x1c, 0x5b, 0x2e, 0x2d, 0x47, 0xf9, 0xd8, 0x57, 0xc0, - 0x8a, 0xf9, 0x68, 0x17, 0x56, 0x43, 0xdc, 0xb0, 0xba, 0xf3, 0xc1, 0xae, 0x70, 0x1d, 0xab, 0x79, - 0xfb, 0xc3, 0xe4, 0xb5, 0xb2, 0xb1, 0xec, 0xe9, 0xe6, 0xf9, 0xad, 0x3f, 0x66, 0xe1, 0x51, 0x90, - 0xc2, 0x39, 0x99, 0x62, 0xed, 0xee, 0xff, 0xe4, 0x59, 0xd5, 0x5e, 0x7c, 0x56, 0xed, 0x2e, 0x6d, - 0x1b, 0xa7, 0x2a, 0x9e, 0xf9, 0xff, 0xfe, 0x8d, 0xd5, 0x6e, 0xbe, 0x7e, 0xb3, 0x2d, 0x84, 0x2d, - 0xab, 0xc5, 0x2d, 0x0b, 0xb0, 0xd5, 0xfa, 0x8b, 0x00, 0x62, 0x0c, 0x88, 0xa0, 0x61, 0xff, 0xd1, - 0xd5, 0xf4, 0x60, 0xeb, 0x52, 0x2d, 0xca, 0x3e, 0xdc, 0xa2, 0xdc, 0x7b, 0x5a, 0x94, 0x5f, 0x68, - 0x51, 0xbb, 0x95, 0xcc, 0x67, 0x7d, 0xc9, 0x05, 0xef, 0xf9, 0xad, 0x3f, 0x64, 0xa0, 0x96, 0x98, - 0xc1, 0x20, 0xa9, 0x76, 0x6a, 0xf2, 0x04, 0x56, 0xfc, 0xa7, 0x4b, 0x5b, 0x18, 0x34, 0x6f, 0xd9, - 0xfc, 0x65, 0xbe, 0x61, 0xfe, 0x7e, 0x9c, 0x9e, 0xbf, 0xec, 0xb2, 0x16, 0x2f, 0x96, 0x3b, 0x35, - 0x85, 0x3f, 0x81, 0xb8, 0xe1, 0xe1, 0x9d, 0x92, 0x63, 0x70, 0xdb, 0x7e, 0x00, 0x28, 0x63, 0x3f, - 0xb8, 0x5e, 0xaa, 0xd1, 0x46, 0xae, 0x60, 0xe3, 0x2a, 0x7c, 0xc3, 0xb8, 0xee, 0x9f, 0x81, 0xb8, - 0xf8, 0x7f, 0x5a, 0xb4, 0x01, 0xc8, 0x35, 0xa6, 0x26, 0xd1, 0x93, 0x16, 0x71, 0x05, 0x3d, 0x85, - 0xcd, 0x79, 0x7c, 0x6c, 0xca, 0x28, 0xec, 0xff, 0x32, 0x5d, 0xf5, 0xe0, 0x8e, 0xfb, 0x10, 0xb6, - 0x27, 0xfd, 0xde, 0x85, 0xac, 0x8c, 0x3a, 0xe7, 0xea, 0xf8, 0xa5, 0x3a, 0x1a, 0x77, 0xc6, 0x93, - 0x91, 0x3a, 0xe9, 0x8f, 0x86, 0x72, 0xb7, 0x77, 0xda, 0x93, 0x4f, 0xc4, 0x15, 0x54, 0x87, 0x6a, - 0xaf, 0x7f, 0x3c, 0x98, 0xf4, 0x4f, 0xd4, 0xd1, 0xa4, 0xdb, 0x95, 0x47, 0x23, 0x51, 0x40, 0x5b, - 0xf0, 0x78, 0x28, 0xf7, 0x4f, 0x7a, 0xfd, 0x33, 0x35, 0x34, 0xca, 0x2f, 0xe5, 0xee, 0x64, 0xdc, - 0x1b, 0xf4, 0xc5, 0x0c, 0xda, 0x84, 0xfa, 0xb0, 0x1b, 0x68, 0xe4, 0x78, 0x5f, 0x96, 0x06, 0x9f, - 0x34, 0x9c, 0x76, 0x7a, 0xe7, 0xf2, 0x89, 0x98, 0x43, 0xeb, 0x50, 0x1b, 0x76, 0xd5, 0xd0, 0xa5, - 0x22, 0x5f, 0xc8, 0xca, 0x58, 0xcc, 0xa3, 0x06, 0x88, 0x83, 0xc9, 0x98, 0xfb, 0x0f, 0x8c, 0x62, - 0x21, 0xa5, 0x0d, 0x5d, 0x17, 0x69, 0x9c, 0x91, 0x36, 0xf0, 0x5b, 0x42, 0xab, 0x50, 0xea, 0x76, - 0xfa, 0x5d, 0x99, 0x4a, 0xe5, 0xfd, 0x01, 0x14, 0x82, 0xcc, 0xab, 0x50, 0x49, 0x67, 0x59, 0x81, - 0x62, 0x78, 0x80, 0x40, 0x77, 0x0d, 0x8e, 0x47, 0xb2, 0x72, 0x21, 0x9f, 0x88, 0x19, 0x2a, 0xf1, - 0x80, 0xe4, 0x13, 0x31, 0x4b, 0x17, 0x76, 0x8e, 0x07, 0x4c, 0xc8, 0xed, 0xff, 0x56, 0x80, 0x02, - 0xe7, 0x14, 0x84, 0x60, 0x2d, 0xe1, 0x51, 0x1d, 0xbf, 0x14, 0x57, 0x50, 0x11, 0xb2, 0x67, 0x1d, - 0x5a, 0xae, 0x3a, 0x54, 0xcf, 0x3a, 0x23, 0xb5, 0x43, 0xd3, 0xe8, 0x7c, 0x71, 0x3e, 0xe8, 0x50, - 0xbf, 0x65, 0xc8, 0x9f, 0x4e, 0xfa, 0x27, 0xb4, 0x2c, 0xeb, 0x50, 0x63, 0x9f, 0xa9, 0x15, 0x39, - 0x16, 0x54, 0x20, 0xe4, 0xe9, 0x01, 0x61, 0xa9, 0x83, 0xfa, 0x14, 0x90, 0x08, 0xab, 0x8a, 0x3c, - 0xea, 0x4e, 0x64, 0x95, 0x7b, 0x2a, 0xee, 0xff, 0x59, 0x80, 0xfa, 0x12, 0xb2, 0x43, 0xbb, 0xb0, - 0x15, 0xee, 0x3e, 0x97, 0xcf, 0x3a, 0xdd, 0x2f, 0xd4, 0x7b, 0xd1, 0x6e, 0x00, 0x5a, 0x58, 0xc2, - 0x83, 0x97, 0xa0, 0xb1, 0xa0, 0xe7, 0x87, 0x65, 0xd0, 0x47, 0xb0, 0xb3, 0xcc, 0x92, 0xca, 0x22, - 0x8b, 0x5a, 0xd0, 0xbc, 0xef, 0x37, 0x9d, 0xe9, 0xf1, 0xf0, 0xab, 0xb7, 0x4d, 0xe1, 0xeb, 0xb7, - 0x4d, 0xe1, 0xef, 0x6f, 0x9b, 0xc2, 0xef, 0xde, 0x35, 0x57, 0xbe, 0x7e, 0xd7, 0x5c, 0xf9, 0xeb, - 0xbb, 0xe6, 0xca, 0x2f, 0x3e, 0x9d, 0x1a, 0xde, 0xd5, 0xfc, 0xf2, 0x40, 0xb3, 0x66, 0x87, 0xf6, - 0xdc, 0xbd, 0x62, 0x7c, 0xc7, 0xbe, 0x9e, 0xb3, 0xcf, 0xe7, 0xa6, 0xa5, 0x93, 0x43, 0xff, 0x30, - 0x9e, 0x20, 0xf6, 0x63, 0xdd, 0x65, 0x81, 0xfd, 0xec, 0xf6, 0xbd, 0x7f, 0x07, 0x00, 0x00, 0xff, - 0xff, 0x93, 0x36, 0x0a, 0x95, 0xc9, 0x13, 0x00, 0x00, + // 1936 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x58, 0xbd, 0x6f, 0xe3, 0xc8, + 0x15, 0x37, 0xf5, 0xad, 0x27, 0xaf, 0x45, 0x8d, 0x64, 0x9b, 0xbb, 0x7b, 0x96, 0x6d, 0xdd, 0x5d, + 0xd6, 0xf0, 0x61, 0xed, 0x3d, 0x27, 0x39, 0x20, 0x02, 0x82, 0x40, 0x96, 0x69, 0x9f, 0x12, 0x47, + 0x52, 0x28, 0xc9, 0xd9, 0x4b, 0x43, 0x8c, 0xc9, 0x59, 0x99, 0x38, 0x8b, 0x14, 0x38, 0x94, 0x4d, + 0xd7, 0x41, 0x9a, 0x34, 0x49, 0xb9, 0xe5, 0x36, 0x69, 0x92, 0x26, 0x45, 0xfe, 0x88, 0x2b, 0x2f, + 0x5d, 0x80, 0x34, 0xc1, 0x6e, 0x91, 0xfc, 0x17, 0x09, 0x66, 0x86, 0x14, 0x49, 0x59, 0xde, 0xbb, + 0x20, 0x45, 0x80, 0x6b, 0xd6, 0x7c, 0x1f, 0x7c, 0xf3, 0x3e, 0x7e, 0xef, 0x37, 0x5c, 0x81, 0x32, + 0x23, 0x3e, 0x31, 0x66, 0x9e, 0xe3, 0x1e, 0xde, 0x7c, 0x7a, 0xe8, 0xdd, 0x4d, 0x09, 0x3d, 0x98, + 0xba, 0x8e, 0xe7, 0xa0, 0xd5, 0xb9, 0xe5, 0xe0, 0xe6, 0xd3, 0x27, 0xb5, 0xb1, 0x33, 0x76, 0xb8, + 0xe1, 0x90, 0x3d, 0x09, 0x9f, 0x27, 0x15, 0x3c, 0xb1, 0x6c, 0xe7, 0x90, 0xff, 0x2b, 0x54, 0x0d, + 0x0a, 0xb9, 0x3e, 0x76, 0xf1, 0x84, 0xa2, 0x2d, 0x00, 0xea, 0x4c, 0x88, 0x7e, 0x83, 0xaf, 0x67, + 0x44, 0x49, 0xed, 0x48, 0x7b, 0x05, 0xad, 0xc8, 0x34, 0x17, 0x4c, 0x81, 0x3e, 0x01, 0x34, 0xc1, + 0xbe, 0x3e, 0xc6, 0xf4, 0x9a, 0x50, 0xaa, 0x7b, 0xfc, 0x51, 0x49, 0xef, 0x48, 0x7b, 0x19, 0xad, + 0x3c, 0xc1, 0xfe, 0x99, 0x30, 0x0c, 0xd9, 0x43, 0x73, 0xeb, 0xf5, 0x9b, 0xed, 0x95, 0x7f, 0xbd, + 0xd9, 0x96, 0x7e, 0xfb, 0xcf, 0x3f, 0xef, 0xcb, 0x51, 0xce, 0x53, 0x7e, 0x54, 0xe3, 0xef, 0x29, + 0x90, 0x47, 0xb6, 0x75, 0x43, 0x5c, 0x8a, 0xaf, 0xfb, 0xf8, 0xee, 0xda, 0xc1, 0x26, 0x5a, 0x83, + 0x94, 0xe7, 0x28, 0xd2, 0x8e, 0xb4, 0x57, 0xd4, 0x52, 0x9e, 0x83, 0x6a, 0x90, 0x8d, 0x52, 0x29, + 0x6a, 0x42, 0x40, 0x08, 0x32, 0x26, 0xf6, 0x30, 0x3f, 0xb8, 0xa8, 0xf1, 0x67, 0xf4, 0x14, 0x8a, + 0x63, 0x4c, 0xf5, 0x6b, 0x6b, 0x62, 0x79, 0x4a, 0x86, 0x1b, 0x0a, 0x63, 0x4c, 0xcf, 0x99, 0x8c, + 0x3e, 0x06, 0x96, 0x9d, 0xfe, 0x8a, 0x10, 0x7d, 0x4a, 0x5c, 0x9e, 0x74, 0x96, 0xbb, 0xac, 0x4e, + 0xb0, 0x7f, 0x4a, 0x48, 0x9f, 0xb8, 0x67, 0x98, 0xa2, 0xcf, 0x40, 0x61, 0x6e, 0x53, 0xd7, 0x72, + 0x5c, 0xcb, 0xbb, 0x4b, 0xf8, 0xe7, 0xb8, 0x7f, 0x6d, 0x82, 0xfd, 0x7e, 0x60, 0x8e, 0xde, 0xab, + 0x41, 0xd6, 0x76, 0x6c, 0x83, 0x28, 0x79, 0x91, 0x25, 0x17, 0xd0, 0x13, 0x28, 0x98, 0x04, 0x9b, + 0xd7, 0x96, 0x4d, 0x94, 0x82, 0x48, 0x28, 0x94, 0xd1, 0x0f, 0x21, 0x77, 0xa3, 0xb3, 0xc9, 0x29, + 0xc5, 0x1d, 0x69, 0x6f, 0xed, 0xa8, 0x7e, 0x10, 0x9f, 0xdc, 0xc1, 0x05, 0x71, 0xad, 0x57, 0x96, + 0x81, 0x3d, 0xcb, 0xb1, 0x87, 0x77, 0x53, 0xa2, 0x65, 0x6f, 0xd8, 0x9f, 0xe6, 0x5e, 0xbc, 0xa5, + 0x4f, 0xa3, 0x96, 0xce, 0xc2, 0x3e, 0xea, 0x53, 0xd1, 0xc8, 0xc6, 0x6b, 0x09, 0xd0, 0xbc, 0xbb, + 0x2d, 0xc3, 0x70, 0x66, 0xb6, 0xd7, 0x31, 0xd1, 0x33, 0x28, 0x1b, 0x57, 0xd8, 0xb2, 0x75, 0x1b, + 0x4f, 0x08, 0x9d, 0x62, 0x83, 0x04, 0xcd, 0x5e, 0xe3, 0xea, 0x6e, 0xa8, 0x45, 0x8f, 0xa1, 0x20, + 0x1c, 0x2d, 0x33, 0xe8, 0x7d, 0x9e, 0xcb, 0x1d, 0x93, 0x55, 0xeb, 0xdc, 0xda, 0xc4, 0x0d, 0xda, + 0x2f, 0x84, 0x6f, 0x91, 0x1a, 0x16, 0x59, 0x34, 0x0c, 0x40, 0x1a, 0xb9, 0x21, 0xae, 0xd7, 0xb1, + 0xa9, 0xe7, 0xce, 0x0c, 0x56, 0x24, 0x45, 0x1f, 0xc3, 0xda, 0xab, 0x99, 0x6d, 0xea, 0x2e, 0x31, + 0xac, 0xa9, 0x45, 0x6c, 0x2f, 0x48, 0xec, 0x11, 0xd3, 0x6a, 0xa1, 0xb2, 0xf9, 0xbd, 0xf0, 0x88, + 0xad, 0xe8, 0x08, 0x97, 0x47, 0xd3, 0xad, 0x58, 0xb8, 0xc6, 0x1f, 0x32, 0x90, 0xef, 0xd8, 0x97, + 0xce, 0xcc, 0x36, 0xd1, 0x2e, 0xac, 0x52, 0x67, 0xe6, 0x1a, 0x44, 0xe7, 0x25, 0x04, 0x81, 0x4b, + 0x42, 0xd7, 0x66, 0x2a, 0xb4, 0x09, 0x79, 0xcf, 0xd7, 0xaf, 0x30, 0xbd, 0x0a, 0xaa, 0xcd, 0x79, + 0xfe, 0xe7, 0x98, 0x5e, 0xa1, 0x0d, 0xc8, 0x51, 0x62, 0x9b, 0xf3, 0x6a, 0x03, 0x09, 0x7d, 0x00, + 0xc5, 0x28, 0x53, 0x01, 0xb7, 0x48, 0xc1, 0xde, 0xc2, 0x13, 0x56, 0x6c, 0x00, 0xb3, 0x40, 0x62, + 0xeb, 0x85, 0x29, 0x25, 0x9e, 0x8e, 0x4d, 0xd3, 0x0d, 0x20, 0x55, 0xe4, 0x9a, 0x96, 0x69, 0xba, + 0x0c, 0xc3, 0xd7, 0xce, 0x58, 0xb7, 0x6c, 0x93, 0xf8, 0x01, 0x96, 0x0a, 0xd7, 0xce, 0xb8, 0xc3, + 0x64, 0xf4, 0x9c, 0xa7, 0xc8, 0x31, 0x53, 0xe0, 0x98, 0xa9, 0x25, 0x31, 0x33, 0xf4, 0x39, 0x52, + 0x72, 0x1e, 0xff, 0x8b, 0x7e, 0x06, 0x95, 0x7b, 0xa8, 0xe0, 0x60, 0x2b, 0x2d, 0x82, 0x6d, 0x71, + 0x09, 0x35, 0x79, 0xb6, 0xb8, 0x96, 0x9f, 0x40, 0xe5, 0x26, 0x06, 0x49, 0x9d, 0x6f, 0x1f, 0xf0, + 0x04, 0xe5, 0xb8, 0xe1, 0x84, 0x6d, 0xe2, 0x2f, 0xa0, 0xba, 0x64, 0x22, 0x4a, 0x89, 0x9f, 0xbd, + 0x93, 0x3c, 0xfb, 0x3e, 0x10, 0x34, 0xe4, 0xde, 0x07, 0x47, 0x0d, 0xb2, 0x16, 0x6d, 0xab, 0x2d, + 0x65, 0x95, 0x33, 0x92, 0x10, 0xd0, 0x36, 0x94, 0x5c, 0x7c, 0x3b, 0x2f, 0xee, 0x11, 0xcf, 0x07, + 0x5c, 0x7c, 0x1b, 0xa6, 0xbd, 0x09, 0x79, 0x8b, 0xea, 0x53, 0xe3, 0xe8, 0x85, 0xb2, 0xc6, 0x5f, + 0xcc, 0x59, 0xb4, 0x6f, 0x1c, 0xbd, 0x68, 0x16, 0x42, 0xb0, 0x36, 0xbe, 0x92, 0x20, 0xd3, 0x6f, + 0x0f, 0xfd, 0x38, 0x02, 0xa4, 0x07, 0x10, 0x90, 0x4a, 0x20, 0xe0, 0x31, 0x30, 0x7e, 0xd1, 0x67, + 0x94, 0x98, 0x01, 0x03, 0xe6, 0xc7, 0x98, 0x8e, 0x28, 0xe1, 0x80, 0xbb, 0xbc, 0x76, 0x8c, 0x2f, + 0xf5, 0x2b, 0x62, 0x8d, 0xaf, 0x04, 0x3e, 0x32, 0x5a, 0x89, 0xeb, 0x3e, 0xe7, 0x2a, 0x1e, 0xd5, + 0xc3, 0xde, 0x2c, 0x24, 0x96, 0x40, 0x62, 0x10, 0x20, 0xae, 0xeb, 0xb8, 0xfa, 0x84, 0x8e, 0x43, + 0x08, 0x70, 0xc5, 0xcf, 0xe9, 0xb8, 0xf9, 0x41, 0x7c, 0xc7, 0xca, 0x31, 0x46, 0x35, 0x74, 0xcf, + 0x6f, 0xfc, 0x26, 0x05, 0xd5, 0xde, 0xcc, 0xe3, 0x98, 0xef, 0x5d, 0x52, 0xe2, 0xde, 0xf0, 0x99, + 0x20, 0x05, 0xf2, 0x74, 0x66, 0x18, 0x84, 0x52, 0x5e, 0x59, 0x41, 0x0b, 0xc5, 0x7b, 0x79, 0xa6, + 0xee, 0xe7, 0x19, 0x6b, 0x4b, 0x3a, 0xd1, 0x96, 0x44, 0xa2, 0x99, 0x64, 0xa2, 0x68, 0x07, 0x56, + 0x59, 0x6f, 0x18, 0x7f, 0xf2, 0xfe, 0x88, 0x2d, 0x80, 0x31, 0xa6, 0xa7, 0x84, 0xf0, 0x16, 0xbd, + 0x80, 0x1a, 0x9b, 0x8b, 0x7e, 0xeb, 0xe2, 0x29, 0xa3, 0x58, 0xb6, 0x10, 0x2c, 0x43, 0xd1, 0x0d, + 0xc4, 0x6c, 0xbf, 0x14, 0xa6, 0x96, 0xb0, 0x34, 0x9f, 0x85, 0x85, 0xd7, 0xa3, 0xc2, 0x9d, 0xa0, + 0x5c, 0xdd, 0x89, 0xea, 0x6d, 0x4c, 0xa0, 0xdc, 0x73, 0xad, 0xb1, 0x65, 0x63, 0xcf, 0xb2, 0xc7, + 0x7d, 0xe3, 0x7d, 0xc3, 0x4d, 0x6c, 0x5c, 0x2a, 0xb9, 0x71, 0xcd, 0x8f, 0x96, 0xd0, 0x99, 0x13, + 0x45, 0xd6, 0x45, 0xdb, 0xdf, 0xe6, 0x01, 0xc2, 0xb6, 0x0f, 0x7d, 0xb6, 0x2a, 0x26, 0xa1, 0x1e, + 0xf7, 0x71, 0xec, 0x04, 0xe3, 0xc8, 0x31, 0x83, 0xa0, 0x9d, 0x04, 0x8b, 0xa4, 0x1e, 0x66, 0x91, + 0x74, 0x82, 0x45, 0x0e, 0xa0, 0x4a, 0x7c, 0x8f, 0xb8, 0x36, 0x23, 0xd5, 0x88, 0x4e, 0xc4, 0x10, + 0x2a, 0xa1, 0xa9, 0x35, 0xa7, 0x95, 0x3d, 0x90, 0xa7, 0x2e, 0x6b, 0x76, 0xcc, 0x59, 0x4c, 0x64, + 0x8d, 0xeb, 0x23, 0xcf, 0x08, 0xeb, 0xb9, 0x04, 0xd6, 0x15, 0xc8, 0x87, 0x5b, 0x26, 0x30, 0x19, + 0x8a, 0xc9, 0x6b, 0xb7, 0xb0, 0x70, 0xed, 0xc6, 0x28, 0xab, 0xf8, 0x2d, 0x28, 0xeb, 0x08, 0xb2, + 0xbc, 0xa5, 0x9c, 0x59, 0x4a, 0x47, 0x5b, 0x49, 0xe7, 0x85, 0x99, 0x6a, 0x99, 0x29, 0x9b, 0xec, + 0x31, 0x94, 0xc4, 0xec, 0x89, 0xc9, 0xde, 0x14, 0x24, 0xb3, 0xbb, 0xf0, 0xe6, 0xfd, 0xa5, 0xd0, + 0x20, 0x7c, 0x6b, 0xe8, 0xb3, 0x8f, 0x0e, 0xcb, 0xe4, 0xd4, 0x52, 0xd4, 0x52, 0x96, 0x89, 0x7e, + 0x0c, 0xe5, 0x39, 0xb0, 0x82, 0x25, 0x7d, 0xb4, 0x2c, 0xfd, 0x01, 0xb7, 0x69, 0x6b, 0xa1, 0xb3, + 0x90, 0x1f, 0xe2, 0xbf, 0xb5, 0xff, 0x81, 0xff, 0x8e, 0xa1, 0x3a, 0x35, 0xf4, 0x20, 0xaa, 0x78, + 0xdf, 0x72, 0x6c, 0xa5, 0xcc, 0x43, 0xa2, 0x64, 0x48, 0xc6, 0x66, 0x5a, 0x65, 0x6a, 0x88, 0xd0, + 0x6a, 0xe8, 0x1c, 0x4e, 0x6a, 0xea, 0x5a, 0x06, 0x51, 0xe4, 0xf9, 0xa4, 0xfa, 0x4c, 0x66, 0x0b, + 0x12, 0x2c, 0xac, 0x52, 0x11, 0x93, 0x17, 0xbb, 0x3a, 0x3f, 0x99, 0xdf, 0xcd, 0xd1, 0xc9, 0xe8, + 0xfd, 0x27, 0x33, 0xef, 0xe8, 0xe4, 0x7d, 0xa8, 0x04, 0x01, 0xe8, 0x2d, 0x9e, 0xea, 0x9c, 0x25, + 0x94, 0x2a, 0x3f, 0xa6, 0x2c, 0x0c, 0x83, 0x5b, 0x3c, 0x55, 0x99, 0x3a, 0xcc, 0xd2, 0x73, 0xbe, + 0x24, 0xb6, 0x52, 0x9b, 0x67, 0x39, 0x64, 0x32, 0xe3, 0x2b, 0x7c, 0xe9, 0xb8, 0x9e, 0xee, 0x12, + 0x4c, 0x1d, 0x5b, 0x59, 0x17, 0x17, 0x39, 0xd7, 0x69, 0x5c, 0x15, 0xa7, 0xfc, 0x8d, 0x38, 0xe5, + 0xa3, 0x23, 0x58, 0xe7, 0x84, 0x63, 0x38, 0xb6, 0xe7, 0x62, 0xc3, 0x9b, 0x33, 0xce, 0x26, 0x0f, + 0x52, 0x65, 0xc6, 0x76, 0x60, 0x0b, 0x29, 0x27, 0xba, 0x26, 0xfe, 0x2d, 0x41, 0x69, 0x7e, 0x4f, + 0xce, 0x21, 0x23, 0xcd, 0x21, 0xf3, 0x03, 0x00, 0x4b, 0x7c, 0x6d, 0x30, 0x14, 0xa6, 0x78, 0x77, + 0xd6, 0x93, 0xdd, 0x09, 0xbe, 0x46, 0xb4, 0x62, 0xe0, 0x38, 0xf4, 0xd1, 0xb3, 0x10, 0xf0, 0xe9, + 0x9d, 0xf4, 0x03, 0xed, 0x14, 0x28, 0xff, 0x11, 0x94, 0xe6, 0x88, 0xf4, 0x7c, 0x25, 0xc3, 0xdd, + 0x95, 0xe5, 0x28, 0x1f, 0xfa, 0x1a, 0x38, 0x11, 0x1f, 0xed, 0xc2, 0x6a, 0x88, 0x1b, 0xde, 0x77, + 0xb1, 0xd8, 0x25, 0xa1, 0xe3, 0x3d, 0x6f, 0x7e, 0x18, 0xbf, 0x56, 0x36, 0x96, 0x7d, 0xba, 0x79, + 0x7e, 0xe3, 0x4f, 0x69, 0x78, 0x14, 0x94, 0x70, 0x4e, 0xc6, 0xd8, 0xb8, 0xfb, 0x8e, 0x7c, 0x56, + 0x35, 0x17, 0x3f, 0xab, 0x76, 0x97, 0x8e, 0x4d, 0x50, 0x95, 0xa8, 0xfc, 0xff, 0xff, 0x8d, 0xd5, + 0xac, 0xbf, 0x7e, 0xb3, 0x2d, 0x85, 0x23, 0xab, 0x44, 0x23, 0x0b, 0xb0, 0xd5, 0xf8, 0xab, 0x04, + 0x72, 0x04, 0x88, 0x60, 0x60, 0xff, 0xd5, 0xd5, 0xf4, 0xe0, 0xe8, 0x12, 0x23, 0x4a, 0x3f, 0x3c, + 0xa2, 0xcc, 0x7b, 0x46, 0x94, 0x5d, 0x18, 0x51, 0xb3, 0x11, 0xaf, 0x67, 0x7d, 0xc9, 0x05, 0xef, + 0xf9, 0x8d, 0x3f, 0xa6, 0xa0, 0x12, 0xdb, 0xc1, 0xa0, 0xa8, 0x66, 0x62, 0xf3, 0x24, 0xde, 0xfc, + 0xa7, 0x4b, 0x47, 0x18, 0x0c, 0x6f, 0xd9, 0xfe, 0xa5, 0xbe, 0x61, 0xff, 0x7e, 0x92, 0xdc, 0xbf, + 0xf4, 0xb2, 0x11, 0x2f, 0xb6, 0x3b, 0xb1, 0x85, 0x3f, 0x85, 0x68, 0xe0, 0xe1, 0x9d, 0x92, 0xe1, + 0x70, 0xdb, 0x7e, 0x00, 0x28, 0x43, 0x3f, 0xb8, 0x5e, 0xca, 0xf3, 0x17, 0x85, 0x82, 0xaf, 0xab, + 0xf4, 0x0d, 0xeb, 0xba, 0x7f, 0x06, 0xf2, 0xe2, 0x7f, 0x22, 0xd1, 0x06, 0x20, 0x6a, 0x8d, 0x6d, + 0x62, 0xc6, 0x2d, 0xf2, 0x0a, 0x7a, 0x0a, 0x9b, 0xb3, 0xe8, 0xd8, 0x84, 0x51, 0xda, 0xff, 0x75, + 0xb2, 0xeb, 0xc1, 0x1d, 0xf7, 0x21, 0x6c, 0x8f, 0xba, 0x9d, 0x0b, 0x55, 0x1b, 0xb4, 0xce, 0xf5, + 0xe1, 0x4b, 0x7d, 0x30, 0x6c, 0x0d, 0x47, 0x03, 0x7d, 0xd4, 0x1d, 0xf4, 0xd5, 0x76, 0xe7, 0xb4, + 0xa3, 0x9e, 0xc8, 0x2b, 0xa8, 0x0a, 0xe5, 0x4e, 0xf7, 0xb8, 0x37, 0xea, 0x9e, 0xe8, 0x83, 0x51, + 0xbb, 0xad, 0x0e, 0x06, 0xb2, 0x84, 0xb6, 0xe0, 0x71, 0x5f, 0xed, 0x9e, 0x74, 0xba, 0x67, 0x7a, + 0x68, 0x54, 0x5f, 0xaa, 0xed, 0xd1, 0xb0, 0xd3, 0xeb, 0xca, 0x29, 0xb4, 0x09, 0xd5, 0x7e, 0x3b, + 0xd0, 0xa8, 0xd1, 0x7b, 0x69, 0x96, 0x7c, 0xdc, 0x70, 0xda, 0xea, 0x9c, 0xab, 0x27, 0x72, 0x06, + 0xad, 0x43, 0xa5, 0xdf, 0xd6, 0xc3, 0x90, 0x9a, 0x7a, 0xa1, 0x6a, 0x43, 0x39, 0x8b, 0x6a, 0x20, + 0xf7, 0x46, 0x43, 0x11, 0x3f, 0x30, 0xca, 0xb9, 0x84, 0x36, 0x0c, 0x9d, 0x67, 0x79, 0xce, 0xb5, + 0x41, 0xdc, 0x02, 0x5a, 0x85, 0x42, 0xbb, 0xd5, 0x6d, 0xab, 0x4c, 0x2a, 0xee, 0xf7, 0x20, 0x17, + 0x54, 0x5e, 0x86, 0x52, 0xb2, 0xca, 0x12, 0xe4, 0xc3, 0x03, 0x24, 0xf6, 0x56, 0xef, 0x78, 0xa0, + 0x6a, 0x17, 0xea, 0x89, 0x9c, 0x62, 0x92, 0x48, 0x48, 0x3d, 0x91, 0xd3, 0xcc, 0xb1, 0x75, 0xdc, + 0xe3, 0x42, 0x66, 0xff, 0x77, 0x12, 0xe4, 0x04, 0xa7, 0x20, 0x04, 0x6b, 0xb1, 0x88, 0xfa, 0xf0, + 0xa5, 0xbc, 0x82, 0xf2, 0x90, 0x3e, 0x6b, 0xb1, 0x76, 0x55, 0xa1, 0x7c, 0xd6, 0x1a, 0xe8, 0x2d, + 0x56, 0x46, 0xeb, 0x8b, 0xf3, 0x5e, 0x8b, 0xc5, 0x2d, 0x42, 0xf6, 0x74, 0xd4, 0x3d, 0x61, 0x6d, + 0x59, 0x87, 0x0a, 0x7f, 0x4c, 0x78, 0x64, 0x78, 0x52, 0x81, 0x90, 0x65, 0x07, 0x84, 0xad, 0x0e, + 0xfa, 0x93, 0x43, 0x32, 0xac, 0x6a, 0xea, 0xa0, 0x3d, 0x52, 0x75, 0x11, 0x29, 0xbf, 0xff, 0x17, + 0x09, 0xaa, 0x4b, 0xc8, 0x0e, 0xed, 0xc2, 0x56, 0xf8, 0xf6, 0xb9, 0x7a, 0xd6, 0x6a, 0x7f, 0xa1, + 0xdf, 0xcb, 0x76, 0x03, 0xd0, 0x82, 0x8b, 0x48, 0x5e, 0x81, 0xda, 0x82, 0x5e, 0x1c, 0x96, 0x42, + 0x1f, 0xc1, 0xce, 0x32, 0x4b, 0xa2, 0x8a, 0x34, 0x6a, 0x40, 0xfd, 0x7e, 0xdc, 0x64, 0xa5, 0xc7, + 0xfd, 0xaf, 0xde, 0xd6, 0xa5, 0xaf, 0xdf, 0xd6, 0xa5, 0x7f, 0xbc, 0xad, 0x4b, 0xbf, 0x7f, 0x57, + 0x5f, 0xf9, 0xfa, 0x5d, 0x7d, 0xe5, 0x6f, 0xef, 0xea, 0x2b, 0xbf, 0xfa, 0x6c, 0x6c, 0x79, 0x57, + 0xb3, 0xcb, 0x03, 0xc3, 0x99, 0x1c, 0x4e, 0x67, 0xf4, 0x8a, 0xf3, 0x1d, 0x7f, 0x7a, 0xce, 0x1f, + 0x9f, 0xdb, 0x8e, 0x49, 0x0e, 0xfd, 0xc3, 0x68, 0x83, 0xf8, 0x4f, 0x69, 0x97, 0x39, 0xfe, 0xa3, + 0xd8, 0xf7, 0xff, 0x13, 0x00, 0x00, 0xff, 0xff, 0xf6, 0xe3, 0x9d, 0xd5, 0x67, 0x13, 0x00, 0x00, } func (this *Params) Equal(that interface{}) bool { @@ -1619,6 +1570,9 @@ func (this *Params) Equal(that interface{}) bool { if this.SomeValue != that1.SomeValue { return false } + if this.MaxGaslessTxGas != that1.MaxGaslessTxGas { + return false + } return true } func (this *UniversalPayload) Equal(that interface{}) bool { @@ -1669,36 +1623,6 @@ func (this *UniversalPayload) Equal(that interface{}) bool { } return true } -func (this *MigrationPayload) Equal(that interface{}) bool { - if that == nil { - return this == nil - } - - that1, ok := that.(*MigrationPayload) - if !ok { - that2, ok := that.(MigrationPayload) - if ok { - that1 = &that2 - } else { - return false - } - } - if that1 == nil { - return this == nil - } else if this == nil { - return false - } - if this.Migration != that1.Migration { - return false - } - if this.Nonce != that1.Nonce { - return false - } - if this.Deadline != that1.Deadline { - return false - } - return true -} func (this *UniversalAccountId) Equal(that interface{}) bool { if that == nil { return this == nil @@ -2202,6 +2126,11 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.MaxGaslessTxGas != 0 { + i = encodeVarintTypes(dAtA, i, uint64(m.MaxGaslessTxGas)) + i-- + dAtA[i] = 0x18 + } if m.SomeValue { i-- if m.SomeValue { @@ -2299,50 +2228,6 @@ func (m *UniversalPayload) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } -func (m *MigrationPayload) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *MigrationPayload) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *MigrationPayload) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.Deadline) > 0 { - i -= len(m.Deadline) - copy(dAtA[i:], m.Deadline) - i = encodeVarintTypes(dAtA, i, uint64(len(m.Deadline))) - i-- - dAtA[i] = 0x1a - } - if len(m.Nonce) > 0 { - i -= len(m.Nonce) - copy(dAtA[i:], m.Nonce) - i = encodeVarintTypes(dAtA, i, uint64(len(m.Nonce))) - i-- - dAtA[i] = 0x12 - } - if len(m.Migration) > 0 { - i -= len(m.Migration) - copy(dAtA[i:], m.Migration) - i = encodeVarintTypes(dAtA, i, uint64(len(m.Migration))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - func (m *UniversalAccountId) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -3257,6 +3142,9 @@ func (m *Params) Size() (n int) { if m.SomeValue { n += 2 } + if m.MaxGaslessTxGas != 0 { + n += 1 + sovTypes(uint64(m.MaxGaslessTxGas)) + } return n } @@ -3304,27 +3192,6 @@ func (m *UniversalPayload) Size() (n int) { return n } -func (m *MigrationPayload) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Migration) - if l > 0 { - n += 1 + l + sovTypes(uint64(l)) - } - l = len(m.Nonce) - if l > 0 { - n += 1 + l + sovTypes(uint64(l)) - } - l = len(m.Deadline) - if l > 0 { - n += 1 + l + sovTypes(uint64(l)) - } - return n -} - func (m *UniversalAccountId) Size() (n int) { if m == nil { return 0 @@ -3789,6 +3656,25 @@ func (m *Params) Unmarshal(dAtA []byte) error { } } m.SomeValue = bool(v != 0) + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MaxGaslessTxGas", wireType) + } + m.MaxGaslessTxGas = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MaxGaslessTxGas |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipTypes(dAtA[iNdEx:]) @@ -4135,152 +4021,6 @@ func (m *UniversalPayload) Unmarshal(dAtA []byte) error { } return nil } -func (m *MigrationPayload) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTypes - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: MigrationPayload: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: MigrationPayload: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Migration", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTypes - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthTypes - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthTypes - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Migration = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Nonce", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTypes - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthTypes - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthTypes - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Nonce = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Deadline", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTypes - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthTypes - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthTypes - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Deadline = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipTypes(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthTypes - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} func (m *UniversalAccountId) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/x/uexecutor/types/uint256.go b/x/uexecutor/types/uint256.go new file mode 100644 index 000000000..6efe7d229 --- /dev/null +++ b/x/uexecutor/types/uint256.go @@ -0,0 +1,65 @@ +package types + +import ( + "math/big" + + "cosmossdk.io/errors" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const ( + // MaxUint256Bits is the width of a Solidity uint256. Anything wider cannot be + // ABI-encoded faithfully: go-ethereum's encoder truncates mod 2^256 *silently*, + // so an over-range field would make the UEA execute a value different from the + // one the user signed over. + MaxUint256Bits = 256 + + // MaxUint256DecimalLen caps the decimal string length accepted for a uint256 + // field. 2^256-1 is exactly 78 digits; 80 leaves slack for clients that + // zero-pad. It is a cheap pre-filter, not the range check — see + // ValidateUint256String. + MaxUint256DecimalLen = 80 +) + +// ValidateUint256String parses value as a base-10 uint256 and returns it. +// +// The order of the three checks is load-bearing (audit finding F-2026-18798): +// +// 1. Length cap FIRST, before big.Int.SetString. big.Int decimal parsing is +// superlinear in the digit count — as reported in the finding: 78 digits +// 18µs · 100k 24.2ms · 400k 486.6ms · 900k 3.353s. This runs in +// ValidateBasic, which BaseApp executes via validateBasicTxMsgs *before* the +// ante handler, on messages that are gasless — so the work is free and +// unmetered to the attacker, and it is paid per field. Rejecting on len() +// makes that O(1) instead of O(n²). +// +// 2. Parse, rejecting non-numeric and negative input (pre-existing behaviour). +// +// 3. BitLen() <= 256. This is the authoritative range check and the one that +// closes the silent-truncation gap. The length cap alone is NOT sufficient: +// 78 nines is only 78 characters but has BitLen 260, i.e. it fits the cap +// and still overflows uint256. +// +// errMsg is the caller's message for a malformed or negative value, so each call +// site keeps its own wording; the two range failures append a specific reason. +func ValidateUint256String(value string, errMsg string) (*big.Int, error) { + // 1. Cheap reject before the expensive parse. + if len(value) > MaxUint256DecimalLen { + return nil, errors.Wrapf(sdkerrors.ErrInvalidRequest, + "%s: length %d exceeds the maximum of %d characters", errMsg, len(value), MaxUint256DecimalLen) + } + + // 2. Parse. + bi, ok := new(big.Int).SetString(value, 10) + if !ok || bi.Sign() < 0 { + return nil, errors.Wrap(sdkerrors.ErrInvalidRequest, errMsg) + } + + // 3. Authoritative uint256 range check. + if bi.BitLen() > MaxUint256Bits { + return nil, errors.Wrapf(sdkerrors.ErrInvalidRequest, + "%s: value exceeds the uint256 range", errMsg) + } + + return bi, nil +} diff --git a/x/uexecutor/types/uint256_test.go b/x/uexecutor/types/uint256_test.go new file mode 100644 index 000000000..fd3e6365e --- /dev/null +++ b/x/uexecutor/types/uint256_test.go @@ -0,0 +1,542 @@ +package types_test + +import ( + "math/big" + "strings" + "testing" + "time" + + "github.com/pushchain/push-chain-node/x/uexecutor/types" + "github.com/stretchr/testify/require" +) + +// Regression coverage for F-2026-18798 — UExecutor ValidateBasic parsed unbounded +// decimal strings before ante, and never bounded them to uint256. +// +// Two independent defects, and therefore two independent kinds of test here: +// +// - DoS: big.Int decimal parsing is superlinear, ValidateBasic runs before the +// ante handler on a gasless message, and the cost is paid per field. The +// length cap is what makes the reject O(1) — only the *timing* assertions +// below catch its removal, because BitLen still rejects the value. +// - Silent truncation: go-ethereum's ABI encoder truncates mod 2^256 without +// erroring, so an over-range value would execute an amount different from the +// one signed. Only BitLen catches that — 78 nines fits inside the 80-char cap +// but has BitLen 260. + +const ( + // 2^256-1 — the largest legal uint256, exactly 78 digits, BitLen 256. + maxUint256Dec = "115792089237316195423570985008687907853269984665640564039457584007913129639935" + // 2^256 — one past the top, BitLen 257. + overMaxUint256Dec = "115792089237316195423570985008687907853269984665640564039457584007913129639936" + // dosDigits sizes the DoS input. big.Int decimal parsing is superlinear — + // measured on the dev machine: 78 digits 24µs · 100k 9.2ms · 400k 107ms · + // 900k 532ms · 2M 2.6s · 3M 5.7s (the finding reports 3.353s at 900k on + // slower hardware). 3M is chosen so that the two margins are both wide: the + // length cap rejects it in O(1) — nanoseconds — while a parse of it overruns + // dosBudget several times over, so removing the cap fails this test loudly. + dosDigits = 3_000_000 + // dosBudget is deliberately generous relative to the ~nanoseconds an O(1) + // length reject costs, so the assertion cannot flake on a loaded CI runner, + // while still failing hard if the length cap is removed and the superlinear + // parse comes back. + dosBudget = time.Second +) + +// nines78 is 78 characters — inside the 80-char cap — but BitLen 260. This is the +// case that proves a length cap alone is not sufficient. +func nines78() string { return strings.Repeat("9", 78) } + +func hugeDecimal() string { return strings.Repeat("9", dosDigits) } + +// baseValidInbound mirrors the valid FUNDS fixture used in inbound_test.go. +func baseValidInbound() types.Inbound { + return types.Inbound{ + SourceChain: "eip155:11155111", + TxHash: "0x123abc", + Sender: "0x000000000000000000000000000000000000dead", + Recipient: "0x000000000000000000000000000000000000beef", + Amount: "1000", + AssetAddr: "0x000000000000000000000000000000000000cafe", + LogIndex: "1", + TxType: types.TxType_FUNDS, + } +} + +func TestValidateUint256String_Bounds(t *testing.T) { + tests := []struct { + name string + value string + expectError bool + errContains string + }{ + {name: "zero", value: "0"}, + {name: "small", value: "21000"}, + {name: "one wei", value: "1"}, + {name: "typical 1e18", value: "1000000000000000000"}, + {name: "zero padded within cap", value: strings.Repeat("0", 60) + "12345"}, + { + name: "max uint256 accepted", + value: maxUint256Dec, + }, + { + name: "2^256 rejected", + value: overMaxUint256Dec, + expectError: true, + errContains: "exceeds the uint256 range", + }, + { + name: "78 nines rejected despite fitting the length cap", + value: nines78(), + expectError: true, + errContains: "exceeds the uint256 range", + }, + { + name: "negative rejected", + value: "-1", + expectError: true, + errContains: "test field must be valid", + }, + { + name: "non-numeric rejected", + value: "not-a-number", + expectError: true, + errContains: "test field must be valid", + }, + { + name: "decimal point rejected", + value: "12.34", + expectError: true, + errContains: "test field must be valid", + }, + { + name: "over length cap rejected", + value: strings.Repeat("1", types.MaxUint256DecimalLen+1), + expectError: true, + errContains: "exceeds the maximum of 80 characters", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + bi, err := types.ValidateUint256String(tc.value, "test field must be valid") + + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errContains) + require.Nil(t, bi) + return + } + + require.NoError(t, err) + require.NotNil(t, bi) + expected, ok := new(big.Int).SetString(tc.value, 10) + require.True(t, ok) + require.Zero(t, bi.Cmp(expected)) + }) + } +} + +// The boundary pair, stated explicitly: max uint256 in, one past it out. +func TestValidateUint256String_BitLenBoundary(t *testing.T) { + max, ok := new(big.Int).SetString(maxUint256Dec, 10) + require.True(t, ok) + require.Equal(t, 256, max.BitLen(), "sanity: max uint256 is 256 bits") + + over, ok := new(big.Int).SetString(overMaxUint256Dec, 10) + require.True(t, ok) + require.Equal(t, 257, over.BitLen(), "sanity: 2^256 is 257 bits") + + nines, ok := new(big.Int).SetString(nines78(), 10) + require.True(t, ok) + require.Equal(t, 260, nines.BitLen(), "sanity: 78 nines is 260 bits") + require.LessOrEqual(t, len(nines78()), types.MaxUint256DecimalLen, + "sanity: 78 nines fits the length cap, so only BitLen can reject it") + + _, err := types.ValidateUint256String(maxUint256Dec, "amount must be valid") + require.NoError(t, err, "2^256-1 must be accepted") + + _, err = types.ValidateUint256String(overMaxUint256Dec, "amount must be valid") + require.Error(t, err, "2^256 must be rejected") + + _, err = types.ValidateUint256String(nines78(), "amount must be valid") + require.Error(t, err, "78 nines must be rejected") +} + +// F-2026-18798, DoS half. A multi-million-digit field must be rejected, and +// rejected fast. The timing bound is asserted first and on purpose: it is the only +// assertion that fails if the length cap is dropped, because BitLen still rejects +// the value — just after paying for the parse. +func TestUniversalPayload_ValidateBasic_RejectsHugeDecimalFast(t *testing.T) { + huge := hugeDecimal() + + // Every numeric field is reachable, and in the real message the attacker pays + // for none of them — ValidateBasic runs before ante on a gasless msg. + fields := []struct { + name string + payload types.UniversalPayload + }{ + {"value", types.UniversalPayload{To: mockHexAddress(), Value: huge}}, + {"gas_limit", types.UniversalPayload{To: mockHexAddress(), GasLimit: huge}}, + {"max_fee_per_gas", types.UniversalPayload{To: mockHexAddress(), MaxFeePerGas: huge}}, + {"max_priority_fee_per_gas", types.UniversalPayload{To: mockHexAddress(), MaxPriorityFeePerGas: huge}}, + {"nonce", types.UniversalPayload{To: mockHexAddress(), Nonce: huge}}, + {"deadline", types.UniversalPayload{To: mockHexAddress(), Deadline: huge}}, + } + + for _, f := range fields { + t.Run(f.name, func(t *testing.T) { + start := time.Now() + err := f.payload.ValidateBasic() + elapsed := time.Since(start) + + require.Error(t, err, "%s: a %d-digit value must be rejected", f.name, dosDigits) + require.Less(t, elapsed, dosBudget, + "%s: rejecting a %d-digit value took %s — the length cap must reject before big.Int parses", + f.name, dosDigits, elapsed) + // The payload-level size cap (F-2026-18146) rejects this input before + // the per-field cap gets to it — a 3M-digit field is a >128 KiB + // payload. Both rejections are O(1) on len(), which is the property + // this test exists to hold; the per-field message itself stays pinned + // by TestValidateUint256String_Bounds and by + // TestInboundAndOutbound_RejectHugeDecimalFast, neither of which is + // size capped. + require.True(t, + strings.Contains(err.Error(), "exceeds the maximum of 80 characters") || + strings.Contains(err.Error(), "universal payload too large"), + "%s: must be rejected on size or length, before the parse; got: %v", f.name, err) + }) + } +} + +// Same DoS shape at the other two call sites. +func TestInboundAndOutbound_RejectHugeDecimalFast(t *testing.T) { + huge := hugeDecimal() + + t.Run("inbound amount", func(t *testing.T) { + ib := baseValidInbound() + ib.Amount = huge + + start := time.Now() + err := ib.ValidateForExecution() + elapsed := time.Since(start) + + require.Error(t, err) + require.Less(t, elapsed, dosBudget, "rejecting a %d-digit amount took %s", dosDigits, elapsed) + require.Contains(t, err.Error(), "exceeds the maximum of 80 characters") + }) + + t.Run("outbound amount", func(t *testing.T) { + ob := baseValidOutbound() + ob.Amount = huge + + start := time.Now() + err := ob.ValidateBasic() + elapsed := time.Since(start) + + require.Error(t, err) + require.Less(t, elapsed, dosBudget, "rejecting a %d-digit amount took %s", dosDigits, elapsed) + require.Contains(t, err.Error(), "exceeds the maximum of 80 characters") + }) +} + +// F-2026-18798, truncation half, per call site. go-ethereum packs an over-range +// value mod 2^256 without erroring, so these must never reach the encoder. +func TestUniversalPayload_ValidateBasic_Uint256Range(t *testing.T) { + tests := []struct { + name string + payload types.UniversalPayload + expectError bool + }{ + { + name: "max uint256 value accepted", + payload: types.UniversalPayload{To: mockHexAddress(), Value: maxUint256Dec}, + }, + { + name: "max uint256 on every field accepted", + payload: types.UniversalPayload{ + To: mockHexAddress(), + Value: maxUint256Dec, + GasLimit: maxUint256Dec, + MaxFeePerGas: maxUint256Dec, + MaxPriorityFeePerGas: maxUint256Dec, + Nonce: maxUint256Dec, + Deadline: maxUint256Dec, + }, + }, + { + name: "empty numeric fields still skipped", + payload: types.UniversalPayload{To: mockHexAddress()}, + }, + { + name: "2^256 value rejected", + payload: types.UniversalPayload{To: mockHexAddress(), Value: overMaxUint256Dec}, + expectError: true, + }, + { + name: "78 nines value rejected", + payload: types.UniversalPayload{To: mockHexAddress(), Value: nines78()}, + expectError: true, + }, + { + name: "78 nines gas_limit rejected", + payload: types.UniversalPayload{To: mockHexAddress(), GasLimit: nines78()}, + expectError: true, + }, + { + name: "78 nines nonce rejected", + payload: types.UniversalPayload{To: mockHexAddress(), Nonce: nines78()}, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.payload.ValidateBasic() + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), "exceeds the uint256 range") + } else { + require.NoError(t, err) + } + }) + } +} + +func TestInbound_ValidateForExecution_Uint256Range(t *testing.T) { + tests := []struct { + name string + amount string + expectError bool + }{ + {name: "normal amount accepted", amount: "1000"}, + {name: "max uint256 accepted", amount: maxUint256Dec}, + {name: "2^256 rejected", amount: overMaxUint256Dec, expectError: true}, + {name: "78 nines rejected", amount: nines78(), expectError: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ib := baseValidInbound() + ib.Amount = tc.amount + + err := ib.ValidateForExecution() + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), "exceeds the uint256 range") + } else { + require.NoError(t, err) + } + }) + } +} + +func TestOutboundTx_ValidateBasic_Uint256Range(t *testing.T) { + tests := []struct { + name string + amount string + expectError bool + errContains string + }{ + {name: "normal amount accepted", amount: "1000"}, + {name: "max uint256 accepted", amount: maxUint256Dec}, + {name: "2^256 rejected", amount: overMaxUint256Dec, expectError: true, errContains: "exceeds the uint256 range"}, + {name: "78 nines rejected", amount: nines78(), expectError: true, errContains: "exceeds the uint256 range"}, + // Pre-existing semantics preserved: this site requires strictly positive. + {name: "zero still rejected", amount: "0", expectError: true, errContains: "amount must be a valid positive uint256"}, + {name: "negative still rejected", amount: "-1", expectError: true, errContains: "amount must be a valid positive uint256"}, + {name: "non-numeric still rejected", amount: "abc", expectError: true, errContains: "amount must be a valid positive uint256"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ob := baseValidOutbound() + ob.Amount = tc.amount + + err := ob.ValidateBasic() + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errContains) + } else { + require.NoError(t, err) + } + }) + } +} + +// baseValidVoteOutbound is a well-formed success vote; only gas_fee_used varies +// in the tests below. +func baseValidVoteOutbound() types.MsgVoteOutbound { + return types.MsgVoteOutbound{ + Signer: "push1fgaewhyd9fkwtqaj9c233letwcuey6dgly9gv9", + TxId: "ob-1", + UtxId: "utx-1", + ObservedTx: &types.OutboundObservation{ + Success: true, + BlockHeight: 100, + TxHash: "0xb28f49668e7e76dc96d7aabe5b7f63fecfbd1c3574774c05e8204e749fd96fbd", + GasFeeUsed: "21000", + }, + } +} + +// F-2026-18798, DoS half, for the two gas fields that were missed in the first +// pass. Neither message is size capped, so the per-field length cap is the only +// thing standing between a validator-signed multi-million-digit string and the +// superlinear parse — and both messages are gasless. +func TestGasFields_RejectHugeDecimalFast(t *testing.T) { + huge := hugeDecimal() + + t.Run("outbound gas_limit", func(t *testing.T) { + ob := baseValidOutbound() + ob.GasLimit = huge + + start := time.Now() + err := ob.ValidateBasic() + elapsed := time.Since(start) + + require.Error(t, err) + require.Less(t, elapsed, dosBudget, "rejecting a %d-digit gas_limit took %s", dosDigits, elapsed) + require.Contains(t, err.Error(), "exceeds the maximum of 80 characters") + }) + + t.Run("vote outbound gas_fee_used", func(t *testing.T) { + msg := baseValidVoteOutbound() + msg.ObservedTx.GasFeeUsed = huge + + start := time.Now() + err := msg.ValidateBasic() + elapsed := time.Since(start) + + require.Error(t, err) + require.Less(t, elapsed, dosBudget, "rejecting a %d-digit gas_fee_used took %s", dosDigits, elapsed) + require.Contains(t, err.Error(), "exceeds the maximum of 80 characters") + }) +} + +func TestOutboundTx_ValidateBasic_GasLimitUint256(t *testing.T) { + tests := []struct { + name string + gasLimit string + expectError bool + errContains string + }{ + {name: "normal gas_limit accepted", gasLimit: "21000"}, + {name: "zero accepted", gasLimit: "0"}, + {name: "empty gas_limit still skipped", gasLimit: ""}, + {name: "max uint256 accepted", gasLimit: maxUint256Dec}, + { + name: "2^256 rejected", + gasLimit: overMaxUint256Dec, + expectError: true, + errContains: "exceeds the uint256 range", + }, + { + name: "78 nines rejected despite fitting the length cap", + gasLimit: nines78(), + expectError: true, + errContains: "exceeds the uint256 range", + }, + { + name: "over length cap rejected", + gasLimit: strings.Repeat("1", types.MaxUint256DecimalLen+1), + expectError: true, + errContains: "exceeds the maximum of 80 characters", + }, + // Regression: the old check discarded the parsed value, so it never looked + // at the sign and accepted a negative gas_limit. + { + name: "negative rejected", + gasLimit: "-5", + expectError: true, + errContains: "gas_limit must be a valid uint", + }, + { + name: "non-numeric rejected", + gasLimit: "abc", + expectError: true, + errContains: "gas_limit must be a valid uint", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ob := baseValidOutbound() + ob.GasLimit = tc.gasLimit + + err := ob.ValidateBasic() + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errContains) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestMsgVoteOutbound_ValidateBasic_GasFeeUsedUint256(t *testing.T) { + tests := []struct { + name string + gasFeeUsed string + expectError bool + errContains string + }{ + {name: "normal gas_fee_used accepted", gasFeeUsed: "21000"}, + {name: "zero accepted", gasFeeUsed: "0"}, + {name: "max uint256 accepted", gasFeeUsed: maxUint256Dec}, + // Pre-existing semantics preserved: the field is required, and keeps its + // own message ahead of the uint256 parse. + { + name: "empty still rejected as required", + gasFeeUsed: "", + expectError: true, + errContains: "observed_tx.gas_fee_used is required", + }, + { + name: "2^256 rejected", + gasFeeUsed: overMaxUint256Dec, + expectError: true, + errContains: "exceeds the uint256 range", + }, + { + name: "78 nines rejected despite fitting the length cap", + gasFeeUsed: nines78(), + expectError: true, + errContains: "exceeds the uint256 range", + }, + { + name: "over length cap rejected", + gasFeeUsed: strings.Repeat("1", types.MaxUint256DecimalLen+1), + expectError: true, + errContains: "exceeds the maximum of 80 characters", + }, + { + name: "negative rejected", + gasFeeUsed: "-1", + expectError: true, + errContains: "observed_tx.gas_fee_used must be a valid uint256", + }, + { + name: "non-numeric rejected", + gasFeeUsed: "abc", + expectError: true, + errContains: "observed_tx.gas_fee_used must be a valid uint256", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + msg := baseValidVoteOutbound() + msg.ObservedTx.GasFeeUsed = tc.gasFeeUsed + + err := msg.ValidateBasic() + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errContains) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/uexecutor/types/universal_payload.go b/x/uexecutor/types/universal_payload.go index 500f8acb8..4f1b2467a 100644 --- a/x/uexecutor/types/universal_payload.go +++ b/x/uexecutor/types/universal_payload.go @@ -3,7 +3,6 @@ package types import ( "encoding/hex" "encoding/json" - "math/big" "strings" "cosmossdk.io/errors" @@ -21,8 +20,47 @@ func (p UniversalPayload) String() string { return string(bz) } +// ValidateSize enforces the flat MaxUniversalPayloadBytes cap on the serialized +// payload. Split out of ValidateBasic so the keeper can apply the cap on its +// own, independently of whichever msg carried the payload in. +func (p *UniversalPayload) ValidateSize() error { + if p == nil { + return nil + } + if n := p.Size(); n > MaxUniversalPayloadBytes { + return errors.Wrapf(sdkerrors.ErrInvalidRequest, + "universal payload too large: %d bytes exceeds the %d byte limit", n, MaxUniversalPayloadBytes) + } + return nil +} + +// ValidateOutboundPayloadBlobSize enforces MaxOutboundPayloadBytes on an +// outbound's payload. +func ValidateOutboundPayloadBlobSize(field, blob string) error { + if len(blob) > MaxOutboundPayloadBytes { + return errors.Wrapf(sdkerrors.ErrInvalidRequest, + "%s too large: %d bytes exceeds the %d byte limit", field, len(blob), MaxOutboundPayloadBytes) + } + return nil +} + +// ValidatePayloadBlobSize enforces MaxUniversalPayloadBytes on a hex blob that +// carries a universal payload (or its verification data) before it is decoded. +func ValidatePayloadBlobSize(field, blob string) error { + if len(blob) > MaxUniversalPayloadBytes { + return errors.Wrapf(sdkerrors.ErrInvalidRequest, + "%s too large: %d bytes exceeds the %d byte limit", field, len(blob), MaxUniversalPayloadBytes) + } + return nil +} + // ValidateBasic does the sanity check on the UniversalPayload fields. func (p UniversalPayload) ValidateBasic() error { + // Reject oversized payloads before any of the per-field work below. + if err := p.ValidateSize(); err != nil { + return err + } + // Validate 'to' address if strings.TrimSpace(p.To) == "" { return errors.Wrap(sdkerrors.ErrInvalidAddress, "to address cannot be empty") @@ -50,9 +88,9 @@ func (p UniversalPayload) ValidateBasic() error { for fieldName, value := range uintFields { if value != "" { - bi, ok := new(big.Int).SetString(value, 10) - if !ok || bi.Sign() < 0 { - return errors.Wrapf(sdkerrors.ErrInvalidRequest, "%s must be a valid unsigned integer", fieldName) + // Length-capped, range-checked uint256 parse — see F-2026-18798. + if _, err := ValidateUint256String(value, fieldName+" must be a valid unsigned integer"); err != nil { + return err } } } diff --git a/x/utss/keeper/msg_initiate_fund_migration.go b/x/utss/keeper/msg_initiate_fund_migration.go index c24fbf36a..0ddd973e7 100644 --- a/x/utss/keeper/msg_initiate_fund_migration.go +++ b/x/utss/keeper/msg_initiate_fund_migration.go @@ -3,6 +3,7 @@ package keeper import ( "context" "fmt" + "math/big" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/pushchain/push-chain-node/x/utss/types" @@ -10,7 +11,15 @@ import ( // InitiateFundMigration validates and creates a fund migration from an old TSS key vault // to the current TSS key vault for a specific chain. -func (k Keeper) InitiateFundMigration(ctx context.Context, oldKeyId, chain string) (uint64, error) { +// +// balance is the native balance the admin observed on the old TSS address. The +// amount to sweep is derived here rather than supplied, because the gas figures +// it depends on are read from UniversalCore below, after the admin signed the +// message. Deriving it here means transfer_amount and the gas fields recorded on +// the migration are consistent by construction, and every universal validator +// signs that one pinned amount instead of re-deriving it from a live balance +// that any 1-wei inflow can shift (F-2026-18142). +func (k Keeper) InitiateFundMigration(ctx context.Context, oldKeyId, chain, balance string) (uint64, error) { sdkCtx := sdk.UnwrapSDKContext(ctx) // 1. Validate old key exists in history @@ -82,11 +91,36 @@ func (k Keeper) InitiateFundMigration(ctx context.Context, oldKeyId, chain strin return 0, fmt.Errorf("failed to get l1 gas fee for chain %s: %w", chain, err) } - // 8. Create migration record - migrationId, err := k.NextMigrationId.Next(ctx) + // 8. Create migration record. + // + // Ids are allocated as sequence + 1, so the first migration on a chain is 1 + // and never 0. MsgVoteFundMigration.ValidateBasic treats migration_id == 0 + // as "unset" and rejects the message, so a migration stored under id 0 + // could never be voted on: it would stay in PendingMigrations forever and + // block every later migration for that chain (F-2026-18789). Keeping 0 + // reserved for "unset" also keeps that ValidateBasic guard meaningful. + seq, err := k.NextMigrationId.Next(ctx) if err != nil { return 0, fmt.Errorf("failed to get next migration id: %w", err) } + migrationId := seq + 1 + + // Derive the sweep amount from the observed balance and the fees just + // fetched. Rejecting here turns a balance that cannot cover its own transfer + // into a clean error at initiate time, rather than a migration that is + // created PENDING and then fails to sign. + observedBalance, err := types.ParseBalance(balance) + if err != nil { + return 0, err + } + totalFee := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gasLimit)) + totalFee.Add(totalFee, l1GasFee) + transferAmount := new(big.Int).Sub(observedBalance, totalFee) + if transferAmount.Sign() <= 0 { + return 0, fmt.Errorf( + "balance %s on the old TSS address does not cover the migration fee %s (gas_price %s * gas_limit %d + l1_gas_fee %s) for chain %s", + observedBalance, totalFee, gasPrice, gasLimit, l1GasFee, chain) + } migration := types.FundMigration{ Id: migrationId, @@ -100,6 +134,7 @@ func (k Keeper) InitiateFundMigration(ctx context.Context, oldKeyId, chain strin GasPrice: gasPrice.String(), GasLimit: gasLimit, L1GasFee: l1GasFee.String(), + TransferAmount: transferAmount.String(), } if err := k.FundMigrations.Set(ctx, migrationId, migration); err != nil { @@ -121,6 +156,7 @@ func (k Keeper) InitiateFundMigration(ctx context.Context, oldKeyId, chain strin GasPrice: gasPrice.String(), GasLimit: gasLimit, L1GasFee: l1GasFee.String(), + TransferAmount: transferAmount.String(), }) if err != nil { return 0, fmt.Errorf("failed to create migration event: %w", err) diff --git a/x/utss/keeper/msg_server.go b/x/utss/keeper/msg_server.go index ff5ff890a..f9ee8056e 100755 --- a/x/utss/keeper/msg_server.go +++ b/x/utss/keeper/msg_server.go @@ -103,7 +103,7 @@ func (ms msgServer) InitiateFundMigration(ctx context.Context, msg *types.MsgIni return nil, errors.Wrapf(sdkErrors.ErrUnauthorized, "invalid authority; expected %s, got %s", params.Admin, msg.Signer) } - migrationId, err := ms.k.InitiateFundMigration(ctx, msg.OldKeyId, msg.Chain) + migrationId, err := ms.k.InitiateFundMigration(ctx, msg.OldKeyId, msg.Chain, msg.Balance) if err != nil { return nil, err } diff --git a/x/utss/types/events.go b/x/utss/types/events.go index 5ae720c6b..53e09c3ec 100644 --- a/x/utss/types/events.go +++ b/x/utss/types/events.go @@ -110,6 +110,10 @@ type FundMigrationInitiatedEventData struct { GasPrice string `json:"gas_price"` GasLimit uint64 `json:"gas_limit"` L1GasFee string `json:"l1_gas_fee"` + // TransferAmount is the native amount (wei) to sweep, pinned by the chain as + // balance - (gas_price * gas_limit) - l1_gas_fee. Universal validators sign + // this value instead of re-deriving it from a live balance (F-2026-18142). + TransferAmount string `json:"transfer_amount"` } // NewFundMigrationInitiatedEvent creates and returns a Cosmos SDK event. @@ -130,6 +134,7 @@ func NewFundMigrationInitiatedEvent(e FundMigrationInitiatedEventData) (sdk.Even sdk.NewAttribute("gas_price", e.GasPrice), sdk.NewAttribute("gas_limit", fmt.Sprintf("%d", e.GasLimit)), sdk.NewAttribute("l1_gas_fee", e.L1GasFee), + sdk.NewAttribute("transfer_amount", e.TransferAmount), sdk.NewAttribute("data", string(bz)), ) diff --git a/x/utss/types/msg_initiate_fund_migration.go b/x/utss/types/msg_initiate_fund_migration.go new file mode 100644 index 000000000..228050bb9 --- /dev/null +++ b/x/utss/types/msg_initiate_fund_migration.go @@ -0,0 +1,69 @@ +package types + +import ( + "math/big" + "strings" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +var _ sdk.Msg = &MsgInitiateFundMigration{} + +const ( + // maxUint256Bits is the width of the native balances this message deals in. + maxUint256Bits = 256 + // maxUint256DecimalLen bounds the decimal string before it is parsed. Max + // uint256 is exactly 78 digits; the slack absorbs a zero-padded client value. + // Checking length first matters: big.Int decimal parsing is superlinear, so a + // caller-supplied million-digit string is rejected in O(1) instead of being + // parsed and then discarded. + maxUint256DecimalLen = 80 +) + +// ValidateBasic does a sanity check on the provided data. +func (msg *MsgInitiateFundMigration) ValidateBasic() error { + if _, err := sdk.AccAddressFromBech32(msg.Signer); err != nil { + return errors.Wrap(err, "invalid signer address") + } + if strings.TrimSpace(msg.OldKeyId) == "" { + return errors.Wrap(sdkerrors.ErrInvalidRequest, "old_key_id is required") + } + if strings.TrimSpace(msg.Chain) == "" { + return errors.Wrap(sdkerrors.ErrInvalidRequest, "chain is required") + } + if _, err := ParseBalance(msg.Balance); err != nil { + return err + } + return nil +} + +// ParseBalance validates the admin-supplied native balance and returns it. +// +// The value is the balance the admin observed on the old TSS address, not the +// amount to sweep: the keeper derives that as balance - gas - l1_gas_fee using +// the fee figures it fetches itself, so the emitted transfer_amount is +// consistent with the emitted fees by construction. The admin cannot compute it +// because those fees are read from UniversalCore inside the handler, after the +// message is signed. +func ParseBalance(balance string) (*big.Int, error) { + balance = strings.TrimSpace(balance) + if balance == "" { + return nil, errors.Wrap(sdkerrors.ErrInvalidRequest, "balance is required") + } + if len(balance) > maxUint256DecimalLen { + return nil, errors.Wrapf(sdkerrors.ErrInvalidRequest, + "balance is too long: %d characters (max %d)", len(balance), maxUint256DecimalLen) + } + bi, ok := new(big.Int).SetString(balance, 10) + if !ok || bi.Sign() < 0 { + return nil, errors.Wrap(sdkerrors.ErrInvalidRequest, "balance must be a valid non-negative integer") + } + // A length cap alone is not enough: 78 nines fits in 78 characters but is + // wider than uint256, and the EVM ABI encoder truncates such values silently. + if bi.BitLen() > maxUint256Bits { + return nil, errors.Wrap(sdkerrors.ErrInvalidRequest, "balance exceeds uint256") + } + return bi, nil +} diff --git a/x/utss/types/msg_initiate_fund_migration_test.go b/x/utss/types/msg_initiate_fund_migration_test.go new file mode 100644 index 000000000..6f6429a8c --- /dev/null +++ b/x/utss/types/msg_initiate_fund_migration_test.go @@ -0,0 +1,112 @@ +package types_test + +import ( + "math/big" + "strings" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/pushchain/push-chain-node/app" + "github.com/pushchain/push-chain-node/x/utss/types" +) + +const validSigner = "push1fgaewhyd9fkwtqaj9c233letwcuey6dgly9gv9" + +// setBech32Prefixes installs the push prefixes on the global SDK config. Without +// it validSigner fails to parse, ValidateBasic returns on the signer check +// before reaching anything else, and every assertion below becomes vacuous. +// Tests in a package share this global, so each entry point sets it rather than +// relying on another test file having run first. +func setBech32Prefixes() { + cfg := sdk.GetConfig() + cfg.SetBech32PrefixForAccount(app.Bech32PrefixAccAddr, app.Bech32PrefixAccPub) + cfg.SetBech32PrefixForValidator(app.Bech32PrefixValAddr, app.Bech32PrefixValPub) +} + +func baseInitiateMsg() *types.MsgInitiateFundMigration { + return &types.MsgInitiateFundMigration{ + Signer: validSigner, + OldKeyId: "old-key-1", + Chain: "eip155:11155111", + Balance: "1000000000000000000", + } +} + +func TestMsgInitiateFundMigration_ValidateBasic(t *testing.T) { + setBech32Prefixes() + + maxUint256 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) + + tests := []struct { + name string + mutate func(*types.MsgInitiateFundMigration) + wantErr string + }{ + {"valid", func(m *types.MsgInitiateFundMigration) {}, ""}, + {"max uint256 balance accepted", func(m *types.MsgInitiateFundMigration) { + m.Balance = maxUint256.String() + }, ""}, + {"zero balance accepted here (the keeper rejects it once fees are known)", func(m *types.MsgInitiateFundMigration) { + m.Balance = "0" + }, ""}, + {"bad signer", func(m *types.MsgInitiateFundMigration) { m.Signer = "not-bech32" }, "invalid signer"}, + {"missing old_key_id", func(m *types.MsgInitiateFundMigration) { m.OldKeyId = " " }, "old_key_id is required"}, + {"missing chain", func(m *types.MsgInitiateFundMigration) { m.Chain = "" }, "chain is required"}, + {"missing balance", func(m *types.MsgInitiateFundMigration) { m.Balance = "" }, "balance is required"}, + {"negative balance", func(m *types.MsgInitiateFundMigration) { m.Balance = "-1" }, "non-negative"}, + {"non-numeric balance", func(m *types.MsgInitiateFundMigration) { m.Balance = "1e18" }, "non-negative"}, + {"balance over uint256", func(m *types.MsgInitiateFundMigration) { + m.Balance = new(big.Int).Add(maxUint256, big.NewInt(1)).String() + }, "exceeds uint256"}, + { + // 78 nines fits the 80-character cap but is wider than uint256, so a + // length check alone would let it through. The EVM ABI encoder + // truncates such values silently, so BitLen is the load-bearing check. + "78 nines rejected despite fitting the length cap", + func(m *types.MsgInitiateFundMigration) { m.Balance = strings.Repeat("9", 78) }, + "exceeds uint256", + }, + {"absurdly long balance", func(m *types.MsgInitiateFundMigration) { + m.Balance = strings.Repeat("9", 1000) + }, "too long"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + msg := baseInitiateMsg() + tc.mutate(msg) + err := msg.ValidateBasic() + if tc.wantErr == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + require.Contains(t, err.Error(), tc.wantErr) + }) + } +} + +// TestMsgInitiateFundMigration_RejectsHugeBalanceFast pins the ordering inside +// ParseBalance: the length cap has to run before big.Int parses the string. +// Decimal parsing is superlinear, so without the cap a caller-supplied +// multi-million-digit balance is fully parsed and then thrown away. +func TestMsgInitiateFundMigration_RejectsHugeBalanceFast(t *testing.T) { + setBech32Prefixes() + + msg := baseInitiateMsg() + msg.Balance = strings.Repeat("9", 3_000_000) + + start := time.Now() + err := msg.ValidateBasic() + elapsed := time.Since(start) + + // Timing first: require.Contains aborts the test, so asserting the message + // before the duration would mean the duration is never checked. + require.Less(t, elapsed, time.Second, + "rejecting a 3000000-digit balance took %s — the length cap must reject before big.Int parses", elapsed) + require.Error(t, err) + require.Contains(t, err.Error(), "too long") +} diff --git a/x/utss/types/tx.pb.go b/x/utss/types/tx.pb.go index b93c81a3c..8c67a0f76 100644 --- a/x/utss/types/tx.pb.go +++ b/x/utss/types/tx.pb.go @@ -330,6 +330,12 @@ type MsgInitiateFundMigration struct { Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"` OldKeyId string `protobuf:"bytes,2,opt,name=old_key_id,json=oldKeyId,proto3" json:"old_key_id,omitempty"` Chain string `protobuf:"bytes,3,opt,name=chain,proto3" json:"chain,omitempty"` + // Native balance (wei, uint256 decimal) observed by the admin on the old TSS + // address. The chain derives transfer_amount = balance - gas - l1_gas_fee from + // it, using the same fee figures it pins into the migration record, so every + // universal validator signs one amount instead of re-deriving it from a live + // balance that a 1-wei inflow can shift (F-2026-18142). + Balance string `protobuf:"bytes,4,opt,name=balance,proto3" json:"balance,omitempty"` } func (m *MsgInitiateFundMigration) Reset() { *m = MsgInitiateFundMigration{} } @@ -386,6 +392,13 @@ func (m *MsgInitiateFundMigration) GetChain() string { return "" } +func (m *MsgInitiateFundMigration) GetBalance() string { + if m != nil { + return m.Balance + } + return "" +} + type MsgInitiateFundMigrationResponse struct { MigrationId uint64 `protobuf:"varint,1,opt,name=migration_id,json=migrationId,proto3" json:"migration_id,omitempty"` } @@ -551,52 +564,53 @@ func init() { func init() { proto.RegisterFile("utss/v1/tx.proto", fileDescriptor_4dcb8cba4d8073e4) } var fileDescriptor_4dcb8cba4d8073e4 = []byte{ - // 711 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x95, 0xcd, 0x4e, 0xdb, 0x4a, - 0x14, 0xc7, 0x63, 0x3e, 0x02, 0x19, 0x10, 0x5c, 0x7c, 0x83, 0x62, 0x22, 0xf0, 0x0d, 0x96, 0x90, - 0x00, 0x29, 0xf1, 0x85, 0x4a, 0x5d, 0x64, 0x57, 0xa4, 0x56, 0x4d, 0x51, 0x24, 0xe4, 0xd2, 0x4a, - 0xed, 0x26, 0x32, 0xf1, 0xc8, 0x1e, 0x81, 0x3d, 0x96, 0xcf, 0x18, 0xc5, 0xbb, 0xaa, 0xab, 0xaa, - 0xab, 0xbe, 0x49, 0x59, 0x74, 0xd1, 0x47, 0xa0, 0x3b, 0xd4, 0x55, 0x17, 0x55, 0x55, 0xc1, 0x82, - 0xd7, 0xa8, 0x3c, 0x1e, 0x3b, 0x38, 0x31, 0x20, 0x95, 0x4d, 0x34, 0x73, 0xbe, 0xe6, 0xff, 0x3b, - 0x67, 0x26, 0x46, 0xff, 0x84, 0x0c, 0x40, 0x3f, 0xdd, 0xd1, 0xd9, 0xa0, 0xe5, 0x07, 0x94, 0x51, - 0x79, 0x26, 0xb6, 0xb4, 0x4e, 0x77, 0xea, 0xb5, 0x3e, 0x05, 0x97, 0x82, 0xee, 0x82, 0x1d, 0x07, - 0xb8, 0x60, 0x27, 0x11, 0xf5, 0xe5, 0x34, 0xc7, 0xc6, 0x1e, 0x06, 0x02, 0xc2, 0xfc, 0x6f, 0x56, - 0x2a, 0xf2, 0x71, 0x6a, 0xac, 0xda, 0xd4, 0xa6, 0x7c, 0xa9, 0xc7, 0x2b, 0x61, 0x5d, 0x49, 0x4a, - 0xf7, 0x12, 0x47, 0xb2, 0x11, 0xae, 0x25, 0xd3, 0x25, 0x1e, 0xd5, 0xf9, 0x6f, 0x62, 0xd2, 0x3e, - 0x48, 0x68, 0xb1, 0x0b, 0xf6, 0x2b, 0xdf, 0x32, 0x19, 0x3e, 0x30, 0x03, 0xd3, 0x05, 0xf9, 0x31, - 0xaa, 0x98, 0x21, 0x73, 0x68, 0x40, 0x58, 0xa4, 0x48, 0x0d, 0x69, 0xb3, 0xb2, 0xa7, 0x7c, 0xff, - 0xd2, 0xac, 0x8a, 0x5a, 0x4f, 0x2c, 0x2b, 0xc0, 0x00, 0x2f, 0x59, 0x40, 0x3c, 0xdb, 0x18, 0x86, - 0xca, 0x4d, 0x54, 0xf6, 0x79, 0x05, 0x65, 0xa2, 0x21, 0x6d, 0xce, 0xed, 0x2e, 0xb6, 0x04, 0x6e, - 0x2b, 0x29, 0xbc, 0x37, 0x75, 0xfe, 0xeb, 0xbf, 0x92, 0x21, 0x82, 0xda, 0x0b, 0xef, 0xaf, 0xcf, - 0xb6, 0x87, 0xe9, 0xda, 0x0a, 0xaa, 0x8d, 0x28, 0x31, 0x30, 0xf8, 0xd4, 0x03, 0xac, 0x7d, 0x95, - 0x90, 0xd2, 0x05, 0xbb, 0xe3, 0x11, 0x46, 0x4c, 0x86, 0x0f, 0x01, 0xf6, 0x71, 0x74, 0x10, 0xd0, - 0x3e, 0x06, 0x90, 0xff, 0x47, 0x65, 0x20, 0xb6, 0x87, 0x83, 0x7b, 0xb5, 0x8a, 0x38, 0xb9, 0x8d, - 0xe6, 0xfd, 0x24, 0xb9, 0x17, 0xf7, 0x93, 0xcb, 0x5d, 0xd8, 0xad, 0x65, 0x72, 0x0f, 0x01, 0x44, - 0xf1, 0xc3, 0xc8, 0xc7, 0xc6, 0x9c, 0x3f, 0xdc, 0xb4, 0x5b, 0xb1, 0x6a, 0x51, 0xe8, 0xe3, 0xf5, - 0xd9, 0xb6, 0xca, 0x27, 0xd3, 0x05, 0xfb, 0x35, 0x65, 0x38, 0x15, 0x38, 0x54, 0xa7, 0x69, 0xa8, - 0x71, 0x9b, 0xf2, 0x0c, 0xef, 0x5c, 0x42, 0x55, 0x51, 0xe1, 0xa1, 0x68, 0x6b, 0x08, 0x31, 0x80, - 0x9e, 0x1f, 0x1e, 0x1d, 0xe3, 0x88, 0x83, 0x55, 0x8c, 0x0a, 0x03, 0x38, 0xe0, 0x06, 0x79, 0x19, - 0x95, 0x8f, 0x71, 0xd4, 0x23, 0x96, 0x32, 0xc9, 0x5d, 0xd3, 0xc7, 0x38, 0xea, 0x58, 0x71, 0x56, - 0xda, 0x10, 0x62, 0x29, 0x53, 0x0d, 0x69, 0x73, 0xca, 0xa8, 0x08, 0x4b, 0xc7, 0x6a, 0x6f, 0x8d, - 0x30, 0xaf, 0xdc, 0x64, 0xce, 0x29, 0xd6, 0x54, 0xb4, 0x5a, 0x64, 0xcf, 0x50, 0x3f, 0xe7, 0x27, - 0xf9, 0x2c, 0xf4, 0xac, 0x2e, 0xb1, 0x03, 0x93, 0x11, 0xea, 0xfd, 0x05, 0xee, 0x2a, 0x42, 0xf4, - 0xc4, 0xea, 0x09, 0xa6, 0x04, 0x77, 0x96, 0x9e, 0x58, 0xfb, 0x1c, 0xab, 0x8a, 0xa6, 0xfb, 0x8e, - 0x49, 0xbc, 0x14, 0x96, 0x6f, 0xda, 0xcd, 0x11, 0x9a, 0xb5, 0x94, 0xa6, 0x50, 0x94, 0xf6, 0x34, - 0x37, 0xc0, 0x9c, 0x2f, 0xa5, 0x92, 0xd7, 0xd1, 0xbc, 0x9b, 0x1a, 0x63, 0x21, 0x12, 0xef, 0xe0, - 0x5c, 0x66, 0xeb, 0x58, 0xda, 0xb7, 0xe1, 0x8c, 0x1f, 0x0a, 0x3d, 0x7a, 0xda, 0xc4, 0xd8, 0x69, - 0x72, 0x0d, 0xcd, 0xb0, 0x41, 0xcf, 0x31, 0xc1, 0x11, 0xec, 0x65, 0x36, 0x78, 0x6e, 0x82, 0x23, - 0x2b, 0x68, 0x06, 0xc2, 0x7e, 0x3c, 0x12, 0x3e, 0xe6, 0x59, 0x23, 0xdd, 0xde, 0x3d, 0xe4, 0x7c, - 0x4b, 0x86, 0x43, 0x2e, 0x6c, 0xc7, 0xee, 0xcf, 0x49, 0x34, 0xd9, 0x05, 0x5b, 0x7e, 0x81, 0xe6, - 0x73, 0x7f, 0x2c, 0x4a, 0xf6, 0xc2, 0x46, 0x1e, 0x7a, 0xbd, 0x71, 0x9b, 0x27, 0x6b, 0x31, 0x46, - 0xcb, 0xc5, 0xcf, 0x7f, 0xfd, 0x66, 0x6a, 0x61, 0x48, 0x7d, 0xeb, 0xde, 0x90, 0xec, 0x98, 0x37, - 0x68, 0x69, 0xfc, 0x19, 0xae, 0xdd, 0xcc, 0x1f, 0x73, 0xd7, 0x37, 0xee, 0x74, 0x17, 0x11, 0xe4, - 0x6f, 0x40, 0x21, 0x41, 0x2e, 0xa4, 0x98, 0xa0, 0xf8, 0x2e, 0x0a, 0x82, 0xfc, 0x11, 0x63, 0x04, - 0xf9, 0xf2, 0x1b, 0x77, 0xba, 0xd3, 0xd2, 0xf5, 0xe9, 0x77, 0xd7, 0x67, 0xdb, 0xd2, 0xde, 0xfe, - 0xf9, 0xa5, 0x2a, 0x5d, 0x5c, 0xaa, 0xd2, 0xef, 0x4b, 0x55, 0xfa, 0x74, 0xa5, 0x96, 0x2e, 0xae, - 0xd4, 0xd2, 0x8f, 0x2b, 0xb5, 0xf4, 0x76, 0xc7, 0x26, 0xcc, 0x09, 0x8f, 0x5a, 0x7d, 0xea, 0xea, - 0x7e, 0x08, 0x0e, 0x7f, 0x70, 0x7c, 0xd5, 0xe4, 0xcb, 0xa6, 0x47, 0x2d, 0xac, 0x0f, 0x74, 0x7e, - 0xb5, 0xf8, 0xa7, 0xec, 0xa8, 0xcc, 0xbf, 0x43, 0x8f, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0x7f, - 0xd9, 0x50, 0x46, 0x2d, 0x07, 0x00, 0x00, + // 724 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0x4d, 0x4f, 0xdb, 0x48, + 0x18, 0x8e, 0xf9, 0x08, 0x64, 0x40, 0xb0, 0x78, 0x83, 0x62, 0x22, 0xf0, 0x06, 0x4b, 0x48, 0x80, + 0x94, 0x78, 0x61, 0xa5, 0x3d, 0xe4, 0x56, 0xa4, 0x56, 0x4d, 0x51, 0x24, 0xe4, 0xd2, 0x4a, 0xed, + 0x25, 0x72, 0xe2, 0x91, 0x3d, 0x02, 0x7b, 0x2c, 0xbf, 0x63, 0x14, 0xdf, 0xaa, 0x9e, 0xaa, 0x9e, + 0xfa, 0x53, 0x38, 0xf4, 0xd0, 0x9f, 0x40, 0x6f, 0xa8, 0xa7, 0x1e, 0xaa, 0xaa, 0x82, 0x03, 0x7f, + 0xa3, 0xf2, 0x78, 0xec, 0xe0, 0xc4, 0x80, 0x54, 0x2e, 0xd6, 0xbc, 0x9f, 0xf3, 0x3c, 0xcf, 0x3b, + 0x33, 0x46, 0x7f, 0x85, 0x0c, 0x40, 0x3f, 0xdb, 0xd3, 0xd9, 0xb0, 0xe5, 0x07, 0x94, 0x51, 0x79, + 0x2e, 0xf6, 0xb4, 0xce, 0xf6, 0xea, 0xb5, 0x01, 0x05, 0x97, 0x82, 0xee, 0x82, 0x1d, 0x27, 0xb8, + 0x60, 0x27, 0x19, 0xf5, 0xd5, 0xb4, 0xc6, 0xc6, 0x1e, 0x06, 0x02, 0xc2, 0xfd, 0x77, 0xd6, 0x2a, + 0xf2, 0x71, 0xea, 0xac, 0xda, 0xd4, 0xa6, 0x7c, 0xa9, 0xc7, 0x2b, 0xe1, 0x5d, 0x4b, 0x5a, 0xf7, + 0x92, 0x40, 0x62, 0x88, 0xd0, 0x8a, 0xe9, 0x12, 0x8f, 0xea, 0xfc, 0x9b, 0xb8, 0xb4, 0x0f, 0x12, + 0x5a, 0xee, 0x82, 0xfd, 0xca, 0xb7, 0x4c, 0x86, 0x8f, 0xcc, 0xc0, 0x74, 0x41, 0xfe, 0x1f, 0x55, + 0xcc, 0x90, 0x39, 0x34, 0x20, 0x2c, 0x52, 0xa4, 0x86, 0xb4, 0x5d, 0x39, 0x50, 0xbe, 0x7d, 0x6e, + 0x56, 0x45, 0xaf, 0x27, 0x96, 0x15, 0x60, 0x80, 0x97, 0x2c, 0x20, 0x9e, 0x6d, 0x8c, 0x52, 0xe5, + 0x26, 0x2a, 0xfb, 0xbc, 0x83, 0x32, 0xd5, 0x90, 0xb6, 0x17, 0xf6, 0x97, 0x5b, 0x82, 0x6e, 0x2b, + 0x69, 0x7c, 0x30, 0x73, 0xf1, 0xf3, 0x9f, 0x92, 0x21, 0x92, 0xda, 0x4b, 0xef, 0x6f, 0xce, 0x77, + 0x47, 0xe5, 0xda, 0x1a, 0xaa, 0x8d, 0x21, 0x31, 0x30, 0xf8, 0xd4, 0x03, 0xac, 0x7d, 0x91, 0x90, + 0xd2, 0x05, 0xbb, 0xe3, 0x11, 0x46, 0x4c, 0x86, 0x8f, 0x01, 0x0e, 0x71, 0x74, 0x14, 0xd0, 0x01, + 0x06, 0x90, 0xff, 0x45, 0x65, 0x20, 0xb6, 0x87, 0x83, 0x07, 0xb1, 0x8a, 0x3c, 0xb9, 0x8d, 0x16, + 0xfd, 0xa4, 0xb8, 0x17, 0xeb, 0xc9, 0xe1, 0x2e, 0xed, 0xd7, 0x32, 0xb8, 0xc7, 0x00, 0xa2, 0xf9, + 0x71, 0xe4, 0x63, 0x63, 0xc1, 0x1f, 0x19, 0xed, 0x56, 0x8c, 0x5a, 0x34, 0xfa, 0x78, 0x73, 0xbe, + 0xab, 0xf2, 0xc9, 0x74, 0xc1, 0x7e, 0x4d, 0x19, 0x4e, 0x01, 0x8e, 0xd0, 0x69, 0x1a, 0x6a, 0xdc, + 0x85, 0x3c, 0xa3, 0x77, 0x21, 0xa1, 0xaa, 0xe8, 0xf0, 0x58, 0x6a, 0x1b, 0x08, 0x31, 0x80, 0x9e, + 0x1f, 0xf6, 0x4f, 0x70, 0xc4, 0x89, 0x55, 0x8c, 0x0a, 0x03, 0x38, 0xe2, 0x0e, 0x79, 0x15, 0x95, + 0x4f, 0x70, 0xd4, 0x23, 0x96, 0x32, 0xcd, 0x43, 0xb3, 0x27, 0x38, 0xea, 0x58, 0x71, 0x55, 0x2a, + 0x08, 0xb1, 0x94, 0x99, 0x86, 0xb4, 0x3d, 0x63, 0x54, 0x84, 0xa7, 0x63, 0xb5, 0x77, 0xc6, 0x38, + 0xaf, 0xdd, 0xe6, 0x9c, 0x43, 0xac, 0xa9, 0x68, 0xbd, 0xc8, 0x9f, 0x51, 0xfd, 0x9a, 0x9f, 0xe4, + 0xb3, 0xd0, 0xb3, 0xba, 0xc4, 0x0e, 0x4c, 0x46, 0xa8, 0xf7, 0x07, 0x74, 0xd7, 0x11, 0xa2, 0xa7, + 0x56, 0x4f, 0x70, 0x4a, 0xe8, 0xce, 0xd3, 0x53, 0xeb, 0x90, 0xd3, 0xaa, 0xa2, 0xd9, 0x81, 0x63, + 0x12, 0x2f, 0x25, 0xcb, 0x0d, 0x59, 0x41, 0x73, 0x7d, 0xf3, 0xd4, 0xf4, 0x06, 0x98, 0x33, 0xad, + 0x18, 0xa9, 0xd9, 0x6e, 0x8e, 0xf1, 0xdc, 0x48, 0x79, 0x16, 0xc2, 0xd5, 0x9e, 0xe6, 0x46, 0x9b, + 0x8b, 0xa5, 0x7c, 0xe5, 0x4d, 0xb4, 0xe8, 0xa6, 0xce, 0x18, 0xa2, 0xc4, 0xb5, 0x5d, 0xc8, 0x7c, + 0x1d, 0x2b, 0x96, 0x24, 0x9d, 0xfe, 0x63, 0xe5, 0x18, 0xdf, 0x6d, 0x6a, 0x62, 0x37, 0xb9, 0x86, + 0xe6, 0xd8, 0xb0, 0xe7, 0x98, 0xe0, 0x08, 0x55, 0xca, 0x6c, 0xf8, 0xdc, 0x04, 0x27, 0x96, 0x05, + 0xc2, 0x41, 0x3c, 0x2c, 0x2e, 0xcb, 0xbc, 0x91, 0x9a, 0xf7, 0x8f, 0x3f, 0x2f, 0xc9, 0x68, 0xfc, + 0x85, 0x72, 0xec, 0xff, 0x98, 0x46, 0xd3, 0x5d, 0xb0, 0xe5, 0x17, 0x68, 0x31, 0xf7, 0xe4, 0x28, + 0xd9, 0xdd, 0x1b, 0x7b, 0x02, 0xea, 0x8d, 0xbb, 0x22, 0x99, 0xc4, 0x18, 0xad, 0x16, 0x3f, 0x0c, + 0x9b, 0xb7, 0x4b, 0x0b, 0x53, 0xea, 0x3b, 0x0f, 0xa6, 0x64, 0xdb, 0xbc, 0x41, 0x2b, 0x93, 0x17, + 0x74, 0xe3, 0x76, 0xfd, 0x44, 0xb8, 0xbe, 0x75, 0x6f, 0xb8, 0x88, 0x41, 0xfe, 0x04, 0x14, 0x32, + 0xc8, 0xa5, 0x14, 0x33, 0x28, 0x3e, 0x8b, 0x82, 0x41, 0x7e, 0x8b, 0x09, 0x06, 0xf9, 0xf6, 0x5b, + 0xf7, 0x86, 0xd3, 0xd6, 0xf5, 0xd9, 0x77, 0x37, 0xe7, 0xbb, 0xd2, 0xc1, 0xe1, 0xc5, 0x95, 0x2a, + 0x5d, 0x5e, 0xa9, 0xd2, 0xaf, 0x2b, 0x55, 0xfa, 0x74, 0xad, 0x96, 0x2e, 0xaf, 0xd5, 0xd2, 0xf7, + 0x6b, 0xb5, 0xf4, 0x76, 0xcf, 0x26, 0xcc, 0x09, 0xfb, 0xad, 0x01, 0x75, 0x75, 0x3f, 0x04, 0x87, + 0x5f, 0x45, 0xbe, 0x6a, 0xf2, 0x65, 0xd3, 0xa3, 0x16, 0xd6, 0x87, 0x3a, 0x3f, 0x5a, 0xfc, 0x27, + 0xd7, 0x2f, 0xf3, 0x3f, 0xd4, 0x7f, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0x58, 0x7a, 0x6f, 0x8f, + 0x47, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -1050,6 +1064,13 @@ func (m *MsgInitiateFundMigration) MarshalToSizedBuffer(dAtA []byte) (int, error _ = i var l int _ = l + if len(m.Balance) > 0 { + i -= len(m.Balance) + copy(dAtA[i:], m.Balance) + i = encodeVarintTx(dAtA, i, uint64(len(m.Balance))) + i-- + dAtA[i] = 0x22 + } if len(m.Chain) > 0 { i -= len(m.Chain) copy(dAtA[i:], m.Chain) @@ -1288,6 +1309,10 @@ func (m *MsgInitiateFundMigration) Size() (n int) { if l > 0 { n += 1 + l + sovTx(uint64(l)) } + l = len(m.Balance) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } return n } @@ -1997,6 +2022,38 @@ func (m *MsgInitiateFundMigration) Unmarshal(dAtA []byte) error { } m.Chain = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Balance", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Balance = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTx(dAtA[iNdEx:]) diff --git a/x/utss/types/types.pb.go b/x/utss/types/types.pb.go index 2bdd43794..b1020b7e6 100644 --- a/x/utss/types/types.pb.go +++ b/x/utss/types/types.pb.go @@ -509,6 +509,7 @@ type FundMigration struct { GasPrice string `protobuf:"bytes,11,opt,name=gas_price,json=gasPrice,proto3" json:"gas_price,omitempty"` GasLimit uint64 `protobuf:"varint,12,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty"` L1GasFee string `protobuf:"bytes,13,opt,name=l1_gas_fee,json=l1GasFee,proto3" json:"l1_gas_fee,omitempty"` + TransferAmount string `protobuf:"bytes,14,opt,name=transfer_amount,json=transferAmount,proto3" json:"transfer_amount,omitempty"` } func (m *FundMigration) Reset() { *m = FundMigration{} } @@ -635,6 +636,13 @@ func (m *FundMigration) GetL1GasFee() string { return "" } +func (m *FundMigration) GetTransferAmount() string { + if m != nil { + return m.TransferAmount + } + return "" +} + func init() { proto.RegisterEnum("utss.v1.TssKeyProcessStatus", TssKeyProcessStatus_name, TssKeyProcessStatus_value) proto.RegisterEnum("utss.v1.TssProcessType", TssProcessType_name, TssProcessType_value) @@ -651,72 +659,73 @@ func init() { func init() { proto.RegisterFile("utss/v1/types.proto", fileDescriptor_6ecfa9650339f6c3) } var fileDescriptor_6ecfa9650339f6c3 = []byte{ - // 1030 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x56, 0xcf, 0x4f, 0xe3, 0x46, - 0x14, 0x8e, 0x13, 0x08, 0x9b, 0x47, 0x48, 0xc3, 0x10, 0x20, 0xcb, 0x8f, 0x00, 0xd9, 0x95, 0x8a, - 0x50, 0x37, 0x56, 0x5a, 0x0e, 0x15, 0xb7, 0x2c, 0x38, 0x60, 0x01, 0x21, 0x75, 0x1c, 0xd4, 0xdd, - 0x8b, 0xeb, 0xc4, 0xb3, 0xce, 0x28, 0x89, 0x6d, 0x65, 0x1c, 0x44, 0x7a, 0xa9, 0xd4, 0x63, 0x4f, - 0x3d, 0xf6, 0xc8, 0x9f, 0xd0, 0xbf, 0xa2, 0xea, 0x71, 0x8f, 0x3d, 0x56, 0xa0, 0xaa, 0xfd, 0x33, - 0xaa, 0x99, 0x71, 0x12, 0x3b, 0xc9, 0x05, 0x66, 0xde, 0xf7, 0xbd, 0xe7, 0x37, 0xdf, 0xfb, 0x66, - 0x00, 0x36, 0x86, 0x3e, 0xa5, 0xf2, 0x43, 0x59, 0xf6, 0x47, 0x1e, 0xa6, 0x25, 0x6f, 0xe0, 0xfa, - 0x2e, 0x5a, 0x61, 0xc1, 0xd2, 0x43, 0x79, 0xa7, 0xd0, 0x76, 0x69, 0xdf, 0xa5, 0x72, 0xcb, 0xa4, - 0x58, 0x7e, 0x28, 0xb7, 0xb0, 0x6f, 0x96, 0xe5, 0xb6, 0x4b, 0x1c, 0x41, 0xdc, 0xd9, 0x0e, 0xf0, - 0x3e, 0xb5, 0x59, 0x8d, 0x3e, 0xb5, 0x03, 0x20, 0x67, 0xbb, 0xb6, 0xcb, 0x97, 0x32, 0x5b, 0x05, - 0xd1, 0x75, 0xb3, 0x4f, 0x1c, 0x57, 0xe6, 0x3f, 0x45, 0xa8, 0xf8, 0x2d, 0x24, 0xeb, 0xe6, 0xc0, - 0xec, 0x53, 0x94, 0x83, 0x65, 0xd3, 0xea, 0x13, 0x27, 0x2f, 0x1d, 0x4a, 0xc7, 0x29, 0x4d, 0x6c, - 0xce, 0xf2, 0xbf, 0x3d, 0x1d, 0xc4, 0xfe, 0x7b, 0x3a, 0x90, 0x7e, 0xf9, 0xf7, 0xf7, 0x93, 0x55, - 0xde, 0xac, 0xc7, 0xf9, 0xc5, 0xa7, 0x38, 0xac, 0xe9, 0x94, 0x5e, 0xe3, 0x51, 0x7d, 0xe0, 0xb6, - 0x31, 0xa5, 0xe8, 0x14, 0x92, 0xd4, 0x37, 0xfd, 0x21, 0xe5, 0x25, 0x32, 0x5f, 0xef, 0x95, 0x82, - 0x73, 0x94, 0x22, 0xbc, 0x06, 0xe7, 0x68, 0x01, 0x17, 0x15, 0x21, 0xed, 0x99, 0x03, 0x9f, 0xb4, - 0x89, 0x67, 0x3a, 0x3e, 0xcd, 0xc7, 0x0f, 0x13, 0xc7, 0x29, 0x2d, 0x12, 0x43, 0x47, 0x90, 0x6e, - 0xf5, 0xdc, 0x76, 0xd7, 0xe8, 0x60, 0x62, 0x77, 0xfc, 0x7c, 0xe2, 0x50, 0x3a, 0x4e, 0x68, 0xab, - 0x3c, 0x76, 0xc5, 0x43, 0xe8, 0x0d, 0xac, 0xe1, 0x47, 0x8f, 0x0c, 0x46, 0x63, 0xce, 0x12, 0xe7, - 0xa4, 0x45, 0x30, 0x20, 0x9d, 0x41, 0xda, 0x13, 0x4d, 0x18, 0x4c, 0xef, 0xfc, 0x32, 0xef, 0x73, - 0x3b, 0xdc, 0x67, 0xd0, 0xa4, 0x3e, 0xf2, 0xb0, 0xb6, 0xea, 0x4d, 0x37, 0x28, 0x03, 0x71, 0x62, - 0xe5, 0x93, 0x87, 0xd2, 0xf1, 0x92, 0x16, 0x27, 0xd6, 0xd9, 0x51, 0x58, 0x99, 0x1c, 0x57, 0xc6, - 0xa7, 0xd4, 0xe8, 0xe2, 0x91, 0x11, 0xa4, 0x15, 0x7f, 0x8e, 0x43, 0x52, 0x1c, 0x1d, 0xed, 0x03, - 0x30, 0xd4, 0x1b, 0xb6, 0xba, 0x78, 0x14, 0x48, 0x9c, 0xf2, 0x29, 0xad, 0xf3, 0x00, 0xda, 0x84, - 0x24, 0x4b, 0x24, 0x56, 0x3e, 0x2e, 0xd4, 0xef, 0xe2, 0x91, 0x6a, 0xcd, 0x69, 0x93, 0x58, 0xa0, - 0xcd, 0x29, 0x6c, 0x7d, 0x22, 0x8e, 0xd9, 0x23, 0x3f, 0x62, 0xcb, 0x88, 0xa8, 0x24, 0x14, 0xc8, - 0x4d, 0xd0, 0xf7, 0x21, 0xb9, 0x4a, 0xb0, 0xd1, 0xc5, 0x23, 0x1b, 0x3b, 0xd1, 0x94, 0x65, 0x9e, - 0xb2, 0x2e, 0xa0, 0x30, 0x7f, 0x1f, 0x60, 0xac, 0xdc, 0x44, 0x85, 0x54, 0x10, 0x51, 0xad, 0xb3, - 0xd7, 0x61, 0x31, 0xd2, 0x61, 0x31, 0x8a, 0xff, 0xc4, 0xe1, 0x95, 0x4e, 0xa9, 0xf2, 0x80, 0x1d, - 0x3f, 0x10, 0x51, 0x1a, 0x8b, 0x88, 0x4e, 0x01, 0x30, 0x03, 0xc4, 0x38, 0xe2, 0x7c, 0x1c, 0x9b, - 0xe1, 0x71, 0xf0, 0x34, 0x3e, 0x8c, 0x14, 0x1e, 0x2f, 0x91, 0x3c, 0x31, 0x5a, 0x62, 0x7e, 0x80, - 0x3c, 0x63, 0xc6, 0x63, 0xd1, 0xee, 0x97, 0x66, 0xba, 0x67, 0xf6, 0x9a, 0xb3, 0x45, 0x2a, 0x3a, - 0xfd, 0xd9, 0x49, 0x24, 0x17, 0x4c, 0x62, 0xce, 0x82, 0x2b, 0x0b, 0x2c, 0x38, 0x6b, 0xe5, 0x57, - 0xf3, 0x56, 0x9e, 0x9a, 0x21, 0x15, 0x36, 0x43, 0xd4, 0x42, 0x30, 0x63, 0xa1, 0xe2, 0x1f, 0x09, - 0x58, 0xab, 0x0e, 0x1d, 0xeb, 0x96, 0xd8, 0x03, 0xd3, 0x27, 0xae, 0x33, 0x27, 0xf6, 0x1e, 0x80, - 0xdb, 0xb3, 0x8c, 0x88, 0xd1, 0x5e, 0xb9, 0x3d, 0xeb, 0x9a, 0x97, 0x7f, 0x0b, 0x19, 0x86, 0x86, - 0x3e, 0x91, 0xe0, 0x8c, 0xb4, 0xdb, 0xb3, 0xf4, 0x89, 0x51, 0xdf, 0x42, 0xa6, 0x3d, 0x1c, 0x0c, - 0xd8, 0xc8, 0x82, 0x3a, 0x4b, 0x82, 0x15, 0x44, 0x45, 0xad, 0xaf, 0x00, 0x8d, 0x59, 0xa1, 0x7a, - 0x42, 0xd6, 0x6c, 0x80, 0x4c, 0x6b, 0xe6, 0x60, 0xb9, 0xdd, 0x31, 0x89, 0xc3, 0x6d, 0x95, 0xd2, - 0xc4, 0x26, 0xf4, 0x9a, 0xac, 0xcc, 0xbc, 0x26, 0x91, 0x53, 0xce, 0x4c, 0xfa, 0x4b, 0xf8, 0x82, - 0x38, 0xc4, 0x27, 0xa6, 0x3f, 0xbe, 0x0d, 0x81, 0xc2, 0x99, 0x49, 0x98, 0xdb, 0x9a, 0x11, 0xdb, - 0x6e, 0xdf, 0xeb, 0xe1, 0x29, 0x31, 0x25, 0x88, 0x93, 0xb0, 0x20, 0x6e, 0xc3, 0x8a, 0xff, 0x68, - 0x74, 0x4c, 0xda, 0x09, 0x34, 0x4f, 0xfa, 0x8f, 0x57, 0x26, 0xed, 0xa0, 0x5d, 0x48, 0xd9, 0x26, - 0x35, 0xbc, 0x01, 0x69, 0xe3, 0xfc, 0xaa, 0x50, 0xd3, 0x36, 0x69, 0x9d, 0xed, 0xc7, 0x60, 0x8f, - 0xf4, 0x89, 0x9f, 0x4f, 0xf3, 0x11, 0x30, 0xf0, 0x86, 0xed, 0xd9, 0x20, 0x7a, 0x65, 0x83, 0xe1, - 0x9f, 0x30, 0xce, 0xaf, 0x89, 0xd4, 0x5e, 0xf9, 0xd2, 0xa4, 0x55, 0x8c, 0x4f, 0xba, 0xb0, 0xb1, - 0xe0, 0xbd, 0x44, 0xbb, 0xb0, 0xad, 0x37, 0x1a, 0xc6, 0xb5, 0xf2, 0xc1, 0xa8, 0x6b, 0x77, 0xe7, - 0x4a, 0xa3, 0x61, 0xd4, 0x95, 0xda, 0x85, 0x5a, 0xbb, 0xcc, 0xc6, 0x16, 0x81, 0x8d, 0xe6, 0x39, - 0xfb, 0x9d, 0x95, 0xd0, 0x0e, 0x6c, 0xcd, 0x82, 0xd5, 0x8a, 0x7a, 0xa3, 0x5c, 0x64, 0xe3, 0x27, - 0x3f, 0x40, 0x26, 0xfa, 0xe8, 0xa1, 0x2d, 0x40, 0x8c, 0x3d, 0x66, 0x5e, 0x2b, 0x1f, 0x2e, 0x95, - 0x5a, 0x36, 0x86, 0xb6, 0x61, 0x23, 0x1c, 0xd7, 0x94, 0xaa, 0xa6, 0x34, 0xae, 0xb2, 0x12, 0xda, - 0x87, 0xd7, 0x61, 0xe0, 0xbb, 0xe6, 0x9d, 0xd6, 0xbc, 0x35, 0xce, 0xaf, 0x2a, 0xb5, 0x4b, 0x25, - 0x1b, 0x3f, 0xb9, 0x81, 0x74, 0xf8, 0x1e, 0xa3, 0x03, 0xd8, 0x65, 0x74, 0xe5, 0x5e, 0xa9, 0xe9, - 0x93, 0x24, 0xb5, 0xa6, 0xea, 0x6a, 0x45, 0x57, 0x2e, 0xa6, 0x67, 0x11, 0x04, 0xd6, 0x74, 0x55, - 0xad, 0x55, 0x6e, 0xd4, 0x8f, 0xca, 0x45, 0x56, 0x3a, 0xb9, 0xe7, 0xfd, 0x86, 0xee, 0x38, 0xca, - 0x41, 0x76, 0x4a, 0xaf, 0x9c, 0xeb, 0xea, 0xbd, 0x32, 0xed, 0x56, 0x44, 0xcf, 0xef, 0x6e, 0xeb, - 0x37, 0x0a, 0xab, 0x2e, 0xa1, 0x4d, 0x58, 0x9f, 0x02, 0xca, 0xf7, 0x75, 0x55, 0xe3, 0x3a, 0xfc, - 0x04, 0x1b, 0x0b, 0x6c, 0x85, 0x8e, 0x60, 0xbf, 0xda, 0xac, 0x5d, 0x18, 0xb7, 0xea, 0xa5, 0x56, - 0xd1, 0xd5, 0xbb, 0x9a, 0xd1, 0xd0, 0x2b, 0x7a, 0x33, 0x2c, 0xfd, 0x1b, 0x38, 0x58, 0x4c, 0x09, - 0x7f, 0xf5, 0x10, 0xf6, 0x16, 0x93, 0xc6, 0x83, 0x78, 0x7f, 0xfd, 0xe7, 0x73, 0x41, 0xfa, 0xfc, - 0x5c, 0x90, 0xfe, 0x7e, 0x2e, 0x48, 0xbf, 0xbe, 0x14, 0x62, 0x9f, 0x5f, 0x0a, 0xb1, 0xbf, 0x5e, - 0x0a, 0xb1, 0x8f, 0x65, 0x9b, 0xf8, 0x9d, 0x61, 0xab, 0xd4, 0x76, 0xfb, 0xb2, 0x37, 0xa4, 0x1d, - 0x7e, 0x3d, 0xf8, 0xea, 0x1d, 0x5f, 0xbe, 0x73, 0x5c, 0x0b, 0xcb, 0x8f, 0xb2, 0x78, 0x75, 0xd9, - 0xbf, 0x11, 0xad, 0x24, 0xff, 0xe3, 0xfe, 0xcd, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x6c, 0xb8, - 0x11, 0x04, 0x5e, 0x08, 0x00, 0x00, + // 1051 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x56, 0x4d, 0x4f, 0xeb, 0x46, + 0x14, 0x8d, 0x13, 0x08, 0xe4, 0x12, 0xd2, 0x30, 0x04, 0xf0, 0xe3, 0x23, 0x40, 0xde, 0x93, 0x8a, + 0x50, 0x5f, 0xac, 0xb4, 0x2c, 0x2a, 0x76, 0x79, 0xe0, 0x80, 0x05, 0x84, 0xd4, 0x31, 0xa8, 0xef, + 0x6d, 0x5c, 0x27, 0x1e, 0x92, 0x51, 0x12, 0xdb, 0xf2, 0x38, 0x88, 0x74, 0x53, 0xa9, 0xcb, 0xae, + 0xba, 0xec, 0x92, 0x9f, 0xd0, 0x9f, 0xd1, 0xe5, 0x5b, 0x76, 0x59, 0x81, 0xaa, 0x76, 0xd7, 0xbf, + 0x50, 0xcd, 0x8c, 0x93, 0x38, 0x1f, 0x9b, 0x64, 0xe6, 0xdc, 0x73, 0xaf, 0xef, 0x9c, 0x7b, 0x3c, + 0x32, 0xac, 0xf7, 0x03, 0x4a, 0x95, 0xc7, 0x92, 0x12, 0x0c, 0x3c, 0x4c, 0x8b, 0x9e, 0xef, 0x06, + 0x2e, 0x5a, 0x62, 0x60, 0xf1, 0xb1, 0xb4, 0x9d, 0x6f, 0xba, 0xb4, 0xe7, 0x52, 0xa5, 0x61, 0x51, + 0xac, 0x3c, 0x96, 0x1a, 0x38, 0xb0, 0x4a, 0x4a, 0xd3, 0x25, 0x8e, 0x20, 0x6e, 0x6f, 0x85, 0xf1, + 0x1e, 0x6d, 0xb1, 0x1a, 0x3d, 0xda, 0x0a, 0x03, 0xb9, 0x96, 0xdb, 0x72, 0xf9, 0x52, 0x61, 0xab, + 0x10, 0x5d, 0xb3, 0x7a, 0xc4, 0x71, 0x15, 0xfe, 0x2b, 0xa0, 0xc2, 0xb7, 0x90, 0xac, 0x59, 0xbe, + 0xd5, 0xa3, 0x28, 0x07, 0x8b, 0x96, 0xdd, 0x23, 0x8e, 0x2c, 0x1d, 0x48, 0x47, 0x29, 0x5d, 0x6c, + 0x4e, 0xe5, 0xdf, 0x9e, 0xf7, 0x63, 0xff, 0x3e, 0xef, 0x4b, 0xbf, 0xfc, 0xf3, 0xfb, 0xf1, 0x0a, + 0x6f, 0xd6, 0xe3, 0xfc, 0xc2, 0x73, 0x1c, 0x56, 0x0d, 0x4a, 0xaf, 0xf0, 0xa0, 0xe6, 0xbb, 0x4d, + 0x4c, 0x29, 0x3a, 0x81, 0x24, 0x0d, 0xac, 0xa0, 0x4f, 0x79, 0x89, 0xcc, 0xd7, 0xbb, 0xc5, 0xf0, + 0x1c, 0xc5, 0x09, 0x5e, 0x9d, 0x73, 0xf4, 0x90, 0x8b, 0x0a, 0x90, 0xf6, 0x2c, 0x3f, 0x20, 0x4d, + 0xe2, 0x59, 0x4e, 0x40, 0xe5, 0xf8, 0x41, 0xe2, 0x28, 0xa5, 0x4f, 0x60, 0xe8, 0x10, 0xd2, 0x8d, + 0xae, 0xdb, 0xec, 0x98, 0x6d, 0x4c, 0x5a, 0xed, 0x40, 0x4e, 0x1c, 0x48, 0x47, 0x09, 0x7d, 0x85, + 0x63, 0x97, 0x1c, 0x42, 0x6f, 0x61, 0x15, 0x3f, 0x79, 0xc4, 0x1f, 0x0c, 0x39, 0x0b, 0x9c, 0x93, + 0x16, 0x60, 0x48, 0x3a, 0x85, 0xb4, 0x27, 0x9a, 0x30, 0x99, 0xde, 0xf2, 0x22, 0xef, 0x73, 0x2b, + 0xda, 0x67, 0xd8, 0xa4, 0x31, 0xf0, 0xb0, 0xbe, 0xe2, 0x8d, 0x37, 0x28, 0x03, 0x71, 0x62, 0xcb, + 0xc9, 0x03, 0xe9, 0x68, 0x41, 0x8f, 0x13, 0xfb, 0xf4, 0x30, 0xaa, 0x4c, 0x8e, 0x2b, 0x13, 0x50, + 0x6a, 0x76, 0xf0, 0xc0, 0x0c, 0xd3, 0x0a, 0x3f, 0xc7, 0x21, 0x29, 0x8e, 0x8e, 0xf6, 0x00, 0x58, + 0xd4, 0xeb, 0x37, 0x3a, 0x78, 0x10, 0x4a, 0x9c, 0x0a, 0x28, 0xad, 0x71, 0x00, 0x6d, 0x40, 0x92, + 0x25, 0x12, 0x5b, 0x8e, 0x0b, 0xf5, 0x3b, 0x78, 0xa0, 0xd9, 0x33, 0xda, 0x24, 0xe6, 0x68, 0x73, + 0x02, 0x9b, 0x0f, 0xc4, 0xb1, 0xba, 0xe4, 0x47, 0x6c, 0x9b, 0x13, 0x2a, 0x09, 0x05, 0x72, 0xa3, + 0xe8, 0x87, 0x88, 0x5c, 0x45, 0x58, 0xef, 0xe0, 0x41, 0x0b, 0x3b, 0x93, 0x29, 0x8b, 0x3c, 0x65, + 0x4d, 0x84, 0xa2, 0xfc, 0x3d, 0x80, 0xa1, 0x72, 0x23, 0x15, 0x52, 0x21, 0xa2, 0xd9, 0xa7, 0x6f, + 0xa2, 0x62, 0xa4, 0xa3, 0x62, 0x14, 0xfe, 0x8e, 0xc3, 0xb2, 0x41, 0xa9, 0xfa, 0x88, 0x9d, 0x20, + 0x14, 0x51, 0x1a, 0x8a, 0x88, 0x4e, 0x00, 0x30, 0x0b, 0x88, 0x71, 0xc4, 0xf9, 0x38, 0x36, 0xa2, + 0xe3, 0xe0, 0x69, 0x7c, 0x18, 0x29, 0x3c, 0x5c, 0x22, 0x65, 0x64, 0xb4, 0xc4, 0xec, 0x00, 0x79, + 0xc6, 0x94, 0xc7, 0x26, 0xbb, 0x5f, 0x98, 0xea, 0x9e, 0xd9, 0x6b, 0xc6, 0x16, 0xa9, 0xc9, 0xe9, + 0x4f, 0x4f, 0x22, 0x39, 0x67, 0x12, 0x33, 0x16, 0x5c, 0x9a, 0x63, 0xc1, 0x69, 0x2b, 0x2f, 0xcf, + 0x5a, 0x79, 0x6c, 0x86, 0x54, 0xd4, 0x0c, 0x93, 0x16, 0x82, 0x29, 0x0b, 0x15, 0xfe, 0x4b, 0xc0, + 0x6a, 0xa5, 0xef, 0xd8, 0x37, 0xa4, 0xe5, 0x5b, 0x01, 0x71, 0x9d, 0x19, 0xb1, 0x77, 0x01, 0xdc, + 0xae, 0x6d, 0x4e, 0x18, 0x6d, 0xd9, 0xed, 0xda, 0x57, 0xbc, 0xfc, 0x3b, 0xc8, 0xb0, 0x68, 0xe4, + 0x11, 0x09, 0xce, 0x48, 0xbb, 0x5d, 0xdb, 0x18, 0x19, 0xf5, 0x1d, 0x64, 0x9a, 0x7d, 0xdf, 0x67, + 0x23, 0x0b, 0xeb, 0x2c, 0x08, 0x56, 0x88, 0x8a, 0x5a, 0x5f, 0x01, 0x1a, 0xb2, 0x22, 0xf5, 0x84, + 0xac, 0xd9, 0x30, 0x32, 0xae, 0x99, 0x83, 0xc5, 0x66, 0xdb, 0x22, 0x0e, 0xb7, 0x55, 0x4a, 0x17, + 0x9b, 0xc8, 0x6d, 0xb2, 0x34, 0x75, 0x9b, 0x4c, 0x9c, 0x72, 0x6a, 0xd2, 0x5f, 0xc2, 0x17, 0xc4, + 0x21, 0x01, 0xb1, 0x82, 0xe1, 0xdb, 0x10, 0x2a, 0x9c, 0x19, 0xc1, 0xdc, 0xd6, 0x8c, 0xd8, 0x74, + 0x7b, 0x5e, 0x17, 0x8f, 0x89, 0x29, 0x41, 0x1c, 0xc1, 0x82, 0xb8, 0x05, 0x4b, 0xc1, 0x93, 0xd9, + 0xb6, 0x68, 0x3b, 0xd4, 0x3c, 0x19, 0x3c, 0x5d, 0x5a, 0xb4, 0x8d, 0x76, 0x20, 0xd5, 0xb2, 0xa8, + 0xe9, 0xf9, 0xa4, 0x89, 0xe5, 0x15, 0xa1, 0x66, 0xcb, 0xa2, 0x35, 0xb6, 0x1f, 0x06, 0xbb, 0xa4, + 0x47, 0x02, 0x39, 0xcd, 0x47, 0xc0, 0x82, 0xd7, 0x6c, 0xcf, 0x06, 0xd1, 0x2d, 0x99, 0x2c, 0xfe, + 0x80, 0xb1, 0xbc, 0x2a, 0x52, 0xbb, 0xa5, 0x0b, 0x8b, 0x56, 0x30, 0x66, 0x9d, 0x05, 0xbe, 0xe5, + 0xd0, 0x07, 0xec, 0x9b, 0x56, 0xcf, 0xed, 0x3b, 0x81, 0x9c, 0xe1, 0x94, 0xcc, 0x10, 0x2e, 0x73, + 0xf4, 0xb8, 0x03, 0xeb, 0x73, 0x2e, 0x56, 0xb4, 0x03, 0x5b, 0x46, 0xbd, 0x6e, 0x5e, 0xa9, 0x1f, + 0xcd, 0x9a, 0x7e, 0x7b, 0xa6, 0xd6, 0xeb, 0x66, 0x4d, 0xad, 0x9e, 0x6b, 0xd5, 0x8b, 0x6c, 0x6c, + 0x5e, 0xb0, 0x7e, 0x77, 0xc6, 0xfe, 0xb3, 0x12, 0xda, 0x86, 0xcd, 0xe9, 0x60, 0xa5, 0xac, 0x5d, + 0xab, 0xe7, 0xd9, 0xf8, 0xf1, 0x0f, 0x90, 0x99, 0xbc, 0x1d, 0xd1, 0x26, 0x20, 0xc6, 0x1e, 0x32, + 0xaf, 0xd4, 0x8f, 0x17, 0x6a, 0x35, 0x1b, 0x43, 0x5b, 0xb0, 0x1e, 0xc5, 0x75, 0xb5, 0xa2, 0xab, + 0xf5, 0xcb, 0xac, 0x84, 0xf6, 0xe0, 0x4d, 0x34, 0xf0, 0xdd, 0xdd, 0xad, 0x7e, 0x77, 0x63, 0x9e, + 0x5d, 0x96, 0xab, 0x17, 0x6a, 0x36, 0x7e, 0x7c, 0x0d, 0xe9, 0xe8, 0x0b, 0x8f, 0xf6, 0x61, 0x87, + 0xd1, 0xd5, 0x7b, 0xb5, 0x6a, 0x8c, 0x92, 0xb4, 0xaa, 0x66, 0x68, 0x65, 0x43, 0x3d, 0x1f, 0x9f, + 0x45, 0x10, 0x58, 0xd3, 0x15, 0xad, 0x5a, 0xbe, 0xd6, 0x3e, 0xa9, 0xe7, 0x59, 0xe9, 0xf8, 0x9e, + 0xf7, 0x1b, 0xb9, 0x0c, 0x50, 0x0e, 0xb2, 0x63, 0x7a, 0xf9, 0xcc, 0xd0, 0xee, 0xd5, 0x71, 0xb7, + 0x02, 0x3d, 0xbb, 0xbd, 0xa9, 0x5d, 0xab, 0xac, 0xba, 0x84, 0x36, 0x60, 0x6d, 0x1c, 0x50, 0xbf, + 0xaf, 0x69, 0x3a, 0xd7, 0xe1, 0x27, 0x58, 0x9f, 0xe3, 0x3f, 0x74, 0x08, 0x7b, 0x95, 0xbb, 0xea, + 0xb9, 0x79, 0xa3, 0x5d, 0xe8, 0x65, 0x43, 0xbb, 0xad, 0x9a, 0x75, 0xa3, 0x6c, 0xdc, 0x45, 0xa5, + 0x7f, 0x0b, 0xfb, 0xf3, 0x29, 0xd1, 0xa7, 0x1e, 0xc0, 0xee, 0x7c, 0xd2, 0x70, 0x10, 0x1f, 0xae, + 0xfe, 0x78, 0xc9, 0x4b, 0x9f, 0x5f, 0xf2, 0xd2, 0x5f, 0x2f, 0x79, 0xe9, 0xd7, 0xd7, 0x7c, 0xec, + 0xf3, 0x6b, 0x3e, 0xf6, 0xe7, 0x6b, 0x3e, 0xf6, 0xa9, 0xd4, 0x22, 0x41, 0xbb, 0xdf, 0x28, 0x36, + 0xdd, 0x9e, 0xe2, 0xf5, 0x69, 0x9b, 0xbf, 0x47, 0x7c, 0xf5, 0x9e, 0x2f, 0xdf, 0x3b, 0xae, 0x8d, + 0x95, 0x27, 0x45, 0x5c, 0xcf, 0xec, 0x7b, 0xa3, 0x91, 0xe4, 0x5f, 0x01, 0xdf, 0xfc, 0x1f, 0x00, + 0x00, 0xff, 0xff, 0x6d, 0xb7, 0x9c, 0x2f, 0x87, 0x08, 0x00, 0x00, } func (this *Params) Equal(that interface{}) bool { @@ -1082,6 +1091,13 @@ func (m *FundMigration) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.TransferAmount) > 0 { + i -= len(m.TransferAmount) + copy(dAtA[i:], m.TransferAmount) + i = encodeVarintTypes(dAtA, i, uint64(len(m.TransferAmount))) + i-- + dAtA[i] = 0x72 + } if len(m.L1GasFee) > 0 { i -= len(m.L1GasFee) copy(dAtA[i:], m.L1GasFee) @@ -1350,6 +1366,10 @@ func (m *FundMigration) Size() (n int) { if l > 0 { n += 1 + l + sovTypes(uint64(l)) } + l = len(m.TransferAmount) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } return n } @@ -2493,6 +2513,38 @@ func (m *FundMigration) Unmarshal(dAtA []byte) error { } m.L1GasFee = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 14: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TransferAmount", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.TransferAmount = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTypes(dAtA[iNdEx:]) diff --git a/x/uvalidator/abci.go b/x/uvalidator/abci.go index 4a0117db1..5034d5513 100644 --- a/x/uvalidator/abci.go +++ b/x/uvalidator/abci.go @@ -65,6 +65,28 @@ func BeginBlocker(ctx sdk.Context, uvalidatorKeeper keeper.Keeper) error { return nil } +// EndBlocker runs the ballot expiry sweep once per block. +// +// Expiry used to be driven off ballot *creation*, which meant it both scanned +// the whole active set on a consensus hot path and only ran when inbound +// traffic happened to arrive. Running it here decouples expiry from traffic. +// +// A failed sweep is logged and swallowed rather than returned: expiry must +// never halt the chain, and the work is idempotent — anything not expired this +// block is still in PendingByExpiry for the next one. +func EndBlocker(ctx sdk.Context, uvalidatorKeeper keeper.Keeper) error { + defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyEndBlocker) + + if err := uvalidatorKeeper.ExpireBallotsBeforeHeight(ctx, ctx.BlockHeight()); err != nil { + ctx.Logger().Error("uvalidator: ballot expiry sweep failed", + "height", ctx.BlockHeight(), + "err", err.Error(), + ) + } + + return nil +} + // AllocateTokens performs reward and fee distribution to all validators based // on the F1 fee distribution specification. func AllocateTokens(ctx context.Context, totalPreviousPower int64, bondedVotes []abci.VoteInfo, k keeper.Keeper) error { diff --git a/x/uvalidator/keeper/ballot.go b/x/uvalidator/keeper/ballot.go index bafeb3115..da901e361 100644 --- a/x/uvalidator/keeper/ballot.go +++ b/x/uvalidator/keeper/ballot.go @@ -2,13 +2,21 @@ package keeper import ( "context" + "errors" "fmt" + "cosmossdk.io/collections" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/pushchain/push-chain-node/x/uvalidator/types" ) +// MaxExpiriesPerBlock bounds how many ballots a single expiry sweep may +// transition. It keeps the per-block cost of the sweep constant even if a +// large backlog of ballots comes due at once; anything left over stays in +// PendingByExpiry and is picked up by the next block's sweep. +const MaxExpiriesPerBlock = 50 + // CreateBallot creates a new ballot with the given parameters, stores it, and marks it as active. func (k Keeper) CreateBallot( ctx context.Context, @@ -33,10 +41,10 @@ func (k Keeper) CreateBallot( "block_height", blockHeight, ) - // First, expire any old ballots before this height - if err := k.ExpireBallotsBeforeHeight(ctx, blockHeight); err != nil { - return types.Ballot{}, err - } + // NOTE: creation deliberately does NOT sweep expired ballots. That scan used + // to run here on every create and walked the whole active set, paying an + // IAVL read + unmarshal per entry. Expiry now runs once per block in the + // module EndBlocker off the PendingByExpiry index instead. // Create ballot ballot := types.NewBallot( @@ -57,6 +65,9 @@ func (k Keeper) CreateBallot( if err := k.ActiveBallotIDs.Set(ctx, ballot.Id); err != nil { return types.Ballot{}, err } + if err := k.indexPending(ctx, ballot.Id, ballot.BlockHeightExpiry); err != nil { + return types.Ballot{}, err + } k.Logger().Debug("ballot created and marked active", "ballot_id", ballot.Id, @@ -100,9 +111,16 @@ func (k Keeper) SetBallot(ctx context.Context, ballot types.Ballot) error { return k.Ballots.Set(ctx, ballot.Id, ballot) } -// DeleteBallot removes a ballot and its ID from all collections +// DeleteBallot removes a ballot and its ID from all collections. +// +// The PendingByExpiry row is keyed by the ballot's expiry height, so the record +// must be read before it is removed. A missing record means there is nothing to +// unindex (DeleteBallot stays idempotent on absent IDs). func (k Keeper) DeleteBallot(ctx context.Context, id string) error { k.Logger().Debug("deleting ballot", "ballot_id", id) + if ballot, err := k.Ballots.Get(ctx, id); err == nil { + _ = k.unindexPending(ctx, id, ballot.BlockHeightExpiry) + } if err := k.Ballots.Remove(ctx, id); err != nil { return err } @@ -137,6 +155,9 @@ func (k Keeper) MarkBallotExpired(ctx context.Context, id string) error { if err := k.ActiveBallotIDs.Remove(ctx, id); err != nil { return err } + if err := k.unindexPending(ctx, id, ballot.BlockHeightExpiry); err != nil { + return err + } if err := k.ExpiredBallotIDs.Set(ctx, id); err != nil { return err } @@ -175,6 +196,9 @@ func (k Keeper) MarkBallotFinalized(ctx context.Context, id string, status types if err := k.ActiveBallotIDs.Remove(ctx, id); err != nil { return err } + if err := k.unindexPending(ctx, id, ballot.BlockHeightExpiry); err != nil { + return err + } if err := k.FinalizedBallotIDs.Set(ctx, id); err != nil { return err } @@ -229,6 +253,29 @@ func (k Keeper) GetAdmin(ctx context.Context) (string, error) { // downstream UVs must re-vote on the same ballot to trigger finalize+execute // via the normal flow. // +// Only the ballot types on the allow-list below may be recomputed. Recompute +// rebuilds the eligible set from the live UV set and applies the 2/3+1 +// threshold, so it is only correct for ballots that were created that way — +// INBOUND_TX, OUTBOUND_TX and FUND_MIGRATION all call GetEligibleVoters() with +// a (2*N)/3+1 threshold at creation, so recompute reproduces creation exactly +// for them. +// +// TSS_KEY ballots are not such ballots and are refused. They are created with a +// 100% quorum over the DKLS participant set (votesNeeded = len(Participants), +// EligibleVoters = Participants; see x/utss/keeper/voting.go), so a recompute +// would rewrite *both* halves: dropping the threshold from N to (2*N)/3+1 and +// replacing the participants with whoever is a live UV now. The eligible-set +// rewrite is the worse half — it can make validators who never took part in +// that DKLS run eligible to attest its key. A TSS ballot whose participants +// changed is not a quorum problem: the DKLS run itself is invalid, and a +// recomputed threshold would manufacture an attestation nobody made. The fix is +// a fresh keygen round, not a lower bar. +// +// The list is default-deny on purpose. A new ballot type inherits a refusal +// rather than silently inheriting a formula that may not apply to it — which is +// exactly how the TSS case went unnoticed. Adding a type here must be a +// deliberate act, after checking how that type is created. +// // Returns the old/new counts and threshold for the response. func (k Keeper) RecomputeBallotQuorum(ctx context.Context, ballotID string) ( oldEligibleCount, newEligibleCount, oldThreshold, newThreshold int64, @@ -244,6 +291,28 @@ func (k Keeper) RecomputeBallotQuorum(ctx context.Context, ballotID string) ( return 0, 0, 0, 0, 0, fmt.Errorf("ballot %s is not pending (status=%s); only pending ballots can be recomputed", ballotID, ballot.Status.String()) } + // Default-deny allow-list of recomputable ballot types. See the doc comment: + // everything not listed here — TSS_KEY, UNSPECIFIED, and any type added + // later — is refused rather than silently recomputed with a formula that may + // not describe how it was created. + switch ballot.BallotType { + case types.BallotObservationType_BALLOT_OBSERVATION_TYPE_INBOUND_TX, + types.BallotObservationType_BALLOT_OBSERVATION_TYPE_OUTBOUND_TX, + types.BallotObservationType_BALLOT_OBSERVATION_TYPE_FUND_MIGRATION: + // Created from GetEligibleVoters() with a (2*N)/3+1 threshold — recompute + // reproduces creation exactly. + default: + return 0, 0, 0, 0, 0, fmt.Errorf( + "ballot %s has type %s, which cannot be recomputed: recompute rebuilds the eligible-voter "+ + "set from the live universal-validator set and applies the 2/3+1 threshold, which only "+ + "reproduces how INBOUND_TX, OUTBOUND_TX and FUND_MIGRATION ballots are created; a %s "+ + "ballot is created differently, so recomputing it would change what the ballot attests. "+ + "Resolve it through its owning module instead (a TSS_KEY ballot whose participants "+ + "changed needs a fresh keygen round, not a lower threshold)", + ballotID, ballot.BallotType.String(), ballot.BallotType.String(), + ) + } + oldEligibleCount = int64(len(ballot.EligibleVoters)) oldThreshold = ballot.VotingThreshold @@ -317,45 +386,83 @@ func (k Keeper) RecomputeBallotQuorum(ctx context.Context, ballotID string) ( return oldEligibleCount, newEligibleCount, oldThreshold, newThreshold, types.BallotStatus_BALLOT_STATUS_PENDING, nil } -// ExpireBallotsBeforeHeight checks active ballots and marks expired ones. -// It uses a two-phase approach: first collect IDs to expire, then mutate, -// to avoid modifying the ActiveBallotIDs collection during iteration. +// indexPending adds the (expiryHeight, ballotID) row that mirrors an entry in +// ActiveBallotIDs. Every ActiveBallotIDs.Set must be paired with this call, or +// the ballot becomes invisible to the expiry sweep. +func (k Keeper) indexPending(ctx context.Context, id string, expiryHeight int64) error { + return k.PendingByExpiry.Set(ctx, collections.Join(expiryHeight, id)) +} + +// unindexPending drops the (expiryHeight, ballotID) row. Every +// ActiveBallotIDs.Remove must be paired with this call, or the sweep keeps +// re-visiting a ballot that is no longer active. +func (k Keeper) unindexPending(ctx context.Context, id string, expiryHeight int64) error { + return k.PendingByExpiry.Remove(ctx, collections.Join(expiryHeight, id)) +} + +// ExpireBallotsBeforeHeight marks every active ballot whose expiry height is at +// or below currentHeight as expired, up to MaxExpiriesPerBlock per call. +// +// It ranges over the PendingByExpiry index rather than ActiveBallotIDs. Because +// collections.Pair orders by its first component, iteration ends at the first +// row past currentHeight: ballots that are not yet due are never visited, and +// no Ballots.Get is needed to decide whether a ballot is due — the expiry +// height IS the key. Only ballots actually being expired are ever loaded. +// +// It keeps the original two-phase shape: IDs are collected while the iterator +// is open and mutated only after it is closed. Mutating a collection mid- +// iteration skips entries at best and panics at worst. func (k Keeper) ExpireBallotsBeforeHeight(ctx context.Context, currentHeight int64) error { - iter, err := k.ActiveBallotIDs.Iterate(ctx, nil) + // NewPrefixUntilPairRange ends at the prefix-end of currentHeight, so the + // range is inclusive of currentHeight — matching the `<=` expiry semantics. + rng := collections.NewPrefixUntilPairRange[int64, string](currentHeight) + + iter, err := k.PendingByExpiry.Iterate(ctx, rng) if err != nil { return err } - // Phase 1: collect IDs to expire - var toExpire []string + // Phase 1: collect the due index keys, bounded by MaxExpiriesPerBlock. + var due []collections.Pair[int64, string] for ; iter.Valid(); iter.Next() { - id, err := iter.Key() + key, err := iter.Key() if err != nil { iter.Close() return err } - ballot, err := k.Ballots.Get(ctx, id) - if err != nil { - iter.Close() - return err - } - - if ballot.BlockHeightExpiry <= currentHeight { - toExpire = append(toExpire, id) + due = append(due, key) + if len(due) >= MaxExpiriesPerBlock { + break } } // Close iterator explicitly before mutation phase to release the IAVL snapshot iter.Close() - if len(toExpire) > 0 { - k.Logger().Debug("expiring stale ballots", "count", len(toExpire), "current_height", currentHeight) + if len(due) == 0 { + return nil } + k.Logger().Debug("expiring stale ballots", "count", len(due), "current_height", currentHeight) + // Phase 2: expire collected ballots (safe — iterator is closed) - for _, id := range toExpire { - if err := k.MarkBallotExpired(ctx, id); err != nil { + for _, key := range due { + id := key.K2() + err := k.MarkBallotExpired(ctx, id) + switch { + case err == nil: + case errors.Is(err, collections.ErrNotFound): + // Defensive: an index row whose ballot record no longer exists must + // not wedge the sweep every block. Drop the orphan row and continue. + k.Logger().Warn("dropping orphaned ballot expiry index entry", + "ballot_id", id, + "expiry_height", key.K1(), + ) + if rErr := k.PendingByExpiry.Remove(ctx, key); rErr != nil { + return rErr + } + default: return err } } diff --git a/x/uvalidator/keeper/ballot_test.go b/x/uvalidator/keeper/ballot_test.go index f6432003d..0c525bcc9 100644 --- a/x/uvalidator/keeper/ballot_test.go +++ b/x/uvalidator/keeper/ballot_test.go @@ -4,10 +4,46 @@ import ( "fmt" "testing" + "cosmossdk.io/collections" + "github.com/pushchain/push-chain-node/x/uvalidator/keeper" "github.com/pushchain/push-chain-node/x/uvalidator/types" "github.com/stretchr/testify/require" ) +const inboundBallot = types.BallotObservationType_BALLOT_OBSERVATION_TYPE_INBOUND_TX + +// pendingIndexed reports whether the (expiryHeight, ballotID) row exists in the +// PendingByExpiry index. +func pendingIndexed(t *testing.T, f *testFixture, id string, expiryHeight int64) bool { + t.Helper() + has, err := f.k.PendingByExpiry.Has(f.ctx, collections.Join(expiryHeight, id)) + require.NoError(t, err) + return has +} + +// pendingIndexIDs returns every ballot ID currently carried by the expiry index. +func pendingIndexIDs(t *testing.T, f *testFixture) []string { + t.Helper() + ids := []string{} + require.NoError(t, f.k.PendingByExpiry.Walk(f.ctx, nil, + func(key collections.Pair[int64, string]) (bool, error) { + ids = append(ids, key.K2()) + return false, nil + })) + return ids +} + +// expiredCount returns how many ballots sit in ExpiredBallotIDs. +func expiredCount(t *testing.T, f *testFixture) int { + t.Helper() + n := 0 + require.NoError(t, f.k.ExpiredBallotIDs.Walk(f.ctx, nil, func(string) (bool, error) { + n++ + return false, nil + })) + return n +} + func TestCreateAndGetBallot(t *testing.T) { f := SetupTest(t) require := require.New(t) @@ -150,32 +186,48 @@ func TestExpireBallotsBeforeHeight(t *testing.T) { require.Equal(types.BallotStatus_BALLOT_STATUS_PENDING, got2.Status) } -func TestCreateBallot_ExpiresOldOnCreate(t *testing.T) { +// Creation must no longer sweep expired ballots: that scan walked the whole +// active set on a consensus hot path. Expiry moved to the module EndBlocker. +func TestCreateBallot_DoesNotExpireOnCreate(t *testing.T) { f := SetupTest(t) require := require.New(t) - // Create a ballot that expires quickly (expiry = 1 block) + // Create a ballot that comes due quickly (expiry = 1 block) oldBallot, err := f.k.CreateBallot(f.ctx, "old", types.BallotObservationType_BALLOT_OBSERVATION_TYPE_INBOUND_TX, []string{"v1"}, 1, 1) require.NoError(err) - // Manually simulate advancing block height + // Advance well past the old ballot's expiry height f.ctx = f.ctx.WithBlockHeight(oldBallot.BlockHeightCreated + 5) - // Now create a NEW ballot → should trigger expiry cleanup of the old one + // Creating a NEW ballot must NOT expire the due one — no scan on create. newBallot, err := f.k.CreateBallot(f.ctx, "new", types.BallotObservationType_BALLOT_OBSERVATION_TYPE_INBOUND_TX, []string{"v2"}, 1, 10) require.NoError(err) - - // New ballot must be created fine require.Equal("new", newBallot.Id) - // Old ballot should now be expired got, err := f.k.GetBallot(f.ctx, "old") require.NoError(err) + require.Equal(types.BallotStatus_BALLOT_STATUS_PENDING, got.Status, + "CreateBallot must not expire ballots; expiry belongs to the EndBlocker sweep") + + has, err := f.k.ActiveBallotIDs.Has(f.ctx, "old") + require.NoError(err) + require.True(has, "due ballot must stay active until the sweep runs") + + // The sweep — not creation — is what expires it. + require.NoError(f.k.ExpireBallotsBeforeHeight(f.ctx, f.ctx.BlockHeight())) + + got, err = f.k.GetBallot(f.ctx, "old") + require.NoError(err) require.Equal(types.BallotStatus_BALLOT_STATUS_EXPIRED, got.Status) + + // The not-yet-due ballot survives the sweep. + gotNew, err := f.k.GetBallot(f.ctx, "new") + require.NoError(err) + require.Equal(types.BallotStatus_BALLOT_STATUS_PENDING, gotNew.Status) } func TestCreateBallot_NoExpiryTriggered(t *testing.T) { @@ -207,7 +259,7 @@ func TestCreateBallot_NoExpiryTriggered(t *testing.T) { require.Equal("newer", got2.Id) } -func TestCreateBallot_ExpiresMultipleOld(t *testing.T) { +func TestCreateBallot_DoesNotExpireMultipleOldOnCreate(t *testing.T) { f := SetupTest(t) require := require.New(t) @@ -225,19 +277,30 @@ func TestCreateBallot_ExpiresMultipleOld(t *testing.T) { // Advance height beyond both expiries f.ctx = f.ctx.WithBlockHeight(b2.BlockHeightCreated + 5) - // Create fresh ballot (triggers cleanup) + // Create fresh ballot — must NOT trigger any cleanup _, err = f.k.CreateBallot(f.ctx, "fresh", types.BallotObservationType_BALLOT_OBSERVATION_TYPE_INBOUND_TX, []string{"v3"}, 1, 5) require.NoError(err) - // Both old ballots should now be expired + // Both due ballots must still be pending — creation does not sweep got1, err := f.k.GetBallot(f.ctx, b1.Id) require.NoError(err) - require.Equal(types.BallotStatus_BALLOT_STATUS_EXPIRED, got1.Status) + require.Equal(types.BallotStatus_BALLOT_STATUS_PENDING, got1.Status) got2, err := f.k.GetBallot(f.ctx, b2.Id) require.NoError(err) + require.Equal(types.BallotStatus_BALLOT_STATUS_PENDING, got2.Status) + + // The sweep expires them. + require.NoError(f.k.ExpireBallotsBeforeHeight(f.ctx, f.ctx.BlockHeight())) + + got1, err = f.k.GetBallot(f.ctx, b1.Id) + require.NoError(err) + require.Equal(types.BallotStatus_BALLOT_STATUS_EXPIRED, got1.Status) + + got2, err = f.k.GetBallot(f.ctx, b2.Id) + require.NoError(err) require.Equal(types.BallotStatus_BALLOT_STATUS_EXPIRED, got2.Status) } @@ -401,3 +464,182 @@ func TestExpireBallotsBeforeHeight_EmptySet(t *testing.T) { err := f.k.ExpireBallotsBeforeHeight(f.ctx, 100) require.NoError(err) } + +// ─── PendingByExpiry index ─────────────────────────────────────────────────── + +// TestPendingByExpiryIndex_MirrorsEveryActiveSetWriter is the guard against a +// missed mirror site. Every writer of ActiveBallotIDs must write PendingByExpiry +// too; a miss makes the index drift from reality, which either hides a ballot +// from the sweep forever or lets the sweep act on a ballot that is no longer +// active. Each subtest asserts the index state FIRST, so the assertion that +// catches the missing mirror runs before anything else can abort the subtest. +func TestPendingByExpiryIndex_MirrorsEveryActiveSetWriter(t *testing.T) { + t.Run("CreateBallot indexes under the expiry height", func(t *testing.T) { + f := SetupTest(t) + + b, err := f.k.CreateBallot(f.ctx, "idx-create", inboundBallot, []string{"v1"}, 1, 7) + require.NoError(t, err) + + require.True(t, pendingIndexed(t, f, "idx-create", b.BlockHeightExpiry), + "CreateBallot must mirror ActiveBallotIDs into PendingByExpiry") + require.Equal(t, []string{"idx-create"}, pendingIndexIDs(t, f)) + + // The row must be keyed by the expiry height, not the creation height — + // that is the whole point of the index. + require.NotEqual(t, b.BlockHeightCreated, b.BlockHeightExpiry) + require.False(t, pendingIndexed(t, f, "idx-create", b.BlockHeightCreated), + "index must be keyed by expiry height, not creation height") + }) + + t.Run("MarkBallotExpired unindexes", func(t *testing.T) { + f := SetupTest(t) + + b, err := f.k.CreateBallot(f.ctx, "idx-expire", inboundBallot, []string{"v1"}, 1, 3) + require.NoError(t, err) + require.NoError(t, f.k.MarkBallotExpired(f.ctx, b.Id)) + + require.False(t, pendingIndexed(t, f, b.Id, b.BlockHeightExpiry), + "MarkBallotExpired must remove the PendingByExpiry row") + require.Empty(t, pendingIndexIDs(t, f)) + + // A leaked row would be re-processed by the sweep every single block, + // permanently consuming part of the per-block budget. + require.NoError(t, f.k.ExpireBallotsBeforeHeight(f.ctx, b.BlockHeightExpiry+1)) + require.Empty(t, pendingIndexIDs(t, f)) + }) + + t.Run("MarkBallotFinalized unindexes", func(t *testing.T) { + f := SetupTest(t) + + b, err := f.k.CreateBallot(f.ctx, "idx-final", inboundBallot, []string{"v1"}, 1, 3) + require.NoError(t, err) + require.NoError(t, f.k.MarkBallotFinalized(f.ctx, b.Id, types.BallotStatus_BALLOT_STATUS_PASSED)) + + require.False(t, pendingIndexed(t, f, b.Id, b.BlockHeightExpiry), + "MarkBallotFinalized must remove the PendingByExpiry row") + + // Behavioural consequence of a leaked row: the sweep would reach a + // finalized ballot and overwrite PASSED with EXPIRED. + require.NoError(t, f.k.ExpireBallotsBeforeHeight(f.ctx, b.BlockHeightExpiry+1)) + got, err := f.k.GetBallot(f.ctx, b.Id) + require.NoError(t, err) + require.Equal(t, types.BallotStatus_BALLOT_STATUS_PASSED, got.Status, + "a stale index row let the sweep overwrite a finalized ballot") + }) + + t.Run("DeleteBallot unindexes", func(t *testing.T) { + f := SetupTest(t) + + b, err := f.k.CreateBallot(f.ctx, "idx-delete", inboundBallot, []string{"v1"}, 1, 3) + require.NoError(t, err) + require.NoError(t, f.k.DeleteBallot(f.ctx, b.Id)) + + require.False(t, pendingIndexed(t, f, b.Id, b.BlockHeightExpiry), + "DeleteBallot must remove the PendingByExpiry row") + require.Empty(t, pendingIndexIDs(t, f)) + + // Deleting an absent ballot stays a no-op. + require.NoError(t, f.k.DeleteBallot(f.ctx, b.Id)) + require.Empty(t, pendingIndexIDs(t, f)) + }) + + t.Run("VoteOnBallot create path indexes the new ballot", func(t *testing.T) { + f := SetupTest(t) + + b, _, isNew, err := f.k.VoteOnBallot(f.ctx, "idx-vote", inboundBallot, + "v1", types.VoteResult_VOTE_RESULT_SUCCESS, + []string{"v1", "v2"}, 2, 9) + require.NoError(t, err) + require.True(t, isNew) + + require.True(t, pendingIndexed(t, f, "idx-vote", b.BlockHeightExpiry), + "the vote-driven create path must leave the expiry index populated") + }) +} + +// TestExpireBallotsBeforeHeight_OnlyDueRowsAreTouched pins the range semantics: +// rows past currentHeight are neither expired nor dropped from the index. +func TestExpireBallotsBeforeHeight_OnlyDueRowsAreTouched(t *testing.T) { + f := SetupTest(t) + require := require.New(t) + + // Created at height 0 → expiry heights 3, 10 and exactly 5. + due, err := f.k.CreateBallot(f.ctx, "due", inboundBallot, []string{"v1"}, 1, 3) + require.NoError(err) + atBoundary, err := f.k.CreateBallot(f.ctx, "at-boundary", inboundBallot, []string{"v1"}, 1, 5) + require.NoError(err) + future, err := f.k.CreateBallot(f.ctx, "future", inboundBallot, []string{"v1"}, 1, 10) + require.NoError(err) + + require.NoError(f.k.ExpireBallotsBeforeHeight(f.ctx, 5)) + + // The not-yet-due row must survive in the index for a later block. + require.Equal([]string{"future"}, pendingIndexIDs(t, f)) + require.True(pendingIndexed(t, f, future.Id, future.BlockHeightExpiry)) + + gotFuture, err := f.k.GetBallot(f.ctx, future.Id) + require.NoError(err) + require.Equal(types.BallotStatus_BALLOT_STATUS_PENDING, gotFuture.Status) + + // Expiry is inclusive of currentHeight (`<=` semantics). + for _, id := range []string{due.Id, atBoundary.Id} { + got, err := f.k.GetBallot(f.ctx, id) + require.NoError(err) + require.Equal(types.BallotStatus_BALLOT_STATUS_EXPIRED, got.Status, "ballot %s should be expired", id) + } +} + +// TestExpireBallotsBeforeHeight_CapsAtMaxExpiriesPerBlock proves the per-block +// bound holds and that the leftovers are carried in the index to the next block. +func TestExpireBallotsBeforeHeight_CapsAtMaxExpiriesPerBlock(t *testing.T) { + f := SetupTest(t) + require := require.New(t) + + const extra = 7 + total := keeper.MaxExpiriesPerBlock + extra + + // All due at height 1, i.e. the whole backlog comes due at once. + for i := 0; i < total; i++ { + _, err := f.k.CreateBallot(f.ctx, fmt.Sprintf("cap-%03d", i), inboundBallot, []string{"v1"}, 1, 1) + require.NoError(err) + } + require.Len(pendingIndexIDs(t, f), total) + + // First sweep: exactly MaxExpiriesPerBlock, never the whole backlog. + require.NoError(f.k.ExpireBallotsBeforeHeight(f.ctx, 100)) + require.Equal(keeper.MaxExpiriesPerBlock, expiredCount(t, f), + "a single sweep must expire at most MaxExpiriesPerBlock ballots") + require.Len(pendingIndexIDs(t, f), extra, + "the remainder must stay in the index for the next block") + + // Second sweep drains the rest. + require.NoError(f.k.ExpireBallotsBeforeHeight(f.ctx, 100)) + require.Equal(total, expiredCount(t, f)) + require.Empty(pendingIndexIDs(t, f)) +} + +// TestExpireBallotsBeforeHeight_OrphanedIndexRow covers the defensive path: an +// index row whose ballot record is gone must be dropped instead of wedging the +// sweep on every subsequent block. +func TestExpireBallotsBeforeHeight_OrphanedIndexRow(t *testing.T) { + f := SetupTest(t) + require := require.New(t) + + b, err := f.k.CreateBallot(f.ctx, "orphan", inboundBallot, []string{"v1"}, 1, 1) + require.NoError(err) + + // Drop the record behind the index row's back. + require.NoError(f.k.Ballots.Remove(f.ctx, b.Id)) + require.True(pendingIndexed(t, f, b.Id, b.BlockHeightExpiry)) + + // A healthy ballot queued behind the orphan must still get expired. + good, err := f.k.CreateBallot(f.ctx, "good", inboundBallot, []string{"v1"}, 1, 2) + require.NoError(err) + + require.NoError(f.k.ExpireBallotsBeforeHeight(f.ctx, 100)) + + require.Empty(pendingIndexIDs(t, f), "the orphaned row must be dropped, not retried forever") + gotGood, err := f.k.GetBallot(f.ctx, good.Id) + require.NoError(err) + require.Equal(types.BallotStatus_BALLOT_STATUS_EXPIRED, gotGood.Status) +} diff --git a/x/uvalidator/keeper/genesis_test.go b/x/uvalidator/keeper/genesis_test.go index b052f32ba..0f025f0c4 100755 --- a/x/uvalidator/keeper/genesis_test.go +++ b/x/uvalidator/keeper/genesis_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "testing" + "cosmossdk.io/collections" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/pushchain/push-chain-node/x/uvalidator/types" "github.com/stretchr/testify/require" @@ -64,3 +65,65 @@ func TestGenesisExportImportRoundTrip(t *testing.T) { require.Equal(t, len(exported.ActiveBallotIds), len(reExported.ActiveBallotIds)) require.Equal(t, exported.UniversalValidators[0].Key, reExported.UniversalValidators[0].Key) } + +// TestInitGenesisRebuildsPendingByExpiry pins the reason this change needs no +// state migration: the expiry index is derived state that InitGenesis rebuilds +// from the ballots it just restored. +func TestInitGenesisRebuildsPendingByExpiry(t *testing.T) { + f := SetupTest(t) + f.k.InitGenesis(f.ctx, &types.GenesisState{Params: types.Params{Admin: f.addrs[0].String()}}) + + genesis := &types.GenesisState{ + Params: types.Params{Admin: f.addrs[0].String()}, + Ballots: []types.Ballot{ + {Id: "g-1", Status: types.BallotStatus_BALLOT_STATUS_PENDING, BlockHeightExpiry: 42}, + {Id: "g-2", Status: types.BallotStatus_BALLOT_STATUS_PENDING, BlockHeightExpiry: 7}, + {Id: "g-3", Status: types.BallotStatus_BALLOT_STATUS_PASSED, BlockHeightExpiry: 9}, + }, + ActiveBallotIds: []string{"g-1", "g-2"}, + FinalizedBallotIds: []string{"g-3"}, + } + + f2 := SetupTest(t) + require.NoError(t, f2.k.InitGenesis(f2.ctx, genesis)) + + // Both active ballots are indexed under their own expiry heights. + for _, tc := range []struct { + id string + expiry int64 + }{{"g-1", 42}, {"g-2", 7}} { + has, err := f2.k.PendingByExpiry.Has(f2.ctx, collections.Join(tc.expiry, tc.id)) + require.NoError(t, err) + require.True(t, has, "InitGenesis must rebuild the expiry index for %s", tc.id) + } + + // The finalized ballot is not active, so it must not be indexed. + has, err := f2.k.PendingByExpiry.Has(f2.ctx, collections.Join(int64(9), "g-3")) + require.NoError(t, err) + require.False(t, has, "only active ballots belong in the expiry index") + + // The rebuilt index drives the sweep: g-2 (expiry 7) is due at height 10, + // g-1 (expiry 42) is not. + require.NoError(t, f2.k.ExpireBallotsBeforeHeight(f2.ctx, 10)) + + got, err := f2.k.GetBallot(f2.ctx, "g-2") + require.NoError(t, err) + require.Equal(t, types.BallotStatus_BALLOT_STATUS_EXPIRED, got.Status) + + got, err = f2.k.GetBallot(f2.ctx, "g-1") + require.NoError(t, err) + require.Equal(t, types.BallotStatus_BALLOT_STATUS_PENDING, got.Status) +} + +// TestInitGenesisRejectsDanglingActiveBallotID: an active ballot id with no +// ballot record cannot be indexed, so it is surfaced rather than silently +// dropped into an index that no longer matches the active set. +func TestInitGenesisRejectsDanglingActiveBallotID(t *testing.T) { + f := SetupTest(t) + err := f.k.InitGenesis(f.ctx, &types.GenesisState{ + Params: types.Params{Admin: f.addrs[0].String()}, + ActiveBallotIds: []string{"ghost"}, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "ghost") +} diff --git a/x/uvalidator/keeper/keeper.go b/x/uvalidator/keeper/keeper.go index 2345d4382..c2603181c 100755 --- a/x/uvalidator/keeper/keeper.go +++ b/x/uvalidator/keeper/keeper.go @@ -2,6 +2,7 @@ package keeper import ( "context" + "fmt" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -33,6 +34,14 @@ type Keeper struct { ExpiredBallotIDs collections.KeySet[string] // set of ballot IDs that have expired (not yet pruned) FinalizedBallotIDs collections.KeySet[string] // set of ballot IDs that are PASSED or REJECTED + // PendingByExpiry is a secondary index over ActiveBallotIDs keyed by + // (expiryHeight, ballotID). collections.Pair orders by the first component, + // so the expiry sweep can range over [0, currentHeight] and stop at the + // first entry beyond it — ballots that are not due are never visited, and + // the height being part of the key means no Ballots.Get is needed to decide + // whether a ballot is due. Every ActiveBallotIDs writer must mirror here. + PendingByExpiry collections.KeySet[collections.Pair[int64, string]] + StakingKeeper types.StakingKeeper SlashingKeeper types.SlashingKeeper UtssKeeper types.UtssKeeper @@ -98,6 +107,10 @@ func NewKeeper( sb, types.FinalizedBallotIDsKey, types.FinalizedBallotIDsName, collections.StringKey, ), + PendingByExpiry: collections.NewKeySet( + sb, types.PendingByExpiryKey, types.PendingByExpiryName, + collections.PairKeyCodec(collections.Int64Key, collections.StringKey), + ), authority: authority, StakingKeeper: stakingKeeper, @@ -144,11 +157,20 @@ func (k *Keeper) InitGenesis(ctx context.Context, data *types.GenesisState) erro } } - // Restore ActiveBallotIDs + // Restore ActiveBallotIDs, rebuilding the PendingByExpiry index from the + // ballots restored just above. This is why no state migration is needed for + // a chain that starts from (or is re-imported through) genesis. for _, id := range data.ActiveBallotIds { + ballot, err := k.Ballots.Get(ctx, id) + if err != nil { + return fmt.Errorf("active ballot id %q has no matching ballot record in genesis: %w", id, err) + } if err := k.ActiveBallotIDs.Set(ctx, id); err != nil { return err } + if err := k.PendingByExpiry.Set(ctx, collections.Join(ballot.BlockHeightExpiry, id)); err != nil { + return err + } } // Restore ExpiredBallotIDs diff --git a/x/uvalidator/keeper/validator.go b/x/uvalidator/keeper/validator.go index 2ebddaa26..164da2f01 100644 --- a/x/uvalidator/keeper/validator.go +++ b/x/uvalidator/keeper/validator.go @@ -50,7 +50,7 @@ func (k Keeper) GetValidatorsByStatus(ctx context.Context, status types.UVStatus // // Eligibility requires BOTH: // - UV lifecycle status is ACTIVE or PENDING_JOIN; AND -// - the underlying Cosmos staking validator is bonded and not tombstoned. +// - the underlying Cosmos staking validator is bonded, not jailed and not tombstoned. // // The staking-state filter prevents stranded UVs (still ACTIVE on paper but // unbonded/jailed/tombstoned on the base chain) from inflating the ballot @@ -80,6 +80,18 @@ func (k Keeper) GetEligibleVoters(ctx context.Context) ([]types.UniversalValidat if !sv.IsBonded() { return false, nil } + // A jailed validator is NOT covered by the IsBonded() check above. + // Cosmos SDK's jailValidator sets Validator.Jailed and deletes the + // power index but never touches Validator.Status, and IsBonded() is + // only `GetStatus() == Bonded`. Slashing jails during BeginBlock while + // the bonded -> unbonding transition happens in staking's EndBlocker, + // so for the whole tx-processing phase in between a jailed validator + // still reports IsBonded() == true. Without this gate it is snapshotted + // into a ballot's EligibleVoters and inflates the threshold + // denominator ((2*N)/3 + 1), which strands the ballot at N <= 3. + if sv.IsJailed() { + return false, nil + } consAddr, caErr := sv.GetConsAddr() if caErr != nil { k.Logger().Debug("eligible voter filter: GetConsAddr failed", "validator", addr.String(), "err", caErr) diff --git a/x/uvalidator/keeper/voting.go b/x/uvalidator/keeper/voting.go index c0adb980b..adecb7cbc 100644 --- a/x/uvalidator/keeper/voting.go +++ b/x/uvalidator/keeper/voting.go @@ -181,6 +181,12 @@ func (k Keeper) VoteOnBallot( if err != nil { return ballot, false, false, errors.Wrap(err, "Error while voting on the ballot") } + // Mirror the active-set write into the expiry index. CreateBallot has + // already written both; both writes are idempotent, and pairing them + // here keeps the invariant local to every ActiveBallotIDs.Set site. + if err := k.indexPending(ctx, id, ballot.BlockHeightExpiry); err != nil { + return ballot, false, false, errors.Wrap(err, "Error while voting on the ballot") + } } ballot, err = k.AddVoteToBallot(ctx, ballot, voter, voteResult) diff --git a/x/uvalidator/module.go b/x/uvalidator/module.go index c24dc10e7..420475a52 100755 --- a/x/uvalidator/module.go +++ b/x/uvalidator/module.go @@ -11,6 +11,7 @@ import ( abci "github.com/cometbft/cometbft/abci/types" "cosmossdk.io/client/v2/autocli" + "cosmossdk.io/core/appmodule" errorsmod "cosmossdk.io/errors" "github.com/cosmos/cosmos-sdk/client" @@ -34,6 +35,13 @@ var ( _ module.AppModuleGenesis = AppModule{} _ module.AppModule = AppModule{} + // The module manager only calls BeginBlock/EndBlock on modules that satisfy + // these interfaces — being listed in SetOrderBeginBlockers/EndBlockers is + // necessary but not sufficient. These assertions fail the build if a + // signature drifts and the hook silently stops firing. + _ appmodule.HasBeginBlocker = AppModule{} + _ appmodule.HasEndBlocker = AppModule{} + _ autocli.HasAutoCLIConfig = AppModule{} ) @@ -187,3 +195,9 @@ func (a AppModule) BeginBlock(ctx context.Context) error { return BeginBlocker(sdkCtx, a.keeper) } + +func (a AppModule) EndBlock(ctx context.Context) error { + sdkCtx := sdk.UnwrapSDKContext(ctx) + + return EndBlocker(sdkCtx, a.keeper) +} diff --git a/x/uvalidator/types/ballot.go b/x/uvalidator/types/ballot.go index 03f37d71f..2b11e1531 100644 --- a/x/uvalidator/types/ballot.go +++ b/x/uvalidator/types/ballot.go @@ -45,6 +45,44 @@ func (b Ballot) AddVote(address string, vote VoteResult) (Ballot, error) { return b, nil } +// HasUnvotedEligibleVoter reports whether at least one eligible voter still +// holds a NOT_YET_VOTED slot, i.e. whether AddVote can still succeed for +// somebody. +// +// A ballot whose Votes slice is shorter than EligibleVoters is malformed; the +// missing slots are counted as unvoted. That is the conservative answer for +// every caller here, and it matches HasVoted, which would panic indexing them. +func (b Ballot) HasUnvotedEligibleVoter() bool { + for i := range b.EligibleVoters { + if i >= len(b.Votes) || b.Votes[i] == VoteResult_VOTE_RESULT_NOT_YET_VOTED { + return true + } + } + return false +} + +// IsUnreachablePending reports whether the ballot is stored PENDING yet is +// terminal in fact: every eligible voter has already voted, so AddVote can +// never fire again (it rejects repeat votes) and no further vote event can move +// the ballot to PASSED or REJECTED. Such a ballot stays PENDING forever unless +// its eligible-voter set is rebuilt. See F-2026-18147. +// +// A ballot with no eligible voters at all is deliberately NOT reported as +// unreachable. RecomputeBallotQuorum rebuilds the voter list from the live +// universal-validator set, so an empty ballot either gains real voters and +// becomes votable or is auto-expired by that same call; a shipped path already +// resolves it. An empty voter list is also evidence of a malformed ballot +// rather than of a completed vote. +func (b Ballot) IsUnreachablePending() bool { + if b.Status != BallotStatus_BALLOT_STATUS_PENDING { + return false + } + if len(b.EligibleVoters) == 0 { + return false + } + return !b.HasUnvotedEligibleVoter() +} + // CountVotes counts the YES and NO votes in the ballot. func (b Ballot) CountVotes() (yes, no int) { for _, v := range b.Votes { diff --git a/x/uvalidator/types/ballot_test.go b/x/uvalidator/types/ballot_test.go index 258a14381..201e24983 100644 --- a/x/uvalidator/types/ballot_test.go +++ b/x/uvalidator/types/ballot_test.go @@ -188,3 +188,64 @@ func TestIsFinalizingVote(t *testing.T) { _, done = b.IsFinalizingVote() require.False(t, done) } + +// TestIsUnreachablePending pins the F-2026-18147 predicate: a PENDING ballot +// whose every eligible voter has already voted can never receive another vote +// (AddVote rejects repeats), so it is terminal in fact. +func TestIsUnreachablePending(t *testing.T) { + voted := func(v ...VoteResult) Ballot { + return Ballot{ + Id: "b", + Status: BallotStatus_BALLOT_STATUS_PENDING, + EligibleVoters: []string{"addr1", "addr2", "addr3"}[:len(v)], + Votes: v, + VotingThreshold: 2, + } + } + + // Every slot filled → unreachable, regardless of the vote arithmetic. + require.True(t, voted( + VoteResult_VOTE_RESULT_SUCCESS, + VoteResult_VOTE_RESULT_SUCCESS, + VoteResult_VOTE_RESULT_SUCCESS, + ).IsUnreachablePending(), "YES above threshold but nobody left to vote") + require.True(t, voted( + VoteResult_VOTE_RESULT_SUCCESS, + VoteResult_VOTE_RESULT_FAILURE, + VoteResult_VOTE_RESULT_FAILURE, + ).IsUnreachablePending(), "YES below threshold and nobody left to vote") + + // One slot still open → the ballot can still finalize normally. + require.False(t, voted( + VoteResult_VOTE_RESULT_SUCCESS, + VoteResult_VOTE_RESULT_SUCCESS, + VoteResult_VOTE_RESULT_NOT_YET_VOTED, + ).IsUnreachablePending()) + + // Only PENDING ballots qualify; terminal ones are fully voted by + // construction and must not be swept in. + for _, st := range []BallotStatus{ + BallotStatus_BALLOT_STATUS_PASSED, + BallotStatus_BALLOT_STATUS_REJECTED, + BallotStatus_BALLOT_STATUS_EXPIRED, + } { + b := voted(VoteResult_VOTE_RESULT_SUCCESS, VoteResult_VOTE_RESULT_SUCCESS) + b.Status = st + require.False(t, b.IsUnreachablePending(), "status %s must not be reported as unreachable-pending", st) + } + + // Degenerate ballots are refused rather than swept in: an empty voter list + // is a malformed ballot, and RecomputeBallotQuorum resolves it by rebuilding + // the list from the live UV set (or auto-expiring at zero). + require.False(t, Ballot{Status: BallotStatus_BALLOT_STATUS_PENDING}.IsUnreachablePending()) + + // A Votes slice shorter than EligibleVoters is malformed too; the missing + // slots count as unvoted, which is the conservative answer. + short := Ballot{ + Status: BallotStatus_BALLOT_STATUS_PENDING, + EligibleVoters: []string{"addr1", "addr2"}, + Votes: []VoteResult{VoteResult_VOTE_RESULT_SUCCESS}, + } + require.True(t, short.HasUnvotedEligibleVoter()) + require.False(t, short.IsUnreachablePending()) +} diff --git a/x/uvalidator/types/keys.go b/x/uvalidator/types/keys.go index 9f0fb92da..1b9ef7d30 100755 --- a/x/uvalidator/types/keys.go +++ b/x/uvalidator/types/keys.go @@ -49,6 +49,13 @@ var ( // FinalizedBallotIDsName is the name of the finalized ballot IDs set. FinalizedBallotIDsName = "finalized_ballot_ids" + + // PendingByExpiryKey is the key for the (expiryHeight, ballotID) index over + // the active ballot set. Next free prefix after FinalizedBallotIDsKey(6). + PendingByExpiryKey = collections.NewPrefix(7) + + // PendingByExpiryName is the name of the pending-by-expiry index. + PendingByExpiryName = "pending_ballots_by_expiry" ) const (