diff --git a/client/v2/CHANGELOG.md b/client/v2/CHANGELOG.md index 2287e6bd07cc..090ff67e362e 100644 --- a/client/v2/CHANGELOG.md +++ b/client/v2/CHANGELOG.md @@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* [#18626](https://github.com/cosmos/cosmos-sdk/pull/18626) Support for off-chain signing and verification of a file. * [#18461](https://github.com/cosmos/cosmos-sdk/pull/18461) Support governance proposals. ### API Breaking Changes diff --git a/client/v2/README.md b/client/v2/README.md index 5830667f644b..3e94e2c462ea 100644 --- a/client/v2/README.md +++ b/client/v2/README.md @@ -216,3 +216,60 @@ https://github.com/cosmos/cosmos-sdk/blob/main/client/grpc/cmtservice/autocli.go To further enhance your CLI experience with Cosmos SDK-based blockchains, you can use `hubl`. `hubl` is a tool that allows you to query any Cosmos SDK-based blockchain using the new AutoCLI feature of the Cosmos SDK. With `hubl`, you can easily configure a new chain and query modules with just a few simple commands. For more information on `hubl`, including how to configure a new chain and query a module, see the [Hubl documentation](https://docs.cosmos.network/main/tooling/hubl). + +# Off-Chain + +Off-chain functionalities allow you to sign and verify files with two commands: ++ `sign-file` for signing a file. ++ `verify-file` for verifying a previously signed file. + +Signing a file will result in a Tx with a `MsgSignArbitraryData` as described in the [Off-chain CIP](https://github.com/cosmos/cips/blob/main/cips/cip-X.md). + +## Sign a file + +To sign a file `sign-file` command offers some helpful flags: +```text + --encoding string Choose an encoding method for the file content to be added as msg data (no-encoding|base64|hex) (default "no-encoding") + --indent string Choose an indent for the tx (default " ") + --notEmitUnpopulated Don't show unpopulated fields in the tx + --output string Choose an output format for the tx (json|text (default "json") + --output-document string The document will be written to the given file instead of STDOUT +``` + +The `encoding` flag lets you choose how the contents of the file should be encoded. For example: ++ `simd off-chain sign-file alice myFile.json` + + ```json + { + "@type": "/offchain.MsgSignArbitraryData", + "appDomain": "simd", + "signer": "cosmos1x33fy6rusfprkntvjsfregss7rvsvyy4lkwrqu", + "data": "Hello World!\n" + } + ``` ++ `simd off-chain sign-file alice myFile.json --encoding base64` + + ```json + { + "@type": "/offchain.MsgSignArbitraryData", + "appDomain": "simd", + "signer": "cosmos1x33fy6rusfprkntvjsfregss7rvsvyy4lkwrqu", + "data": "SGVsbG8gV29ybGQhCg==" + } + ``` ++ `simd off-chain sign-file alice myFile.json --encoding hex` + + ```json + { + "@type": "/offchain.MsgSignArbitraryData", + "appDomain": "simd", + "signer": "cosmos1x33fy6rusfprkntvjsfregss7rvsvyy4lkwrqu", + "data": "48656c6c6f20576f726c64210a" + } + ``` + +## Verify a file + +To verify a file only the key name used and the previously signed file are needed. + +```text +➜ simd off-chain verify-file alice signedFile.json +Verification OK! +``` diff --git a/client/v2/internal/offchain/msgSignArbitraryData.proto b/client/v2/internal/offchain/msgSignArbitraryData.proto new file mode 100644 index 000000000000..4baa453f2355 --- /dev/null +++ b/client/v2/internal/offchain/msgSignArbitraryData.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package offchain; + +import "cosmos_proto/cosmos.proto"; +import "cosmos/msg/v1/msg.proto"; +import "amino/amino.proto"; + +// MsgSignArbitraryData defines an arbitrary, general-purpose, off-chain message +message MsgSignArbitraryData { + option (amino.name) = "offchain/MsgSignArbitraryData"; + option (cosmos.msg.v1.signer) = "signer"; + // AppDomain is the application requesting off-chain message signing + string app_domain = 1; + // Signer is the sdk.AccAddress of the message signer + string signer = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + // Data represents the raw bytes of the content that is signed (text, json, etc) + string data = 3; +} + diff --git a/client/v2/internal/offchain/msgSignArbitraryData.pulsar.go b/client/v2/internal/offchain/msgSignArbitraryData.pulsar.go new file mode 100644 index 000000000000..d05e1b45f4c0 --- /dev/null +++ b/client/v2/internal/offchain/msgSignArbitraryData.pulsar.go @@ -0,0 +1,730 @@ +// Code generated by protoc-gen-go-pulsar. DO NOT EDIT. +package offchain + +import ( + _ "cosmossdk.io/api/amino" + _ "cosmossdk.io/api/cosmos/msg/v1" + fmt "fmt" + _ "github.com/cosmos/cosmos-proto" + runtime "github.com/cosmos/cosmos-proto/runtime" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoiface "google.golang.org/protobuf/runtime/protoiface" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + io "io" + reflect "reflect" + sync "sync" +) + +var ( + md_MsgSignArbitraryData protoreflect.MessageDescriptor + fd_MsgSignArbitraryData_app_domain protoreflect.FieldDescriptor + fd_MsgSignArbitraryData_signer protoreflect.FieldDescriptor + fd_MsgSignArbitraryData_data protoreflect.FieldDescriptor +) + +func init() { + file_offchain_msgSignArbitraryData_proto_init() + md_MsgSignArbitraryData = File_offchain_msgSignArbitraryData_proto.Messages().ByName("MsgSignArbitraryData") + fd_MsgSignArbitraryData_app_domain = md_MsgSignArbitraryData.Fields().ByName("app_domain") + fd_MsgSignArbitraryData_signer = md_MsgSignArbitraryData.Fields().ByName("signer") + fd_MsgSignArbitraryData_data = md_MsgSignArbitraryData.Fields().ByName("data") +} + +var _ protoreflect.Message = (*fastReflection_MsgSignArbitraryData)(nil) + +type fastReflection_MsgSignArbitraryData MsgSignArbitraryData + +func (x *MsgSignArbitraryData) ProtoReflect() protoreflect.Message { + return (*fastReflection_MsgSignArbitraryData)(x) +} + +func (x *MsgSignArbitraryData) slowProtoReflect() protoreflect.Message { + mi := &file_offchain_msgSignArbitraryData_proto_msgTypes[0] + 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_MsgSignArbitraryData_messageType fastReflection_MsgSignArbitraryData_messageType +var _ protoreflect.MessageType = fastReflection_MsgSignArbitraryData_messageType{} + +type fastReflection_MsgSignArbitraryData_messageType struct{} + +func (x fastReflection_MsgSignArbitraryData_messageType) Zero() protoreflect.Message { + return (*fastReflection_MsgSignArbitraryData)(nil) +} +func (x fastReflection_MsgSignArbitraryData_messageType) New() protoreflect.Message { + return new(fastReflection_MsgSignArbitraryData) +} +func (x fastReflection_MsgSignArbitraryData_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MsgSignArbitraryData +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_MsgSignArbitraryData) Descriptor() protoreflect.MessageDescriptor { + return md_MsgSignArbitraryData +} + +// 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_MsgSignArbitraryData) Type() protoreflect.MessageType { + return _fastReflection_MsgSignArbitraryData_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_MsgSignArbitraryData) New() protoreflect.Message { + return new(fastReflection_MsgSignArbitraryData) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_MsgSignArbitraryData) Interface() protoreflect.ProtoMessage { + return (*MsgSignArbitraryData)(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_MsgSignArbitraryData) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if x.AppDomain != "" { + value := protoreflect.ValueOfString(x.AppDomain) + if !f(fd_MsgSignArbitraryData_app_domain, value) { + return + } + } + if x.Signer != "" { + value := protoreflect.ValueOfString(x.Signer) + if !f(fd_MsgSignArbitraryData_signer, value) { + return + } + } + if x.Data != "" { + value := protoreflect.ValueOfString(x.Data) + if !f(fd_MsgSignArbitraryData_data, 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_MsgSignArbitraryData) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "offchain.MsgSignArbitraryData.app_domain": + return x.AppDomain != "" + case "offchain.MsgSignArbitraryData.signer": + return x.Signer != "" + case "offchain.MsgSignArbitraryData.data": + return x.Data != "" + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: offchain.MsgSignArbitraryData")) + } + panic(fmt.Errorf("message offchain.MsgSignArbitraryData 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_MsgSignArbitraryData) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "offchain.MsgSignArbitraryData.app_domain": + x.AppDomain = "" + case "offchain.MsgSignArbitraryData.signer": + x.Signer = "" + case "offchain.MsgSignArbitraryData.data": + x.Data = "" + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: offchain.MsgSignArbitraryData")) + } + panic(fmt.Errorf("message offchain.MsgSignArbitraryData 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_MsgSignArbitraryData) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "offchain.MsgSignArbitraryData.app_domain": + value := x.AppDomain + return protoreflect.ValueOfString(value) + case "offchain.MsgSignArbitraryData.signer": + value := x.Signer + return protoreflect.ValueOfString(value) + case "offchain.MsgSignArbitraryData.data": + value := x.Data + return protoreflect.ValueOfString(value) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: offchain.MsgSignArbitraryData")) + } + panic(fmt.Errorf("message offchain.MsgSignArbitraryData 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_MsgSignArbitraryData) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "offchain.MsgSignArbitraryData.app_domain": + x.AppDomain = value.Interface().(string) + case "offchain.MsgSignArbitraryData.signer": + x.Signer = value.Interface().(string) + case "offchain.MsgSignArbitraryData.data": + x.Data = value.Interface().(string) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: offchain.MsgSignArbitraryData")) + } + panic(fmt.Errorf("message offchain.MsgSignArbitraryData 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_MsgSignArbitraryData) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "offchain.MsgSignArbitraryData.app_domain": + panic(fmt.Errorf("field app_domain of message offchain.MsgSignArbitraryData is not mutable")) + case "offchain.MsgSignArbitraryData.signer": + panic(fmt.Errorf("field signer of message offchain.MsgSignArbitraryData is not mutable")) + case "offchain.MsgSignArbitraryData.data": + panic(fmt.Errorf("field data of message offchain.MsgSignArbitraryData is not mutable")) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: offchain.MsgSignArbitraryData")) + } + panic(fmt.Errorf("message offchain.MsgSignArbitraryData 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_MsgSignArbitraryData) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "offchain.MsgSignArbitraryData.app_domain": + return protoreflect.ValueOfString("") + case "offchain.MsgSignArbitraryData.signer": + return protoreflect.ValueOfString("") + case "offchain.MsgSignArbitraryData.data": + return protoreflect.ValueOfString("") + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: offchain.MsgSignArbitraryData")) + } + panic(fmt.Errorf("message offchain.MsgSignArbitraryData 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_MsgSignArbitraryData) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in offchain.MsgSignArbitraryData", 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_MsgSignArbitraryData) 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_MsgSignArbitraryData) 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_MsgSignArbitraryData) 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_MsgSignArbitraryData) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*MsgSignArbitraryData) + 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.AppDomain) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + l = len(x.Signer) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + l = len(x.Data) + 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().(*MsgSignArbitraryData) + 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.Data) > 0 { + i -= len(x.Data) + copy(dAtA[i:], x.Data) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.Data))) + i-- + dAtA[i] = 0x1a + } + 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] = 0x12 + } + if len(x.AppDomain) > 0 { + i -= len(x.AppDomain) + copy(dAtA[i:], x.AppDomain) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.AppDomain))) + 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().(*MsgSignArbitraryData) + 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: MsgSignArbitraryData: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MsgSignArbitraryData: 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 AppDomain", 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.AppDomain = 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 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 3: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Data", 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.Data = 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, + } +} + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.0 +// protoc (unknown) +// source: offchain/msgSignArbitraryData.proto + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// MsgSignArbitraryData defines an arbitrary, general-purpose, off-chain message +type MsgSignArbitraryData struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // AppDomain is the application requesting off-chain message signing + AppDomain string `protobuf:"bytes,1,opt,name=app_domain,json=appDomain,proto3" json:"app_domain,omitempty"` + // Signer is the sdk.AccAddress of the message signer + Signer string `protobuf:"bytes,2,opt,name=signer,proto3" json:"signer,omitempty"` + // Data represents the raw bytes of the content that is signed (text, json, etc) + Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *MsgSignArbitraryData) Reset() { + *x = MsgSignArbitraryData{} + if protoimpl.UnsafeEnabled { + mi := &file_offchain_msgSignArbitraryData_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MsgSignArbitraryData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MsgSignArbitraryData) ProtoMessage() {} + +// Deprecated: Use MsgSignArbitraryData.ProtoReflect.Descriptor instead. +func (*MsgSignArbitraryData) Descriptor() ([]byte, []int) { + return file_offchain_msgSignArbitraryData_proto_rawDescGZIP(), []int{0} +} + +func (x *MsgSignArbitraryData) GetAppDomain() string { + if x != nil { + return x.AppDomain + } + return "" +} + +func (x *MsgSignArbitraryData) GetSigner() string { + if x != nil { + return x.Signer + } + return "" +} + +func (x *MsgSignArbitraryData) GetData() string { + if x != nil { + return x.Data + } + return "" +} + +var File_offchain_msgSignArbitraryData_proto protoreflect.FileDescriptor + +var file_offchain_msgSignArbitraryData_proto_rawDesc = []byte{ + 0x0a, 0x23, 0x6f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2f, 0x6d, 0x73, 0x67, 0x53, 0x69, + 0x67, 0x6e, 0x41, 0x72, 0x62, 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, 0x44, 0x61, 0x74, 0x61, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x6f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x1a, + 0x19, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, + 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x63, 0x6f, 0x73, 0x6d, + 0x6f, 0x73, 0x2f, 0x6d, 0x73, 0x67, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x73, 0x67, 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, 0xaa, 0x01, 0x0a, 0x14, 0x4d, 0x73, 0x67, 0x53, 0x69, + 0x67, 0x6e, 0x41, 0x72, 0x62, 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, 0x44, 0x61, 0x74, 0x61, 0x12, + 0x1d, 0x0a, 0x0a, 0x61, 0x70, 0x70, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x70, 0x70, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x30, + 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, + 0xd2, 0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, + 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x64, 0x61, 0x74, 0x61, 0x3a, 0x2d, 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x65, + 0x72, 0x8a, 0xe7, 0xb0, 0x2a, 0x1d, 0x6f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2f, 0x4d, + 0x73, 0x67, 0x53, 0x69, 0x67, 0x6e, 0x41, 0x72, 0x62, 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, 0x44, + 0x61, 0x74, 0x61, 0x42, 0xa3, 0x01, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x66, 0x66, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x42, 0x19, 0x4d, 0x73, 0x67, 0x53, 0x69, 0x67, 0x6e, 0x41, 0x72, 0x62, + 0x69, 0x74, 0x72, 0x61, 0x72, 0x79, 0x44, 0x61, 0x74, 0x61, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, + 0x01, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, + 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2d, 0x73, 0x64, 0x6b, 0x2f, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x76, 0x32, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2f, 0x6f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0xa2, 0x02, 0x03, 0x4f, 0x58, + 0x58, 0xaa, 0x02, 0x08, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0xca, 0x02, 0x08, 0x4f, + 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0xe2, 0x02, 0x14, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, + 0x69, 0x6e, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, + 0x08, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_offchain_msgSignArbitraryData_proto_rawDescOnce sync.Once + file_offchain_msgSignArbitraryData_proto_rawDescData = file_offchain_msgSignArbitraryData_proto_rawDesc +) + +func file_offchain_msgSignArbitraryData_proto_rawDescGZIP() []byte { + file_offchain_msgSignArbitraryData_proto_rawDescOnce.Do(func() { + file_offchain_msgSignArbitraryData_proto_rawDescData = protoimpl.X.CompressGZIP(file_offchain_msgSignArbitraryData_proto_rawDescData) + }) + return file_offchain_msgSignArbitraryData_proto_rawDescData +} + +var file_offchain_msgSignArbitraryData_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_offchain_msgSignArbitraryData_proto_goTypes = []interface{}{ + (*MsgSignArbitraryData)(nil), // 0: offchain.MsgSignArbitraryData +} +var file_offchain_msgSignArbitraryData_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_offchain_msgSignArbitraryData_proto_init() } +func file_offchain_msgSignArbitraryData_proto_init() { + if File_offchain_msgSignArbitraryData_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_offchain_msgSignArbitraryData_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MsgSignArbitraryData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_offchain_msgSignArbitraryData_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_offchain_msgSignArbitraryData_proto_goTypes, + DependencyIndexes: file_offchain_msgSignArbitraryData_proto_depIdxs, + MessageInfos: file_offchain_msgSignArbitraryData_proto_msgTypes, + }.Build() + File_offchain_msgSignArbitraryData_proto = out.File + file_offchain_msgSignArbitraryData_proto_rawDesc = nil + file_offchain_msgSignArbitraryData_proto_goTypes = nil + file_offchain_msgSignArbitraryData_proto_depIdxs = nil +} diff --git a/client/v2/offchain/builder.go b/client/v2/offchain/builder.go new file mode 100644 index 000000000000..c3a8f924ed01 --- /dev/null +++ b/client/v2/offchain/builder.go @@ -0,0 +1,317 @@ +package offchain + +// TODO: remove custom off-chain builder once v2 tx builder is developed. + +import ( + "errors" + "fmt" + + "github.com/cosmos/cosmos-proto/anyutil" + "github.com/cosmos/gogoproto/proto" + protov2 "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" + apitx "cosmossdk.io/api/cosmos/tx/v1beta1" + txsigning "cosmossdk.io/x/tx/signing" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" +) + +type builder struct { + cdc codec.Codec + tx *apitx.Tx +} + +func newBuilder(cdc codec.Codec) *builder { + return &builder{ + cdc: cdc, + tx: &apitx.Tx{ + Body: &apitx.TxBody{}, + AuthInfo: &apitx.AuthInfo{ + Fee: &apitx.Fee{ + Amount: nil, + GasLimit: 0, + Payer: "", + Granter: "", + }, + }, + Signatures: nil, + }, + } +} + +// GetTx returns the tx. +func (b *builder) GetTx() *apitx.Tx { + return b.tx +} + +// GetSigningTxData returns the necessary data to generate sign bytes. +func (b *builder) GetSigningTxData() (txsigning.TxData, error) { + body := b.tx.Body + authInfo := b.tx.AuthInfo + + msgs := make([]*anypb.Any, len(body.Messages)) + for i, msg := range body.Messages { + msgs[i] = &anypb.Any{ + TypeUrl: msg.TypeUrl, + Value: msg.Value, + } + } + + extOptions := make([]*anypb.Any, len(body.ExtensionOptions)) + for i, extOption := range body.ExtensionOptions { + extOptions[i] = &anypb.Any{ + TypeUrl: extOption.TypeUrl, + Value: extOption.Value, + } + } + + nonCriticalExtOptions := make([]*anypb.Any, len(body.NonCriticalExtensionOptions)) + for i, extOption := range body.NonCriticalExtensionOptions { + nonCriticalExtOptions[i] = &anypb.Any{ + TypeUrl: extOption.TypeUrl, + Value: extOption.Value, + } + } + + feeCoins := authInfo.Fee.Amount + feeAmount := make([]*basev1beta1.Coin, len(feeCoins)) + for i, coin := range feeCoins { + feeAmount[i] = &basev1beta1.Coin{ + Denom: coin.Denom, + Amount: coin.Amount, + } + } + + txSignerInfos := make([]*apitx.SignerInfo, len(authInfo.SignerInfos)) + for i, signerInfo := range authInfo.SignerInfos { + txSignerInfo := &apitx.SignerInfo{ + PublicKey: &anypb.Any{ + TypeUrl: signerInfo.PublicKey.TypeUrl, + Value: signerInfo.PublicKey.Value, + }, + Sequence: signerInfo.Sequence, + ModeInfo: signerInfo.ModeInfo, + } + txSignerInfos[i] = txSignerInfo + } + + txAuthInfo := &apitx.AuthInfo{ + SignerInfos: txSignerInfos, + Fee: &apitx.Fee{ + Amount: feeAmount, + GasLimit: authInfo.Fee.GasLimit, + Payer: authInfo.Fee.Payer, + Granter: authInfo.Fee.Granter, + }, + } + + txBody := &apitx.TxBody{ + Messages: msgs, + Memo: body.Memo, + TimeoutHeight: body.TimeoutHeight, + ExtensionOptions: extOptions, + NonCriticalExtensionOptions: nonCriticalExtOptions, + } + authInfoBz, err := protov2.Marshal(b.tx.AuthInfo) + if err != nil { + return txsigning.TxData{}, err + } + bodyBz, err := protov2.Marshal(b.tx.Body) + if err != nil { + return txsigning.TxData{}, err + } + txData := txsigning.TxData{ + AuthInfo: txAuthInfo, + AuthInfoBytes: authInfoBz, + Body: txBody, + BodyBytes: bodyBz, + } + return txData, nil +} + +// GetPubKeys returns the pubKeys of the tx. +func (b *builder) GetPubKeys() ([]cryptotypes.PubKey, error) { // If signer already has pubkey in context, this list will have nil in its place + signerInfos := b.tx.AuthInfo.SignerInfos + pks := make([]cryptotypes.PubKey, len(signerInfos)) + + for i, si := range signerInfos { + // NOTE: it is okay to leave this nil if there is no PubKey in the SignerInfo. + // PubKey's can be left unset in SignerInfo. + if si.PublicKey == nil { + continue + } + var pk cryptotypes.PubKey + anyPk := &codectypes.Any{ + TypeUrl: si.PublicKey.TypeUrl, + Value: si.PublicKey.Value, + } + err := b.cdc.UnpackAny(anyPk, &pk) + if err != nil { + return nil, err + } + pks[i] = pk + } + + return pks, nil +} + +// GetSignatures returns the signatures of the tx. +func (b *builder) GetSignatures() ([]OffchainSignature, error) { + signerInfos := b.tx.AuthInfo.SignerInfos + sigs := b.tx.Signatures + pubKeys, err := b.GetPubKeys() + if err != nil { + return nil, err + } + n := len(signerInfos) + res := make([]OffchainSignature, n) + + for i, si := range signerInfos { + // handle nil signatures (in case of simulation) + if si.ModeInfo == nil { + res[i] = OffchainSignature{ + PubKey: pubKeys[i], + } + } else { + var err error + sigData, err := modeInfoAndSigToSignatureData(si.ModeInfo, sigs[i]) + if err != nil { + return nil, err + } + // sequence number is functionally a transaction nonce and referred to as such in the SDK + nonce := si.GetSequence() + res[i] = OffchainSignature{ + PubKey: pubKeys[i], + Data: sigData, + Sequence: nonce, + } + } + } + + return res, nil +} + +// GetSigners returns the signers of the tx. +func (b *builder) GetSigners() ([][]byte, error) { + signers, _, err := b.getSigners() + return signers, err +} + +func (b *builder) getSigners() ([][]byte, []protov2.Message, error) { + var signers [][]byte + seen := map[string]bool{} + + var msgsv2 []protov2.Message + for _, msg := range b.tx.Body.Messages { + msgv2, err := anyutil.Unpack(msg, b.cdc.InterfaceRegistry(), nil) + if err != nil { + return nil, nil, err + } + xs, err := b.cdc.InterfaceRegistry().SigningContext().GetSigners(msgv2) + if err != nil { + return nil, nil, err + } + + msgsv2 = append(msgsv2, msg) + + for _, signer := range xs { + if !seen[string(signer)] { + signers = append(signers, signer) + seen[string(signer)] = true + } + } + } + + return signers, msgsv2, nil +} + +func (b *builder) setMsgs(msgs ...proto.Message) error { + anys := make([]*anypb.Any, len(msgs)) + for i, msg := range msgs { + protoMsg, ok := msg.(protov2.Message) + if !ok { + return errors.New("message is not a proto.Message") + } + protov2MarshalOpts := protov2.MarshalOptions{Deterministic: true} + bz, err := protov2MarshalOpts.Marshal(protoMsg) + if err != nil { + return err + } + anys[i] = &anypb.Any{ + TypeUrl: codectypes.MsgTypeURL(msg), + Value: bz, + } + } + b.tx.Body.Messages = anys + return nil +} + +// SetSignatures set the signatures of the tx. +func (b *builder) SetSignatures(signatures ...OffchainSignature) error { + n := len(signatures) + signerInfos := make([]*apitx.SignerInfo, n) + rawSigs := make([][]byte, n) + var err error + for i, sig := range signatures { + var mi *apitx.ModeInfo + mi, rawSigs[i], err = b.signatureDataToModeInfoAndSig(sig.Data) + if err != nil { + return err + } + + pubKey, err := codectypes.NewAnyWithValue(sig.PubKey) + if err != nil { + return err + } + + signerInfos[i] = &apitx.SignerInfo{ + PublicKey: &anypb.Any{ + TypeUrl: pubKey.TypeUrl, + Value: pubKey.Value, + }, + ModeInfo: mi, + Sequence: sig.Sequence, + } + } + + b.tx.AuthInfo.SignerInfos = signerInfos + b.tx.Signatures = rawSigs + + return nil +} + +// signatureDataToModeInfoAndSig converts a SignatureData to a ModeInfo and raw bytes signature. +func (b *builder) signatureDataToModeInfoAndSig(data SignatureData) (*apitx.ModeInfo, []byte, error) { + if data == nil { + return nil, nil, errors.New("empty SignatureData") + } + + switch data := data.(type) { + case *SingleSignatureData: + return &apitx.ModeInfo{ + Sum: &apitx.ModeInfo_Single_{ + Single: &apitx.ModeInfo_Single{Mode: data.SignMode}, + }, + }, data.Signature, nil + default: + return nil, nil, fmt.Errorf("unexpected signature data type %T", data) + } +} + +// modeInfoAndSigToSignatureData converts a ModeInfo and raw bytes signature to a SignatureData. +func modeInfoAndSigToSignatureData(modeInfo *apitx.ModeInfo, sig []byte) (SignatureData, error) { + switch modeInfoType := modeInfo.Sum.(type) { + case *apitx.ModeInfo_Single_: + return &SingleSignatureData{ + SignMode: modeInfoType.Single.Mode, + Signature: sig, + }, nil + + default: + return nil, fmt.Errorf("unexpected ModeInfo data type %T", modeInfo) + } +} diff --git a/client/v2/offchain/cli.go b/client/v2/offchain/cli.go new file mode 100644 index 000000000000..1ff6011429dc --- /dev/null +++ b/client/v2/offchain/cli.go @@ -0,0 +1,116 @@ +package offchain + +import ( + "os" + "path/filepath" + + "github.com/spf13/cobra" + + v2flags "cosmossdk.io/client/v2/internal/flags" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" +) + +const ( + flagNotEmitUnpopulated = "notEmitUnpopulated" + flagIndent = "indent" + flagEncoding = "encoding" + flagFileFormat = "file-format" +) + +// OffChain off-chain utilities. +func OffChain() *cobra.Command { + cmd := &cobra.Command{ + Use: "off-chain", + Short: "Off-chain utilities.", + Long: `Utilities for off-chain data.`, + } + + cmd.AddCommand( + SignFile(), + VerifyFile(), + ) + + flags.AddKeyringFlags(cmd.PersistentFlags()) + return cmd +} + +// SignFile signs a file with a key. +func SignFile() *cobra.Command { + cmd := &cobra.Command{ + Use: "sign-file ", + Short: "Sign a file.", + Long: "Sign a file using a given key.", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx := client.GetClientContextFromCmd(cmd) + + bz, err := os.ReadFile(args[1]) + if err != nil { + return err + } + + notEmitUnpopulated, _ := cmd.Flags().GetBool(flagNotEmitUnpopulated) + indent, _ := cmd.Flags().GetString(flagIndent) + encoding, _ := cmd.Flags().GetString(flagEncoding) + outputFormat, _ := cmd.Flags().GetString(v2flags.FlagOutput) + outputFile, _ := cmd.Flags().GetString(flags.FlagOutputDocument) + + signedTx, err := Sign(clientCtx, bz, args[0], indent, encoding, outputFormat, !notEmitUnpopulated) + if err != nil { + return err + } + + if outputFile != "" { + fp, err := os.OpenFile(filepath.Clean(outputFile), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + return err + } + cmd.SetOut(fp) + } + + cmd.Println(signedTx) + return nil + }, + } + + cmd.Flags().String(flagIndent, " ", "Choose an indent for the tx") + cmd.Flags().String(v2flags.FlagOutput, "json", "Choose an output format for the tx (json|text") + cmd.Flags().Bool(flagNotEmitUnpopulated, false, "Don't show unpopulated fields in the tx") + cmd.Flags().String(flagEncoding, "no-encoding", "Choose an encoding method for the file content to be added as msg data (no-encoding|base64|hex)") + cmd.Flags().String(flags.FlagOutputDocument, "", "The document will be written to the given file instead of STDOUT") + return cmd +} + +// VerifyFile verifies given file with given key. +func VerifyFile() *cobra.Command { + cmd := &cobra.Command{ + Use: "verify-file ", + Short: "Verify a file.", + Long: "Verify a previously signed file with the given key.", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + bz, err := os.ReadFile(args[1]) + if err != nil { + return err + } + + fileFormat, _ := cmd.Flags().GetString(flagFileFormat) + + err = Verify(clientCtx, bz, fileFormat) + if err == nil { + cmd.Println("Verification OK!") + } + return err + }, + } + + cmd.Flags().String(flagFileFormat, "json", "Choose whats the file format to be verified (json|text)") + return cmd +} diff --git a/client/v2/offchain/common_test.go b/client/v2/offchain/common_test.go new file mode 100644 index 000000000000..5b862fcb20bb --- /dev/null +++ b/client/v2/offchain/common_test.go @@ -0,0 +1,146 @@ +package offchain + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + + bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" + "cosmossdk.io/x/tx/signing" + "cosmossdk.io/x/tx/signing/aminojson" + "cosmossdk.io/x/tx/signing/direct" + "cosmossdk.io/x/tx/signing/directaux" + "cosmossdk.io/x/tx/signing/textual" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/address" + "github.com/cosmos/cosmos-sdk/codec/testutil" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" +) + +const ( + addressCodecPrefix = "cosmos" + validatorAddressCodecPrefix = "cosmosvaloper" + mnemonic = "have embark stumble card pistol fun gauge obtain forget oil awesome lottery unfold corn sure original exist siren pudding spread uphold dwarf goddess card" +) + +func getCodec() codec.Codec { + registry := testutil.CodecOptions{}.NewInterfaceRegistry() + cryptocodec.RegisterInterfaces(registry) + + return codec.NewProtoCodec(registry) +} + +func newGRPCCoinMetadataQueryFn(grpcConn grpc.ClientConnInterface) textual.CoinMetadataQueryFn { + return func(ctx context.Context, denom string) (*bankv1beta1.Metadata, error) { + bankQueryClient := bankv1beta1.NewQueryClient(grpcConn) + res, err := bankQueryClient.DenomMetadata(ctx, &bankv1beta1.QueryDenomMetadataRequest{ + Denom: denom, + }) + if err != nil { + return nil, err + } + + return res.Metadata, nil + } +} + +// testConfig fulfills client.TxConfig although SignModeHandler is the only method implemented. +type testConfig struct { + handler *signing.HandlerMap +} + +func (t testConfig) SignModeHandler() *signing.HandlerMap { + return t.handler +} + +func (t testConfig) TxEncoder() sdk.TxEncoder { + return nil +} + +func (t testConfig) TxDecoder() sdk.TxDecoder { + return nil +} + +func (t testConfig) TxJSONEncoder() sdk.TxEncoder { + return nil +} + +func (t testConfig) TxJSONDecoder() sdk.TxDecoder { + return nil +} + +func (t testConfig) MarshalSignatureJSON(v2s []signingtypes.SignatureV2) ([]byte, error) { + return nil, nil +} + +func (t testConfig) UnmarshalSignatureJSON(bytes []byte) ([]signingtypes.SignatureV2, error) { + return nil, nil +} + +func (t testConfig) NewTxBuilder() client.TxBuilder { + return nil +} + +func (t testConfig) WrapTxBuilder(s sdk.Tx) (client.TxBuilder, error) { + return nil, nil +} + +func (t testConfig) SigningContext() *signing.Context { + return nil +} + +func newTestConfig(t *testing.T) *testConfig { + t.Helper() + + enabledSignModes := []signingtypes.SignMode{ + signingtypes.SignMode_SIGN_MODE_DIRECT, + signingtypes.SignMode_SIGN_MODE_DIRECT_AUX, + signingtypes.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, + signingtypes.SignMode_SIGN_MODE_TEXTUAL, + } + + var err error + signingOptions := signing.Options{ + AddressCodec: address.NewBech32Codec(addressCodecPrefix), + ValidatorAddressCodec: address.NewBech32Codec(validatorAddressCodecPrefix), + } + signingContext, err := signing.NewContext(signingOptions) + require.NoError(t, err) + + lenSignModes := len(enabledSignModes) + handlers := make([]signing.SignModeHandler, lenSignModes) + for i, m := range enabledSignModes { + var err error + switch m { + case signingtypes.SignMode_SIGN_MODE_DIRECT: + handlers[i] = &direct.SignModeHandler{} + case signingtypes.SignMode_SIGN_MODE_DIRECT_AUX: + handlers[i], err = directaux.NewSignModeHandler(directaux.SignModeHandlerOptions{ + TypeResolver: signingOptions.TypeResolver, + SignersContext: signingContext, + }) + require.NoError(t, err) + case signingtypes.SignMode_SIGN_MODE_LEGACY_AMINO_JSON: + handlers[i] = aminojson.NewSignModeHandler(aminojson.SignModeHandlerOptions{ + FileResolver: signingOptions.FileResolver, + TypeResolver: signingOptions.TypeResolver, + }) + case signingtypes.SignMode_SIGN_MODE_TEXTUAL: + handlers[i], err = textual.NewSignModeHandler(textual.SignModeOptions{ + CoinMetadataQuerier: newGRPCCoinMetadataQueryFn(client.Context{}), + FileResolver: signingOptions.FileResolver, + TypeResolver: signingOptions.TypeResolver, + }) + require.NoError(t, err) + } + } + + handler := signing.NewHandlerMap(handlers...) + return &testConfig{handler: handler} +} diff --git a/client/v2/offchain/encode.go b/client/v2/offchain/encode.go new file mode 100644 index 000000000000..6721ffc28cf2 --- /dev/null +++ b/client/v2/offchain/encode.go @@ -0,0 +1,44 @@ +package offchain + +import ( + "encoding/base64" + "encoding/hex" + "fmt" +) + +const ( + noEncoder = "no-encoding" + b64Encoder = "base64" + hexEncoder = "hex" +) + +type encodingFunc = func([]byte) (string, error) + +// noEncoding returns a byte slice as a string. +func noEncoding(digest []byte) (string, error) { + return string(digest), nil +} + +// base64Encoding returns a byte slice as a b64 encoded string. +func base64Encoding(digest []byte) (string, error) { + return base64.StdEncoding.EncodeToString(digest), nil +} + +// hexEncoding returns a byte slice as a hex encoded string. +func hexEncoding(digest []byte) (string, error) { + return hex.EncodeToString(digest), nil +} + +// getEncoder returns a encodingFunc bases on the encoder id provided. +func getEncoder(encoder string) (encodingFunc, error) { + switch encoder { + case noEncoder: + return noEncoding, nil + case b64Encoder: + return base64Encoding, nil + case hexEncoder: + return hexEncoding, nil + default: + return nil, fmt.Errorf("unknown encoder: %s", encoder) + } +} diff --git a/client/v2/offchain/encode_test.go b/client/v2/offchain/encode_test.go new file mode 100644 index 000000000000..b68760abfd11 --- /dev/null +++ b/client/v2/offchain/encode_test.go @@ -0,0 +1,63 @@ +package offchain + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_EncodingFuncs(t *testing.T) { + tests := []struct { + name string + encodeFunc encodingFunc + digest []byte + want string + }{ + { + name: "No encoding", + encodeFunc: noEncoding, + digest: []byte("Hello!"), + want: "Hello!", + }, + { + name: "base64 encoding", + encodeFunc: base64Encoding, + digest: []byte("Hello!"), + want: "SGVsbG8h", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.encodeFunc(tt.digest) + require.NoError(t, err) + require.Equal(t, got, tt.want) + }) + } +} + +func Test_getEncoder(t *testing.T) { + tests := []struct { + name string + encoder string + want encodingFunc + }{ + { + name: "no encoding", + encoder: "no-encoding", + want: noEncoding, + }, + { + name: "base64", + encoder: "base64", + want: base64Encoding, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getEncoder(tt.encoder) + require.NoError(t, err) + require.Equal(t, reflect.ValueOf(got).Pointer(), reflect.ValueOf(tt.want).Pointer()) + }) + } +} diff --git a/client/v2/offchain/marshal.go b/client/v2/offchain/marshal.go new file mode 100644 index 000000000000..8f90b9830d3f --- /dev/null +++ b/client/v2/offchain/marshal.go @@ -0,0 +1,43 @@ +package offchain + +import ( + "fmt" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" + + apitx "cosmossdk.io/api/cosmos/tx/v1beta1" + v2flags "cosmossdk.io/client/v2/internal/flags" +) + +// marshaller marshals Messages. +type marshaller interface { + Marshal(message proto.Message) ([]byte, error) +} + +// getMarshaller returns the marshaller for the given marshaller id. +func getMarshaller(marshallerId, indent string, emitUnpopulated bool) (marshaller, error) { + switch marshallerId { + case v2flags.OutputFormatJSON: + return protojson.MarshalOptions{ + Indent: indent, + EmitUnpopulated: emitUnpopulated, + }, nil + case v2flags.OutputFormatText: + return prototext.MarshalOptions{ + Indent: indent, + EmitUnknown: emitUnpopulated, + }, nil + } + return nil, fmt.Errorf("marshaller with id '%s' not identified", marshallerId) +} + +// marshalOffChainTx marshals a Tx using given marshaller. +func marshalOffChainTx(tx *apitx.Tx, marshaller marshaller) (string, error) { + bytesTx, err := marshaller.Marshal(tx) + if err != nil { + return "", err + } + return string(bytesTx), nil +} diff --git a/client/v2/offchain/sign.go b/client/v2/offchain/sign.go new file mode 100644 index 000000000000..fd01227ed13e --- /dev/null +++ b/client/v2/offchain/sign.go @@ -0,0 +1,167 @@ +package offchain + +import ( + "context" + + "google.golang.org/protobuf/types/known/anypb" + + apisigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + apitx "cosmossdk.io/api/cosmos/tx/v1beta1" + "cosmossdk.io/client/v2/internal/offchain" + txsigning "cosmossdk.io/x/tx/signing" + + "github.com/cosmos/cosmos-sdk/client" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/version" +) + +const ( + // ExpectedChainID defines the chain id an off-chain message must have + ExpectedChainID = "" + // ExpectedAccountNumber defines the account number an off-chain message must have + ExpectedAccountNumber = 0 + // ExpectedSequence defines the sequence number an off-chain message must have + ExpectedSequence = 0 + + signMode = apisigning.SignMode_SIGN_MODE_TEXTUAL +) + +type signerData struct { + Address string + ChainID string + AccountNumber uint64 + Sequence uint64 + PubKey cryptotypes.PubKey +} + +// Sign signs given bytes using the specified encoder and SignMode. +func Sign(ctx client.Context, rawBytes []byte, fromName, indent, encoding, output string, emitUnpopulated bool) (string, error) { + encoder, err := getEncoder(encoding) + if err != nil { + return "", err + } + + digest, err := encoder(rawBytes) + if err != nil { + return "", err + } + + tx, err := sign(ctx, fromName, digest) + if err != nil { + return "", err + } + + txMarshaller, err := getMarshaller(output, indent, emitUnpopulated) + if err != nil { + return "", err + } + + return marshalOffChainTx(tx, txMarshaller) +} + +// sign signs a digest with provided key and SignMode. +func sign(ctx client.Context, fromName, digest string) (*apitx.Tx, error) { + keybase, err := keyring.NewAutoCLIKeyring(ctx.Keyring) + if err != nil { + return nil, err + } + + pubKey, err := keybase.GetPubKey(fromName) + if err != nil { + return nil, err + } + + addr, err := ctx.AddressCodec.BytesToString(pubKey.Address()) + if err != nil { + return nil, err + } + + msg := &offchain.MsgSignArbitraryData{ + AppDomain: version.AppName, + Signer: addr, + Data: digest, + } + + txBuilder := newBuilder(ctx.Codec) + err = txBuilder.setMsgs(msg) + if err != nil { + return nil, err + } + + signerData := signerData{ + Address: addr, + ChainID: ExpectedChainID, + AccountNumber: ExpectedAccountNumber, + Sequence: ExpectedSequence, + PubKey: pubKey, + } + + sigData := &SingleSignatureData{ + SignMode: signMode, + Signature: nil, + } + + sig := OffchainSignature{ + PubKey: pubKey, + Data: sigData, + Sequence: ExpectedSequence, + } + + sigs := []OffchainSignature{sig} + err = txBuilder.SetSignatures(sigs...) + if err != nil { + return nil, err + } + + bytesToSign, err := getSignBytes( + context.Background(), ctx.TxConfig.SignModeHandler(), signerData, txBuilder) + if err != nil { + return nil, err + } + + signedBytes, err := keybase.Sign(fromName, bytesToSign, signMode) + if err != nil { + return nil, err + } + + sigData.Signature = signedBytes + + err = txBuilder.SetSignatures(sig) + if err != nil { + return nil, err + } + + return txBuilder.GetTx(), nil +} + +// getSignBytes gets the bytes to be signed for the given Tx and SignMode. +func getSignBytes(ctx context.Context, + handlerMap *txsigning.HandlerMap, + signerData signerData, + tx *builder, +) ([]byte, error) { + txData, err := tx.GetSigningTxData() + if err != nil { + return nil, err + } + + anyPk, err := codectypes.NewAnyWithValue(signerData.PubKey) + if err != nil { + return nil, err + } + + txSignerData := txsigning.SignerData{ + ChainID: signerData.ChainID, + AccountNumber: signerData.AccountNumber, + Sequence: signerData.Sequence, + Address: signerData.Address, + PubKey: &anypb.Any{ + TypeUrl: anyPk.TypeUrl, + Value: anyPk.Value, + }, + } + + return handlerMap.GetSignBytes(ctx, signMode, txSignerData, txData) +} diff --git a/client/v2/offchain/sign_test.go b/client/v2/offchain/sign_test.go new file mode 100644 index 000000000000..b47a84584d36 --- /dev/null +++ b/client/v2/offchain/sign_test.go @@ -0,0 +1,51 @@ +package offchain + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec/address" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" +) + +func Test_sign(t *testing.T) { + k := keyring.NewInMemory(getCodec()) + + ctx := client.Context{ + Keyring: k, + TxConfig: newTestConfig(t), + AddressCodec: address.NewBech32Codec("cosmos"), + } + + type args struct { + ctx client.Context + fromName string + digest string + } + tests := []struct { + name string + args args + }{ + { + name: "Sign", + args: args{ + ctx: ctx, + fromName: "direct", + digest: "Hello world!", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := k.NewAccount(tt.args.fromName, mnemonic, tt.name, "m/44'/118'/0'/0/0", hd.Secp256k1) + require.NoError(t, err) + + got, err := sign(tt.args.ctx, tt.args.fromName, tt.args.digest) + require.NoError(t, err) + require.NotNil(t, got) + }) + } +} diff --git a/client/v2/offchain/signature.go b/client/v2/offchain/signature.go new file mode 100644 index 000000000000..d7b9769de983 --- /dev/null +++ b/client/v2/offchain/signature.go @@ -0,0 +1,34 @@ +package offchain + +import ( + apitxsigning "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" +) + +type SignatureData interface { + isSignatureData() +} + +func (m *SingleSignatureData) isSignatureData() {} + +type SingleSignatureData struct { + // SignMode represents the SignMode of the signature + SignMode apitxsigning.SignMode + + // Signature is the raw signature. + Signature []byte +} + +type OffchainSignature struct { + // PubKey is the public key to use for verifying the signature + PubKey cryptotypes.PubKey + + // Data is the actual data of the signature which includes SignMode's and + // the signatures themselves for either single or multi-signatures. + Data SignatureData + + // Sequence is the sequence of this account. Only populated in + // SIGN_MODE_DIRECT. + Sequence uint64 +} diff --git a/client/v2/offchain/verify.go b/client/v2/offchain/verify.go new file mode 100644 index 000000000000..303a086022a7 --- /dev/null +++ b/client/v2/offchain/verify.go @@ -0,0 +1,132 @@ +package offchain + +import ( + "bytes" + "context" + "errors" + "fmt" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/types/known/anypb" + + apitx "cosmossdk.io/api/cosmos/tx/v1beta1" + v2flags "cosmossdk.io/client/v2/internal/flags" + txsigning "cosmossdk.io/x/tx/signing" + + "github.com/cosmos/cosmos-sdk/client" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" +) + +// Verify verifies a digest after unmarshalling it. +func Verify(ctx client.Context, digest []byte, fileFormat string) error { + tx, err := unmarshal(digest, fileFormat) + if err != nil { + return err + } + + return verify(ctx, tx) +} + +// verify verifies given Tx. +func verify(ctx client.Context, tx *apitx.Tx) error { + sigTx := builder{ + cdc: ctx.Codec, + tx: tx, + } + + signModeHandler := ctx.TxConfig.SignModeHandler() + + signers, err := sigTx.GetSigners() + if err != nil { + return err + } + + sigs, err := sigTx.GetSignatures() + if err != nil { + return err + } + + if len(sigs) != len(signers) { + return errors.New("mismatch between the number of signatures and signers") + } + + for i, sig := range sigs { + pubKey := sig.PubKey + if !bytes.Equal(pubKey.Address(), signers[i]) { + return errors.New("signature does not match its respective signer") + } + + addr, err := ctx.AddressCodec.BytesToString(pubKey.Address()) + if err != nil { + return err + } + + anyPk, err := codectypes.NewAnyWithValue(pubKey) + if err != nil { + return err + } + + txSignerData := txsigning.SignerData{ + ChainID: ExpectedChainID, + AccountNumber: ExpectedAccountNumber, + Sequence: ExpectedSequence, + Address: addr, + PubKey: &anypb.Any{ + TypeUrl: anyPk.TypeUrl, + Value: anyPk.Value, + }, + } + + txData, err := sigTx.GetSigningTxData() + if err != nil { + return err + } + + err = verifySignature(context.Background(), pubKey, txSignerData, sig.Data, signModeHandler, txData) + if err != nil { + return err + } + } + return nil +} + +// unmarshal unmarshalls a digest to a Tx using protobuf protojson. +func unmarshal(digest []byte, fileFormat string) (*apitx.Tx, error) { + var err error + tx := &apitx.Tx{} + switch fileFormat { + case v2flags.OutputFormatJSON: + err = protojson.Unmarshal(digest, tx) + case v2flags.OutputFormatText: + err = prototext.Unmarshal(digest, tx) + default: + return nil, fmt.Errorf("unsupported file format: %s", fileFormat) + } + return tx, err +} + +// verifySignature verifies a transaction signature contained in SignatureData abstracting over different signing modes. +func verifySignature( + ctx context.Context, + pubKey cryptotypes.PubKey, + signerData txsigning.SignerData, + signatureData SignatureData, + handler *txsigning.HandlerMap, + txData txsigning.TxData, +) error { + switch data := signatureData.(type) { + case *SingleSignatureData: + signBytes, err := handler.GetSignBytes(ctx, data.SignMode, signerData, txData) + if err != nil { + return err + } + if !pubKey.VerifySignature(signBytes, data.Signature) { + return fmt.Errorf("unable to verify single signer signature") + } + return nil + default: + return fmt.Errorf("unexpected SignatureData %T", signatureData) + } +} diff --git a/client/v2/offchain/verify_test.go b/client/v2/offchain/verify_test.go new file mode 100644 index 000000000000..ecdd57e75b5b --- /dev/null +++ b/client/v2/offchain/verify_test.go @@ -0,0 +1,112 @@ +package offchain + +import ( + "testing" + + "github.com/stretchr/testify/require" + + _ "cosmossdk.io/api/cosmos/crypto/secp256k1" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec/address" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" +) + +func Test_Verify(t *testing.T) { + ctx := client.Context{ + TxConfig: newTestConfig(t), + Codec: getCodec(), + AddressCodec: address.NewBech32Codec("cosmos"), + } + + tests := []struct { + name string + digest []byte + fileFormat string + ctx client.Context + wantErr bool + }{ + { + name: "verify json", + digest: []byte("{\"body\":{\"messages\":[{\"@type\":\"/offchain.MsgSignArbitraryData\", \"appDomain\":\"simd\", \"signer\":\"cosmos1x33fy6rusfprkntvjsfregss7rvsvyy4lkwrqu\", \"data\":\"{\\n\\t\\\"name\\\": \\\"John\\\",\\n\\t\\\"surname\\\": \\\"Connor\\\",\\n\\t\\\"age\\\": 15\\n}\\n\"}]}, \"authInfo\":{\"signerInfos\":[{\"publicKey\":{\"@type\":\"/cosmos.crypto.secp256k1.PubKey\", \"key\":\"A/Bfsb7grZtysreo48oB1XAXbcgHnEJyhAqzDMgbLlXw\"}, \"modeInfo\":{\"single\":{\"mode\":\"SIGN_MODE_TEXTUAL\"}}}], \"fee\":{}}, \"signatures\":[\"gRufjcmATaJ3hZSiXII3lcsLDJlHM4OhQs3O/QgAK4weQ73kmj30/gw3HwTKxGb4pnVe0iyLXrKRNeSl1O3zSQ==\"]}"), + fileFormat: "json", + ctx: ctx, + }, + { + name: "wrong signer json", + digest: []byte("{\"body\":{\"messages\":[{\"@type\":\"/offchain.MsgSignArbitraryData\", \"appDomain\":\"simd\", \"signer\":\"cosmos1450l4uau674z55c36df0v7904rnvdk9aq8w96j\", \"data\":\"{\\n\\t\\\"name\\\": \\\"John\\\",\\n\\t\\\"surname\\\": \\\"Connor\\\",\\n\\t\\\"age\\\": 15\\n}\\n\"}]}, \"authInfo\":{\"signerInfos\":[{\"publicKey\":{\"@type\":\"/cosmos.crypto.secp256k1.PubKey\", \"key\":\"A/Bfsb7grZtysreo48oB1XAXbcgHnEJyhAqzDMgbLlXw\"}, \"modeInfo\":{\"single\":{\"mode\":\"SIGN_MODE_TEXTUAL\"}}}], \"fee\":{}}, \"signatures\":[\"gRufjcmATaJ3hZSiXII3lcsLDJlHM4OhQs3O/QgAK4weQ73kmj30/gw3HwTKxGb4pnVe0iyLXrKRNeSl1O3zSQ==\"]}"), + fileFormat: "json", + ctx: ctx, + wantErr: true, + }, + { + name: "verify text", + digest: []byte("body:{messages:{[/offchain.MsgSignArbitraryData]:{app_domain:\"simd\" signer:\"cosmos1x33fy6rusfprkntvjsfregss7rvsvyy4lkwrqu\" data:\"{\\n\\t\\\"name\\\": \\\"John\\\",\\n\\t\\\"surname\\\": \\\"Connor\\\",\\n\\t\\\"age\\\": 15\\n}\\n\"}}} auth_info:{signer_infos:{public_key:{[/cosmos.crypto.secp256k1.PubKey]:{key:\"\\x03\\xf0_\\xb1\\xbe\u0B5Br\\xb2\\xb7\\xa8\\xe3\\xca\\x01\\xd5p\\x17m\\xc8\\x07\\x9cBr\\x84\\n\\xb3\\x0c\\xc8\\x1b.U\\xf0\"}} mode_info:{single:{mode:SIGN_MODE_TEXTUAL}}} fee:{}} signatures:\"\\x81\\x1b\\x9f\\x8dɀM\\xa2w\\x85\\x94\\xa2\\\\\\x827\\x95\\xcb\\x0b\\x0c\\x99G3\\x83\\xa1B\\xcd\\xce\\xfd\\x08\\x00+\\x8c\\x1eC\\xbd\\xe4\\x9a=\\xf4\\xfe\\x0c7\\x1f\\x04\\xca\\xc4f\\xf8\\xa6u^\\xd2,\\x8b^\\xb2\\x915\\xe4\\xa5\\xd4\\xed\\xf3I\"\n"), + fileFormat: "text", + ctx: ctx, + }, + { + name: "wrong signer text", + digest: []byte("\"body:{messages:{[/offchain.MsgSignArbitraryData]:{app_domain:\\\"simd\\\" signer:\\\"cosmos1450l4uau674z55c36df0v7904rnvdk9aq8w96j\\\" data:\\\"{\\\\n\\\\t\\\\\\\"name\\\\\\\": \\\\\\\"John\\\\\\\",\\\\n\\\\t\\\\\\\"surname\\\\\\\": \\\\\\\"Connor\\\\\\\",\\\\n\\\\t\\\\\\\"age\\\\\\\": 15\\\\n}\\\\n\\\"}}} auth_info:{signer_infos:{public_key:{[/cosmos.crypto.secp256k1.PubKey]:{key:\\\"\\\\x03\\\\xf0_\\\\xb1\\\\xbe\\u0B5Br\\\\xb2\\\\xb7\\\\xa8\\\\xe3\\\\xca\\\\x01\\\\xd5p\\\\x17m\\\\xc8\\\\x07\\\\x9cBr\\\\x84\\\\n\\\\xb3\\\\x0c\\\\xc8\\\\x1b.U\\\\xf0\\\"}} mode_info:{single:{mode:SIGN_MODE_TEXTUAL}}} fee:{}} signatures:\\\"\\\\x81\\\\x1b\\\\x9f\\\\x8dɀM\\\\xa2w\\\\x85\\\\x94\\\\xa2\\\\\\\\\\\\x827\\\\x95\\\\xcb\\\\x0b\\\\x0c\\\\x99G3\\\\x83\\\\xa1B\\\\xcd\\\\xce\\\\xfd\\\\x08\\\\x00+\\\\x8c\\\\x1eC\\\\xbd\\\\xe4\\\\x9a=\\\\xf4\\\\xfe\\\\x0c7\\\\x1f\\\\x04\\\\xca\\\\xc4f\\\\xf8\\\\xa6u^\\\\xd2,\\\\x8b^\\\\xb2\\\\x915\\\\xe4\\\\xa5\\\\xd4\\\\xed\\\\xf3I\\\"\\n"), + fileFormat: "text", + ctx: ctx, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Verify(tt.ctx, tt.digest, tt.fileFormat) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_SignVerify(t *testing.T) { + k := keyring.NewInMemory(getCodec()) + _, err := k.NewAccount("signVerify", mnemonic, "", "m/44'/118'/0'/0/0", hd.Secp256k1) + require.NoError(t, err) + + ctx := client.Context{ + TxConfig: newTestConfig(t), + Codec: getCodec(), + AddressCodec: address.NewBech32Codec("cosmos"), + Keyring: k, + } + + tx, err := sign(ctx, "signVerify", "digest") + require.NoError(t, err) + + err = verify(ctx, tx) + require.NoError(t, err) +} + +func Test_unmarshal(t *testing.T) { + tests := []struct { + name string + digest []byte + fileFormat string + }{ + { + name: "json test", + digest: []byte(`{"body":{"messages":[{"@type":"/offchain.MsgSignArbitraryData", "appDomain":"simd", "signer":"cosmos1x33fy6rusfprkntvjsfregss7rvsvyy4lkwrqu", "data":"{\n\t\"name\": \"John\",\n\t\"surname\": \"Connor\",\n\t\"age\": 15\n}\n"}]}, "authInfo":{"signerInfos":[{"publicKey":{"@type":"/cosmos.crypto.secp256k1.PubKey", "key":"A/Bfsb7grZtysreo48oB1XAXbcgHnEJyhAqzDMgbLlXw"}, "modeInfo":{"single":{"mode":"SIGN_MODE_TEXTUAL"}}}], "fee":{}}, "signatures":["gRufjcmATaJ3hZSiXII3lcsLDJlHM4OhQs3O/QgAK4weQ73kmj30/gw3HwTKxGb4pnVe0iyLXrKRNeSl1O3zSQ=="]}`), + fileFormat: "json", + }, + { + name: "text test", + digest: []byte("body:{messages:{[/offchain.MsgSignArbitraryData]:{app_domain:\"simd\" signer:\"cosmos1x33fy6rusfprkntvjsfregss7rvsvyy4lkwrqu\" data:\"{\\n\\t\\\"name\\\": \\\"John\\\",\\n\\t\\\"surname\\\": \\\"Connor\\\",\\n\\t\\\"age\\\": 15\\n}\\n\"}}} auth_info:{signer_infos:{public_key:{[/cosmos.crypto.secp256k1.PubKey]:{key:\"\\x03\\xf0_\\xb1\\xbe\u0B5Br\\xb2\\xb7\\xa8\\xe3\\xca\\x01\\xd5p\\x17m\\xc8\\x07\\x9cBr\\x84\\n\\xb3\\x0c\\xc8\\x1b.U\\xf0\"}} mode_info:{single:{mode:SIGN_MODE_TEXTUAL}}} fee:{}} signatures:\"\\x81\\x1b\\x9f\\x8dɀM\\xa2w\\x85\\x94\\xa2\\\\\\x827\\x95\\xcb\\x0b\\x0c\\x99G3\\x83\\xa1B\\xcd\\xce\\xfd\\x08\\x00+\\x8c\\x1eC\\xbd\\xe4\\x9a=\\xf4\\xfe\\x0c7\\x1f\\x04\\xca\\xc4f\\xf8\\xa6u^\\xd2,\\x8b^\\xb2\\x915\\xe4\\xa5\\xd4\\xed\\xf3I\"\n"), + fileFormat: "text", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := unmarshal(tt.digest, tt.fileFormat) + require.NoError(t, err) + require.NotNil(t, got) + }) + } +} diff --git a/simapp/simd/cmd/commands.go b/simapp/simd/cmd/commands.go index 32d8f6150e49..ae4bc424689a 100644 --- a/simapp/simd/cmd/commands.go +++ b/simapp/simd/cmd/commands.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "cosmossdk.io/client/v2/offchain" "cosmossdk.io/log" "cosmossdk.io/simapp" confixcmd "cosmossdk.io/tools/confix/cmd" @@ -59,6 +60,7 @@ func initRootCmd( queryCommand(), txCommand(), keys.Commands(), + offchain.OffChain(), ) }