From cafe91964691bf214a150c3d8903b92967529c83 Mon Sep 17 00:00:00 2001 From: Bob Smith <5396652+bitcoin-coder-bob@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:43:08 -0500 Subject: [PATCH 1/8] SignMessage rpc --- .../swagger/signer/v1/service.openapi.json | 60 +++++ api-spec/protobuf/gen/signer/v1/service.pb.go | 117 +++++++++- .../protobuf/gen/signer/v1/service.pb.rgw.go | 35 +++ .../protobuf/gen/signer/v1/service_grpc.pb.go | 38 +++ api-spec/protobuf/signer/v1/service.proto | 13 ++ internal/core/ports/signer.go | 1 + internal/infrastructure/signer/client.go | 12 + pkg/arkd-wallet/core/application/types.go | 1 + .../wallet/fixtures/service_fixtures.json | 48 ++++ .../core/application/wallet/service.go | 13 ++ .../core/application/wallet/service_test.go | 219 ++++++++++++++++++ .../interface/grpc/handlers/signer_handler.go | 10 + 12 files changed, 557 insertions(+), 10 deletions(-) create mode 100644 pkg/arkd-wallet/core/application/wallet/fixtures/service_fixtures.json create mode 100644 pkg/arkd-wallet/core/application/wallet/service_test.go diff --git a/api-spec/openapi/swagger/signer/v1/service.openapi.json b/api-spec/openapi/swagger/signer/v1/service.openapi.json index 35adc9078..f34e1160e 100644 --- a/api-spec/openapi/swagger/signer/v1/service.openapi.json +++ b/api-spec/openapi/swagger/signer/v1/service.openapi.json @@ -35,6 +35,46 @@ } } }, + "/v1/sign-message": { + "post": { + "tags": [ + "SignerService" + ], + "operationId": "SignerService_SignMessage", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignMessageRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "a successful response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignMessageResponse" + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Status" + } + } + } + } + } + } + }, "/v1/sign-transaction": { "post": { "tags": [ @@ -184,6 +224,26 @@ } } }, + "SignMessageRequest": { + "title": "SignMessageRequest", + "type": "object", + "properties": { + "message": { + "type": "string", + "format": "byte" + } + } + }, + "SignMessageResponse": { + "title": "SignMessageResponse", + "type": "object", + "properties": { + "signature": { + "type": "string", + "format": "byte" + } + } + }, "SignTransactionRequest": { "title": "SignTransactionRequest", "type": "object", diff --git a/api-spec/protobuf/gen/signer/v1/service.pb.go b/api-spec/protobuf/gen/signer/v1/service.pb.go index 61a86e027..9831d2988 100644 --- a/api-spec/protobuf/gen/signer/v1/service.pb.go +++ b/api-spec/protobuf/gen/signer/v1/service.pb.go @@ -374,6 +374,94 @@ func (x *SignTransactionTapscriptResponse) GetSignedTx() string { return "" } +type SignMessageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message []byte `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignMessageRequest) Reset() { + *x = SignMessageRequest{} + mi := &file_signer_v1_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignMessageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignMessageRequest) ProtoMessage() {} + +func (x *SignMessageRequest) ProtoReflect() protoreflect.Message { + mi := &file_signer_v1_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignMessageRequest.ProtoReflect.Descriptor instead. +func (*SignMessageRequest) Descriptor() ([]byte, []int) { + return file_signer_v1_service_proto_rawDescGZIP(), []int{8} +} + +func (x *SignMessageRequest) GetMessage() []byte { + if x != nil { + return x.Message + } + return nil +} + +type SignMessageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Signature []byte `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignMessageResponse) Reset() { + *x = SignMessageResponse{} + mi := &file_signer_v1_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignMessageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignMessageResponse) ProtoMessage() {} + +func (x *SignMessageResponse) ProtoReflect() protoreflect.Message { + mi := &file_signer_v1_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignMessageResponse.ProtoReflect.Descriptor instead. +func (*SignMessageResponse) Descriptor() ([]byte, []int) { + return file_signer_v1_service_proto_rawDescGZIP(), []int{9} +} + +func (x *SignMessageResponse) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + var File_signer_v1_service_proto protoreflect.FileDescriptor const file_signer_v1_service_proto_rawDesc = "" + @@ -396,14 +484,19 @@ const file_signer_v1_service_proto_rawDesc = "" + "partial_tx\x18\x01 \x01(\tR\tpartialTx\x12#\n" + "\rinput_indexes\x18\x02 \x03(\x05R\finputIndexes\"?\n" + " SignTransactionTapscriptResponse\x12\x1b\n" + - "\tsigned_tx\x18\x01 \x01(\tR\bsignedTx2\xd7\x03\n" + + "\tsigned_tx\x18\x01 \x01(\tR\bsignedTx\".\n" + + "\x12SignMessageRequest\x12\x18\n" + + "\amessage\x18\x01 \x01(\fR\amessage\"3\n" + + "\x13SignMessageResponse\x12\x1c\n" + + "\tsignature\x18\x01 \x01(\fR\tsignature2\xbf\x04\n" + "\rSignerService\x12W\n" + "\tGetStatus\x12\x1b.signer.v1.GetStatusRequest\x1a\x1c.signer.v1.GetStatusResponse\"\x0f\xb2J\f\x12\n" + "/v1/status\x12W\n" + "\tGetPubkey\x12\x1b.signer.v1.GetPubkeyRequest\x1a\x1c.signer.v1.GetPubkeyResponse\"\x0f\xb2J\f\x12\n" + "/v1/pubkey\x12v\n" + "\x0fSignTransaction\x12!.signer.v1.SignTransactionRequest\x1a\".signer.v1.SignTransactionResponse\"\x1c\xb2J\x19B\x01*\"\x14/v1/sign-transaction\x12\x9b\x01\n" + - "\x18SignTransactionTapscript\x12*.signer.v1.SignTransactionTapscriptRequest\x1a+.signer.v1.SignTransactionTapscriptResponse\"&\xb2J#B\x01*\"\x1e/v1/sign-transaction-tapscriptB\x90\x01\n" + + "\x18SignTransactionTapscript\x12*.signer.v1.SignTransactionTapscriptRequest\x1a+.signer.v1.SignTransactionTapscriptResponse\"&\xb2J#B\x01*\"\x1e/v1/sign-transaction-tapscript\x12f\n" + + "\vSignMessage\x12\x1d.signer.v1.SignMessageRequest\x1a\x1e.signer.v1.SignMessageResponse\"\x18\xb2J\x15B\x01*\"\x10/v1/sign-messageB\x90\x01\n" + "\rcom.signer.v1B\fServiceProtoP\x01Z,github.com/arkade-os/arkd/signer/v1;signerv1\xa2\x02\x03SXX\xaa\x02\tSigner.V1\xca\x02\tSigner\\V1\xe2\x02\x15Signer\\V1\\GPBMetadata\xea\x02\n" + "Signer::V1b\x06proto3" @@ -419,7 +512,7 @@ func file_signer_v1_service_proto_rawDescGZIP() []byte { return file_signer_v1_service_proto_rawDescData } -var file_signer_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_signer_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_signer_v1_service_proto_goTypes = []any{ (*GetStatusRequest)(nil), // 0: signer.v1.GetStatusRequest (*GetStatusResponse)(nil), // 1: signer.v1.GetStatusResponse @@ -429,18 +522,22 @@ var file_signer_v1_service_proto_goTypes = []any{ (*SignTransactionResponse)(nil), // 5: signer.v1.SignTransactionResponse (*SignTransactionTapscriptRequest)(nil), // 6: signer.v1.SignTransactionTapscriptRequest (*SignTransactionTapscriptResponse)(nil), // 7: signer.v1.SignTransactionTapscriptResponse + (*SignMessageRequest)(nil), // 8: signer.v1.SignMessageRequest + (*SignMessageResponse)(nil), // 9: signer.v1.SignMessageResponse } var file_signer_v1_service_proto_depIdxs = []int32{ 0, // 0: signer.v1.SignerService.GetStatus:input_type -> signer.v1.GetStatusRequest 2, // 1: signer.v1.SignerService.GetPubkey:input_type -> signer.v1.GetPubkeyRequest 4, // 2: signer.v1.SignerService.SignTransaction:input_type -> signer.v1.SignTransactionRequest 6, // 3: signer.v1.SignerService.SignTransactionTapscript:input_type -> signer.v1.SignTransactionTapscriptRequest - 1, // 4: signer.v1.SignerService.GetStatus:output_type -> signer.v1.GetStatusResponse - 3, // 5: signer.v1.SignerService.GetPubkey:output_type -> signer.v1.GetPubkeyResponse - 5, // 6: signer.v1.SignerService.SignTransaction:output_type -> signer.v1.SignTransactionResponse - 7, // 7: signer.v1.SignerService.SignTransactionTapscript:output_type -> signer.v1.SignTransactionTapscriptResponse - 4, // [4:8] is the sub-list for method output_type - 0, // [0:4] is the sub-list for method input_type + 8, // 4: signer.v1.SignerService.SignMessage:input_type -> signer.v1.SignMessageRequest + 1, // 5: signer.v1.SignerService.GetStatus:output_type -> signer.v1.GetStatusResponse + 3, // 6: signer.v1.SignerService.GetPubkey:output_type -> signer.v1.GetPubkeyResponse + 5, // 7: signer.v1.SignerService.SignTransaction:output_type -> signer.v1.SignTransactionResponse + 7, // 8: signer.v1.SignerService.SignTransactionTapscript:output_type -> signer.v1.SignTransactionTapscriptResponse + 9, // 9: signer.v1.SignerService.SignMessage:output_type -> signer.v1.SignMessageResponse + 5, // [5:10] is the sub-list for method output_type + 0, // [0:5] 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 @@ -457,7 +554,7 @@ func file_signer_v1_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_signer_v1_service_proto_rawDesc), len(file_signer_v1_service_proto_rawDesc)), NumEnums: 0, - NumMessages: 8, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/api-spec/protobuf/gen/signer/v1/service.pb.rgw.go b/api-spec/protobuf/gen/signer/v1/service.pb.rgw.go index 30374e624..e3f8b0012 100644 --- a/api-spec/protobuf/gen/signer/v1/service.pb.rgw.go +++ b/api-spec/protobuf/gen/signer/v1/service.pb.rgw.go @@ -63,6 +63,19 @@ func request_SignerService_SignTransactionTapscript_0(ctx context.Context, marsh } +func request_SignerService_SignMessage_0(ctx context.Context, marshaler gateway.Marshaler, mux *gateway.ServeMux, client SignerServiceClient, req *http.Request, pathParams gateway.Params) (proto.Message, gateway.ServerMetadata, error) { + var protoReq SignMessageRequest + var metadata gateway.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, gateway.ErrMarshal{Err: err, Inbound: true} + } + + msg, err := client.SignMessage(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + // RegisterSignerServiceHandlerFromEndpoint is same as RegisterSignerServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterSignerServiceHandlerFromEndpoint(ctx context.Context, mux *gateway.ServeMux, endpoint string, opts []grpc.DialOption) error { @@ -190,4 +203,26 @@ func RegisterSignerServiceHandlerClient(ctx context.Context, mux *gateway.ServeM mux.ForwardResponseMessage(annotatedContext, outboundMarshaler, w, req, resp) }) + mux.HandleWithParams("POST", "/v1/sign-message", func(w http.ResponseWriter, req *http.Request, pathParams gateway.Params) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := mux.MarshalerForRequest(req) + var err error + var annotatedContext context.Context + annotatedContext, err = gateway.AnnotateContext(ctx, mux, req, "/signer.v1.SignerService/SignMessage", gateway.WithHTTPPathPattern("/v1/sign-message")) + if err != nil { + mux.HTTPError(ctx, outboundMarshaler, w, req, err) + return + } + + resp, md, err := request_SignerService_SignMessage_0(annotatedContext, inboundMarshaler, mux, client, req, pathParams) + annotatedContext = gateway.NewServerMetadataContext(annotatedContext, md) + if err != nil { + mux.HTTPError(annotatedContext, outboundMarshaler, w, req, err) + return + } + + mux.ForwardResponseMessage(annotatedContext, outboundMarshaler, w, req, resp) + }) + } diff --git a/api-spec/protobuf/gen/signer/v1/service_grpc.pb.go b/api-spec/protobuf/gen/signer/v1/service_grpc.pb.go index e937ce06e..dbec1c1f0 100644 --- a/api-spec/protobuf/gen/signer/v1/service_grpc.pb.go +++ b/api-spec/protobuf/gen/signer/v1/service_grpc.pb.go @@ -23,6 +23,7 @@ const ( SignerService_GetPubkey_FullMethodName = "/signer.v1.SignerService/GetPubkey" SignerService_SignTransaction_FullMethodName = "/signer.v1.SignerService/SignTransaction" SignerService_SignTransactionTapscript_FullMethodName = "/signer.v1.SignerService/SignTransactionTapscript" + SignerService_SignMessage_FullMethodName = "/signer.v1.SignerService/SignMessage" ) // SignerServiceClient is the client API for SignerService service. @@ -35,6 +36,7 @@ type SignerServiceClient interface { GetPubkey(ctx context.Context, in *GetPubkeyRequest, opts ...grpc.CallOption) (*GetPubkeyResponse, error) SignTransaction(ctx context.Context, in *SignTransactionRequest, opts ...grpc.CallOption) (*SignTransactionResponse, error) SignTransactionTapscript(ctx context.Context, in *SignTransactionTapscriptRequest, opts ...grpc.CallOption) (*SignTransactionTapscriptResponse, error) + SignMessage(ctx context.Context, in *SignMessageRequest, opts ...grpc.CallOption) (*SignMessageResponse, error) } type signerServiceClient struct { @@ -85,6 +87,16 @@ func (c *signerServiceClient) SignTransactionTapscript(ctx context.Context, in * return out, nil } +func (c *signerServiceClient) SignMessage(ctx context.Context, in *SignMessageRequest, opts ...grpc.CallOption) (*SignMessageResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SignMessageResponse) + err := c.cc.Invoke(ctx, SignerService_SignMessage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // SignerServiceServer is the server API for SignerService service. // All implementations should embed UnimplementedSignerServiceServer // for forward compatibility. @@ -95,6 +107,7 @@ type SignerServiceServer interface { GetPubkey(context.Context, *GetPubkeyRequest) (*GetPubkeyResponse, error) SignTransaction(context.Context, *SignTransactionRequest) (*SignTransactionResponse, error) SignTransactionTapscript(context.Context, *SignTransactionTapscriptRequest) (*SignTransactionTapscriptResponse, error) + SignMessage(context.Context, *SignMessageRequest) (*SignMessageResponse, error) } // UnimplementedSignerServiceServer should be embedded to have @@ -116,6 +129,9 @@ func (UnimplementedSignerServiceServer) SignTransaction(context.Context, *SignTr func (UnimplementedSignerServiceServer) SignTransactionTapscript(context.Context, *SignTransactionTapscriptRequest) (*SignTransactionTapscriptResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SignTransactionTapscript not implemented") } +func (UnimplementedSignerServiceServer) SignMessage(context.Context, *SignMessageRequest) (*SignMessageResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SignMessage not implemented") +} func (UnimplementedSignerServiceServer) testEmbeddedByValue() {} // UnsafeSignerServiceServer may be embedded to opt out of forward compatibility for this service. @@ -208,6 +224,24 @@ func _SignerService_SignTransactionTapscript_Handler(srv interface{}, ctx contex return interceptor(ctx, in, info, handler) } +func _SignerService_SignMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignMessageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SignerServiceServer).SignMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SignerService_SignMessage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SignerServiceServer).SignMessage(ctx, req.(*SignMessageRequest)) + } + return interceptor(ctx, in, info, handler) +} + // SignerService_ServiceDesc is the grpc.ServiceDesc for SignerService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -231,6 +265,10 @@ var SignerService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SignTransactionTapscript", Handler: _SignerService_SignTransactionTapscript_Handler, }, + { + MethodName: "SignMessage", + Handler: _SignerService_SignMessage_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "signer/v1/service.proto", diff --git a/api-spec/protobuf/signer/v1/service.proto b/api-spec/protobuf/signer/v1/service.proto index c694fd5e2..6045200a5 100644 --- a/api-spec/protobuf/signer/v1/service.proto +++ b/api-spec/protobuf/signer/v1/service.proto @@ -28,6 +28,12 @@ service SignerService { body: "*" }; } + rpc SignMessage(SignMessageRequest) returns (SignMessageResponse) { + option (meshapi.gateway.http) = { + post: "/v1/sign-message" + body: "*" + }; + } } message GetStatusRequest {} @@ -54,4 +60,11 @@ message SignTransactionTapscriptRequest { } message SignTransactionTapscriptResponse { string signed_tx = 1; +} + +message SignMessageRequest { + bytes message = 1; +} +message SignMessageResponse { + bytes signature = 1; } \ No newline at end of file diff --git a/internal/core/ports/signer.go b/internal/core/ports/signer.go index e825aa362..6c0d6fd1f 100644 --- a/internal/core/ports/signer.go +++ b/internal/core/ports/signer.go @@ -13,4 +13,5 @@ type SignerService interface { SignTransactionTapscript( ctx context.Context, partialTx string, inputIndexes []int, // inputIndexes == nil means sign all inputs ) (string, error) + SignMessage(ctx context.Context, message []byte) ([]byte, error) } diff --git a/internal/infrastructure/signer/client.go b/internal/infrastructure/signer/client.go index e508ac851..5e9daf3d1 100644 --- a/internal/infrastructure/signer/client.go +++ b/internal/infrastructure/signer/client.go @@ -99,3 +99,15 @@ func (c *signerClient) SignTransactionTapscript( } return resp.GetSignedTx(), nil } + +func (c *signerClient) SignMessage( + ctx context.Context, message []byte, +) ([]byte, error) { + resp, err := c.client.SignMessage(ctx, &signerv1.SignMessageRequest{ + Message: message, + }) + if err != nil { + return nil, err + } + return resp.GetSignature(), nil +} diff --git a/pkg/arkd-wallet/core/application/types.go b/pkg/arkd-wallet/core/application/types.go index 15d881be5..3152a0804 100644 --- a/pkg/arkd-wallet/core/application/types.go +++ b/pkg/arkd-wallet/core/application/types.go @@ -46,6 +46,7 @@ type WalletService interface { // Withdraw both main and connectors account funds WithdrawAll(ctx context.Context, destinationAddress string) (string, error) LoadSignerKey(ctx context.Context, prvkey *btcec.PrivateKey) error + SignMessage(ctx context.Context, message []byte) ([]byte, error) Close() } diff --git a/pkg/arkd-wallet/core/application/wallet/fixtures/service_fixtures.json b/pkg/arkd-wallet/core/application/wallet/fixtures/service_fixtures.json new file mode 100644 index 000000000..217022533 --- /dev/null +++ b/pkg/arkd-wallet/core/application/wallet/fixtures/service_fixtures.json @@ -0,0 +1,48 @@ +{ + "sign_message_tests": { + "test_keys": [ + { + "name": "key_1", + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001", + "public_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + { + "name": "key_2", + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000002", + "public_key_hex": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + ], + "test_cases": [ + { + "name": "simple_message", + "message_hex": "48656c6c6f20576f726c64", + "key_name": "key_1" + }, + { + "name": "empty_message", + "message_hex": "", + "key_name": "key_1" + }, + { + "name": "32_byte_message", + "message_hex": "0000000000000000000000000000000000000000000000000000000000000001", + "key_name": "key_1" + }, + { + "name": "36_byte_auth_token_message", + "message_hex": "000000000000000000000000000000000000000000000000000000000000000100000000", + "key_name": "key_1" + }, + { + "name": "large_message", + "message_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "key_name": "key_1" + }, + { + "name": "message_with_different_key", + "message_hex": "48656c6c6f20576f726c64", + "key_name": "key_2" + } + ] + } +} diff --git a/pkg/arkd-wallet/core/application/wallet/service.go b/pkg/arkd-wallet/core/application/wallet/service.go index ded111fd1..96b29f8e6 100644 --- a/pkg/arkd-wallet/core/application/wallet/service.go +++ b/pkg/arkd-wallet/core/application/wallet/service.go @@ -727,6 +727,19 @@ func (w *wallet) LoadSignerKey(ctx context.Context, prvkey *btcec.PrivateKey) er return nil } +func (w *wallet) SignMessage(ctx context.Context, message []byte) ([]byte, error) { + if w.SignerKey == nil { + return nil, fmt.Errorf("signer key not loaded") + } + + msgHash := chainhash.HashB(message) + sig, err := schnorr.Sign(w.SignerKey, msgHash) + if err != nil { + return nil, fmt.Errorf("failed to sign message: %w", err) + } + return sig.Serialize(), nil +} + func (w *wallet) Close() { // nolint:errcheck w.Nbxplorer.Close() diff --git a/pkg/arkd-wallet/core/application/wallet/service_test.go b/pkg/arkd-wallet/core/application/wallet/service_test.go new file mode 100644 index 000000000..c4f66a9fe --- /dev/null +++ b/pkg/arkd-wallet/core/application/wallet/service_test.go @@ -0,0 +1,219 @@ +package wallet + +import ( + "context" + "encoding/hex" + "encoding/json" + "os" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/stretchr/testify/require" +) + +type serviceFixtures struct { + SignMessageTests signMessageTestFixtures `json:"sign_message_tests"` +} + +type signMessageTestFixtures struct { + TestKeys []testKey `json:"test_keys"` + TestCases []signMessageTestCase `json:"test_cases"` +} + +type testKey struct { + Name string `json:"name"` + PrivateKeyHex string `json:"private_key_hex"` + PublicKeyHex string `json:"public_key_hex"` +} + +type signMessageTestCase struct { + Name string `json:"name"` + MessageHex string `json:"message_hex"` + KeyName string `json:"key_name"` +} + +func loadServiceFixtures(t *testing.T) *serviceFixtures { + data, err := os.ReadFile("fixtures/service_fixtures.json") + require.NoError(t, err) + + var f serviceFixtures + err = json.Unmarshal(data, &f) + require.NoError(t, err) + + return &f +} + +func getTestKey(t *testing.T, fixtures *serviceFixtures, keyName string) *btcec.PrivateKey { + for _, k := range fixtures.SignMessageTests.TestKeys { + if k.Name == keyName { + privKeyBytes, err := hex.DecodeString(k.PrivateKeyHex) + require.NoError(t, err) + privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes) + return privKey + } + } + t.Fatalf("test key not found: %s", keyName) + return nil +} + +func TestSignMessage(t *testing.T) { + fixtures := loadServiceFixtures(t) + ctx := context.Background() + + for _, tc := range fixtures.SignMessageTests.TestCases { + t.Run(tc.Name, func(t *testing.T) { + // Get the private key for this test case + privKey := getTestKey(t, fixtures, tc.KeyName) + + // Create wallet with signer key + w := &wallet{ + WalletOptions: WalletOptions{ + SignerKey: privKey, + }, + } + + // Decode message + message, err := hex.DecodeString(tc.MessageHex) + require.NoError(t, err) + + // Sign the message + signature, err := w.SignMessage(ctx, message) + require.NoError(t, err) + require.NotNil(t, signature) + + // Schnorr signatures are always 64 bytes + require.Len(t, signature, 64, "schnorr signature should be 64 bytes") + + // Verify the signature is valid + msgHash := chainhash.HashB(message) + sig, err := schnorr.ParseSignature(signature) + require.NoError(t, err) + + pubKey := privKey.PubKey() + valid := sig.Verify(msgHash, pubKey) + require.True(t, valid, "signature should be valid") + }) + } +} + +func TestSignMessage_NoSignerKey(t *testing.T) { + ctx := context.Background() + + // Create wallet without signer key + w := &wallet{ + WalletOptions: WalletOptions{ + SignerKey: nil, + }, + } + + message := []byte("test message") + signature, err := w.SignMessage(ctx, message) + + require.Error(t, err) + require.Nil(t, signature) + require.Contains(t, err.Error(), "signer key not loaded") +} + +func TestSignMessage_DifferentKeysProduceDifferentSignatures(t *testing.T) { + fixtures := loadServiceFixtures(t) + ctx := context.Background() + + // Get two different keys + privKey1 := getTestKey(t, fixtures, "key_1") + privKey2 := getTestKey(t, fixtures, "key_2") + + w1 := &wallet{WalletOptions: WalletOptions{SignerKey: privKey1}} + w2 := &wallet{WalletOptions: WalletOptions{SignerKey: privKey2}} + + message := []byte("same message") + + sig1, err := w1.SignMessage(ctx, message) + require.NoError(t, err) + + sig2, err := w2.SignMessage(ctx, message) + require.NoError(t, err) + + // Signatures should be different (different keys) + require.NotEqual(t, sig1, sig2, "different keys should produce different signatures") + + // But both should be valid for their respective keys + msgHash := chainhash.HashB(message) + + parsedSig1, err := schnorr.ParseSignature(sig1) + require.NoError(t, err) + require.True(t, parsedSig1.Verify(msgHash, privKey1.PubKey())) + + parsedSig2, err := schnorr.ParseSignature(sig2) + require.NoError(t, err) + require.True(t, parsedSig2.Verify(msgHash, privKey2.PubKey())) + + // Cross-verification should fail + require.False(t, parsedSig1.Verify(msgHash, privKey2.PubKey()), "sig1 should not verify with key2") + require.False(t, parsedSig2.Verify(msgHash, privKey1.PubKey()), "sig2 should not verify with key1") +} + +func TestSignMessage_DifferentMessagesProduceDifferentSignatures(t *testing.T) { + fixtures := loadServiceFixtures(t) + ctx := context.Background() + + privKey := getTestKey(t, fixtures, "key_1") + w := &wallet{WalletOptions: WalletOptions{SignerKey: privKey}} + + message1 := []byte("message one") + message2 := []byte("message two") + + sig1, err := w.SignMessage(ctx, message1) + require.NoError(t, err) + + sig2, err := w.SignMessage(ctx, message2) + require.NoError(t, err) + + // Signatures should be different (different messages) + require.NotEqual(t, sig1, sig2, "different messages should produce different signatures") + + // Each signature should only verify with its corresponding message + pubKey := privKey.PubKey() + + parsedSig1, err := schnorr.ParseSignature(sig1) + require.NoError(t, err) + require.True(t, parsedSig1.Verify(chainhash.HashB(message1), pubKey)) + require.False(t, parsedSig1.Verify(chainhash.HashB(message2), pubKey)) + + parsedSig2, err := schnorr.ParseSignature(sig2) + require.NoError(t, err) + require.True(t, parsedSig2.Verify(chainhash.HashB(message2), pubKey)) + require.False(t, parsedSig2.Verify(chainhash.HashB(message1), pubKey)) +} + +func TestSignMessage_ConsistentWithSchnorrVerify(t *testing.T) { + fixtures := loadServiceFixtures(t) + ctx := context.Background() + + privKey := getTestKey(t, fixtures, "key_1") + w := &wallet{WalletOptions: WalletOptions{SignerKey: privKey}} + + // Test with the 36-byte auth token message format + // This matches what indexer.go uses for auth tokens + txid := make([]byte, 32) + txid[31] = 0x01 // txid = ...0001 + vout := []byte{0x00, 0x00, 0x00, 0x2a} // vout = 42 (big endian) + message := append(txid, vout...) + + signature, err := w.SignMessage(ctx, message) + require.NoError(t, err) + + // Verify using the same method as validateAuthToken in indexer.go + msgHash := chainhash.HashB(message) + sig, err := schnorr.ParseSignature(signature) + require.NoError(t, err) + + // Use schnorr pubkey serialization (x-coordinate only) like indexer does + pubKeyBytes := schnorr.SerializePubKey(privKey.PubKey()) + pubKey, err := schnorr.ParsePubKey(pubKeyBytes) + require.NoError(t, err) + + valid := sig.Verify(msgHash, pubKey) + require.True(t, valid, "signature should verify with schnorr pubkey format") +} diff --git a/pkg/arkd-wallet/interface/grpc/handlers/signer_handler.go b/pkg/arkd-wallet/interface/grpc/handlers/signer_handler.go index 08f98ea96..856e0054b 100644 --- a/pkg/arkd-wallet/interface/grpc/handlers/signer_handler.go +++ b/pkg/arkd-wallet/interface/grpc/handlers/signer_handler.go @@ -60,3 +60,13 @@ func (h *signerHandler) SignTransactionTapscript( } return &signerv1.SignTransactionTapscriptResponse{SignedTx: tx}, nil } + +func (h *signerHandler) SignMessage( + ctx context.Context, req *signerv1.SignMessageRequest, +) (*signerv1.SignMessageResponse, error) { + signature, err := h.wallet.SignMessage(ctx, req.GetMessage()) + if err != nil { + return nil, err + } + return &signerv1.SignMessageResponse{Signature: signature}, nil +} \ No newline at end of file From ebe18da1826c936e559d08cfe118c2e832d556a0 Mon Sep 17 00:00:00 2001 From: Bob Smith <5396652+bitcoin-coder-bob@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:14:36 -0400 Subject: [PATCH 2/8] test updates removing uneeded items, add fixtures --- .../wallet/fixtures/service_fixtures.json | 48 ---- .../core/application/wallet/service_test.go | 224 +++++------------- .../wallet/testdata/signmessage_fixtures.json | 54 +++++ 3 files changed, 117 insertions(+), 209 deletions(-) delete mode 100644 pkg/arkd-wallet/core/application/wallet/fixtures/service_fixtures.json create mode 100644 pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json diff --git a/pkg/arkd-wallet/core/application/wallet/fixtures/service_fixtures.json b/pkg/arkd-wallet/core/application/wallet/fixtures/service_fixtures.json deleted file mode 100644 index 217022533..000000000 --- a/pkg/arkd-wallet/core/application/wallet/fixtures/service_fixtures.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "sign_message_tests": { - "test_keys": [ - { - "name": "key_1", - "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001", - "public_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" - }, - { - "name": "key_2", - "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000002", - "public_key_hex": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" - } - ], - "test_cases": [ - { - "name": "simple_message", - "message_hex": "48656c6c6f20576f726c64", - "key_name": "key_1" - }, - { - "name": "empty_message", - "message_hex": "", - "key_name": "key_1" - }, - { - "name": "32_byte_message", - "message_hex": "0000000000000000000000000000000000000000000000000000000000000001", - "key_name": "key_1" - }, - { - "name": "36_byte_auth_token_message", - "message_hex": "000000000000000000000000000000000000000000000000000000000000000100000000", - "key_name": "key_1" - }, - { - "name": "large_message", - "message_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", - "key_name": "key_1" - }, - { - "name": "message_with_different_key", - "message_hex": "48656c6c6f20576f726c64", - "key_name": "key_2" - } - ] - } -} diff --git a/pkg/arkd-wallet/core/application/wallet/service_test.go b/pkg/arkd-wallet/core/application/wallet/service_test.go index c4f66a9fe..e9cfeff41 100644 --- a/pkg/arkd-wallet/core/application/wallet/service_test.go +++ b/pkg/arkd-wallet/core/application/wallet/service_test.go @@ -13,13 +13,10 @@ import ( "github.com/stretchr/testify/require" ) -type serviceFixtures struct { - SignMessageTests signMessageTestFixtures `json:"sign_message_tests"` -} - -type signMessageTestFixtures struct { - TestKeys []testKey `json:"test_keys"` - TestCases []signMessageTestCase `json:"test_cases"` +type signMessageFixtures struct { + TestKeys []testKey `json:"test_keys"` + Valid []signMessageTestCase `json:"valid"` + Invalid []signMessageInvalidCase `json:"invalid"` } type testKey struct { @@ -34,19 +31,28 @@ type signMessageTestCase struct { KeyName string `json:"key_name"` } -func loadServiceFixtures(t *testing.T) *serviceFixtures { - data, err := os.ReadFile("fixtures/service_fixtures.json") +type signMessageInvalidCase struct { + Name string `json:"name"` + MessageHex string `json:"message_hex"` + KeyName string `json:"key_name"` + ExpectedError string `json:"expected_error"` +} + +func loadSignMessageFixtures(t *testing.T) *signMessageFixtures { + t.Helper() + data, err := os.ReadFile("testdata/signmessage_fixtures.json") require.NoError(t, err) - var f serviceFixtures + var f signMessageFixtures err = json.Unmarshal(data, &f) require.NoError(t, err) return &f } -func getTestKey(t *testing.T, fixtures *serviceFixtures, keyName string) *btcec.PrivateKey { - for _, k := range fixtures.SignMessageTests.TestKeys { +func getTestKey(t *testing.T, fixtures *signMessageFixtures, keyName string) *btcec.PrivateKey { + t.Helper() + for _, k := range fixtures.TestKeys { if k.Name == keyName { privKeyBytes, err := hex.DecodeString(k.PrivateKeyHex) require.NoError(t, err) @@ -59,161 +65,57 @@ func getTestKey(t *testing.T, fixtures *serviceFixtures, keyName string) *btcec. } func TestSignMessage(t *testing.T) { - fixtures := loadServiceFixtures(t) - ctx := context.Background() - - for _, tc := range fixtures.SignMessageTests.TestCases { - t.Run(tc.Name, func(t *testing.T) { - // Get the private key for this test case - privKey := getTestKey(t, fixtures, tc.KeyName) - - // Create wallet with signer key - w := &wallet{ - WalletOptions: WalletOptions{ - SignerKey: privKey, - }, - } - - // Decode message - message, err := hex.DecodeString(tc.MessageHex) - require.NoError(t, err) - - // Sign the message - signature, err := w.SignMessage(ctx, message) - require.NoError(t, err) - require.NotNil(t, signature) - - // Schnorr signatures are always 64 bytes - require.Len(t, signature, 64, "schnorr signature should be 64 bytes") - - // Verify the signature is valid - msgHash := chainhash.HashB(message) - sig, err := schnorr.ParseSignature(signature) - require.NoError(t, err) - - pubKey := privKey.PubKey() - valid := sig.Verify(msgHash, pubKey) - require.True(t, valid, "signature should be valid") - }) - } -} - -func TestSignMessage_NoSignerKey(t *testing.T) { - ctx := context.Background() - - // Create wallet without signer key - w := &wallet{ - WalletOptions: WalletOptions{ - SignerKey: nil, - }, - } - - message := []byte("test message") - signature, err := w.SignMessage(ctx, message) - - require.Error(t, err) - require.Nil(t, signature) - require.Contains(t, err.Error(), "signer key not loaded") -} - -func TestSignMessage_DifferentKeysProduceDifferentSignatures(t *testing.T) { - fixtures := loadServiceFixtures(t) - ctx := context.Background() - - // Get two different keys - privKey1 := getTestKey(t, fixtures, "key_1") - privKey2 := getTestKey(t, fixtures, "key_2") - - w1 := &wallet{WalletOptions: WalletOptions{SignerKey: privKey1}} - w2 := &wallet{WalletOptions: WalletOptions{SignerKey: privKey2}} - - message := []byte("same message") - - sig1, err := w1.SignMessage(ctx, message) - require.NoError(t, err) - - sig2, err := w2.SignMessage(ctx, message) - require.NoError(t, err) - - // Signatures should be different (different keys) - require.NotEqual(t, sig1, sig2, "different keys should produce different signatures") - - // But both should be valid for their respective keys - msgHash := chainhash.HashB(message) - - parsedSig1, err := schnorr.ParseSignature(sig1) - require.NoError(t, err) - require.True(t, parsedSig1.Verify(msgHash, privKey1.PubKey())) - - parsedSig2, err := schnorr.ParseSignature(sig2) - require.NoError(t, err) - require.True(t, parsedSig2.Verify(msgHash, privKey2.PubKey())) - - // Cross-verification should fail - require.False(t, parsedSig1.Verify(msgHash, privKey2.PubKey()), "sig1 should not verify with key2") - require.False(t, parsedSig2.Verify(msgHash, privKey1.PubKey()), "sig2 should not verify with key1") -} - -func TestSignMessage_DifferentMessagesProduceDifferentSignatures(t *testing.T) { - fixtures := loadServiceFixtures(t) + fixtures := loadSignMessageFixtures(t) ctx := context.Background() - privKey := getTestKey(t, fixtures, "key_1") - w := &wallet{WalletOptions: WalletOptions{SignerKey: privKey}} + t.Run("valid", func(t *testing.T) { + for _, tc := range fixtures.Valid { + t.Run(tc.Name, func(t *testing.T) { + privKey := getTestKey(t, fixtures, tc.KeyName) - message1 := []byte("message one") - message2 := []byte("message two") + w := &wallet{ + WalletOptions: WalletOptions{ + SignerKey: privKey, + }, + } - sig1, err := w.SignMessage(ctx, message1) - require.NoError(t, err) - - sig2, err := w.SignMessage(ctx, message2) - require.NoError(t, err) + message, err := hex.DecodeString(tc.MessageHex) + require.NoError(t, err) - // Signatures should be different (different messages) - require.NotEqual(t, sig1, sig2, "different messages should produce different signatures") + signature, err := w.SignMessage(ctx, message) + require.NoError(t, err) + require.NotNil(t, signature) - // Each signature should only verify with its corresponding message - pubKey := privKey.PubKey() + require.Len(t, signature, 64, "schnorr signature should be 64 bytes") - parsedSig1, err := schnorr.ParseSignature(sig1) - require.NoError(t, err) - require.True(t, parsedSig1.Verify(chainhash.HashB(message1), pubKey)) - require.False(t, parsedSig1.Verify(chainhash.HashB(message2), pubKey)) + msgHash := chainhash.HashB(message) + sig, err := schnorr.ParseSignature(signature) + require.NoError(t, err) - parsedSig2, err := schnorr.ParseSignature(sig2) - require.NoError(t, err) - require.True(t, parsedSig2.Verify(chainhash.HashB(message2), pubKey)) - require.False(t, parsedSig2.Verify(chainhash.HashB(message1), pubKey)) -} - -func TestSignMessage_ConsistentWithSchnorrVerify(t *testing.T) { - fixtures := loadServiceFixtures(t) - ctx := context.Background() - - privKey := getTestKey(t, fixtures, "key_1") - w := &wallet{WalletOptions: WalletOptions{SignerKey: privKey}} - - // Test with the 36-byte auth token message format - // This matches what indexer.go uses for auth tokens - txid := make([]byte, 32) - txid[31] = 0x01 // txid = ...0001 - vout := []byte{0x00, 0x00, 0x00, 0x2a} // vout = 42 (big endian) - message := append(txid, vout...) - - signature, err := w.SignMessage(ctx, message) - require.NoError(t, err) - - // Verify using the same method as validateAuthToken in indexer.go - msgHash := chainhash.HashB(message) - sig, err := schnorr.ParseSignature(signature) - require.NoError(t, err) - - // Use schnorr pubkey serialization (x-coordinate only) like indexer does - pubKeyBytes := schnorr.SerializePubKey(privKey.PubKey()) - pubKey, err := schnorr.ParsePubKey(pubKeyBytes) - require.NoError(t, err) - - valid := sig.Verify(msgHash, pubKey) - require.True(t, valid, "signature should verify with schnorr pubkey format") + pubKey := privKey.PubKey() + valid := sig.Verify(msgHash, pubKey) + require.True(t, valid, "signature should be valid") + }) + } + }) + + t.Run("invalid", func(t *testing.T) { + for _, tc := range fixtures.Invalid { + t.Run(tc.Name, func(t *testing.T) { + w := &wallet{ + WalletOptions: WalletOptions{ + SignerKey: nil, + }, + } + + message, err := hex.DecodeString(tc.MessageHex) + require.NoError(t, err) + + signature, err := w.SignMessage(ctx, message) + require.Error(t, err) + require.Nil(t, signature) + require.Contains(t, err.Error(), tc.ExpectedError) + }) + } + }) } diff --git a/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json b/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json new file mode 100644 index 000000000..a02b08349 --- /dev/null +++ b/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json @@ -0,0 +1,54 @@ +{ + "test_keys": [ + { + "name": "key_1", + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001", + "public_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + { + "name": "key_2", + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000002", + "public_key_hex": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + ], + "valid": [ + { + "name": "simple_message", + "message_hex": "48656c6c6f20576f726c64", + "key_name": "key_1" + }, + { + "name": "empty_message", + "message_hex": "", + "key_name": "key_1" + }, + { + "name": "32_byte_message", + "message_hex": "0000000000000000000000000000000000000000000000000000000000000001", + "key_name": "key_1" + }, + { + "name": "36_byte_auth_token_message", + "message_hex": "000000000000000000000000000000000000000000000000000000000000000100000000", + "key_name": "key_1" + }, + { + "name": "large_message", + "message_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "key_name": "key_1" + }, + { + "name": "message_with_different_key", + "message_hex": "48656c6c6f20576f726c64", + "key_name": "key_2" + } + ], + "invalid": [ + { + "name": "no_signer_key", + "message_hex": "48656c6c6f20576f726c64", + "key_name": "", + "expected_error": "signer key not loaded" + } + ] +} From 31e5b44c9ec7f025541e4fa0d686f27d8d684fa9 Mon Sep 17 00:00:00 2001 From: Bob Smith <5396652+bitcoin-coder-bob@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:45:31 -0400 Subject: [PATCH 3/8] SignMessage: use string instead of bytes in proto --- .../swagger/signer/v1/service.openapi.json | 6 ++---- api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go | 2 +- api-spec/protobuf/gen/signer/v1/service.pb.go | 16 ++++++++-------- api-spec/protobuf/signer/v1/service.proto | 4 ++-- internal/infrastructure/signer/client.go | 8 ++++++-- .../interface/grpc/handlers/signer_handler.go | 11 +++++++++-- 6 files changed, 28 insertions(+), 19 deletions(-) diff --git a/api-spec/openapi/swagger/signer/v1/service.openapi.json b/api-spec/openapi/swagger/signer/v1/service.openapi.json index f34e1160e..533bbf9fe 100644 --- a/api-spec/openapi/swagger/signer/v1/service.openapi.json +++ b/api-spec/openapi/swagger/signer/v1/service.openapi.json @@ -229,8 +229,7 @@ "type": "object", "properties": { "message": { - "type": "string", - "format": "byte" + "type": "string" } } }, @@ -239,8 +238,7 @@ "type": "object", "properties": { "signature": { - "type": "string", - "format": "byte" + "type": "string" } } }, diff --git a/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go b/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go index 6a67b4ec6..397f10840 100644 --- a/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go +++ b/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go @@ -125,7 +125,7 @@ func request_IndexerService_GetConnectors_0(ctx context.Context, marshaler gatew var ( query_params_IndexerService_GetVtxoTree_0 = gateway.QueryParameterParseOptions{ - Filter: trie.New("batch_outpoint.txid", "batch_outpoint.vout", "txid", "vout"), + Filter: trie.New("vout", "batch_outpoint.txid", "batch_outpoint.vout", "txid"), } ) diff --git a/api-spec/protobuf/gen/signer/v1/service.pb.go b/api-spec/protobuf/gen/signer/v1/service.pb.go index 9831d2988..510d1c328 100644 --- a/api-spec/protobuf/gen/signer/v1/service.pb.go +++ b/api-spec/protobuf/gen/signer/v1/service.pb.go @@ -376,7 +376,7 @@ func (x *SignTransactionTapscriptResponse) GetSignedTx() string { type SignMessageRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Message []byte `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -411,16 +411,16 @@ func (*SignMessageRequest) Descriptor() ([]byte, []int) { return file_signer_v1_service_proto_rawDescGZIP(), []int{8} } -func (x *SignMessageRequest) GetMessage() []byte { +func (x *SignMessageRequest) GetMessage() string { if x != nil { return x.Message } - return nil + return "" } type SignMessageResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Signature []byte `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` + Signature string `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -455,11 +455,11 @@ func (*SignMessageResponse) Descriptor() ([]byte, []int) { return file_signer_v1_service_proto_rawDescGZIP(), []int{9} } -func (x *SignMessageResponse) GetSignature() []byte { +func (x *SignMessageResponse) GetSignature() string { if x != nil { return x.Signature } - return nil + return "" } var File_signer_v1_service_proto protoreflect.FileDescriptor @@ -486,9 +486,9 @@ const file_signer_v1_service_proto_rawDesc = "" + " SignTransactionTapscriptResponse\x12\x1b\n" + "\tsigned_tx\x18\x01 \x01(\tR\bsignedTx\".\n" + "\x12SignMessageRequest\x12\x18\n" + - "\amessage\x18\x01 \x01(\fR\amessage\"3\n" + + "\amessage\x18\x01 \x01(\tR\amessage\"3\n" + "\x13SignMessageResponse\x12\x1c\n" + - "\tsignature\x18\x01 \x01(\fR\tsignature2\xbf\x04\n" + + "\tsignature\x18\x01 \x01(\tR\tsignature2\xbf\x04\n" + "\rSignerService\x12W\n" + "\tGetStatus\x12\x1b.signer.v1.GetStatusRequest\x1a\x1c.signer.v1.GetStatusResponse\"\x0f\xb2J\f\x12\n" + "/v1/status\x12W\n" + diff --git a/api-spec/protobuf/signer/v1/service.proto b/api-spec/protobuf/signer/v1/service.proto index 6045200a5..966a4da09 100644 --- a/api-spec/protobuf/signer/v1/service.proto +++ b/api-spec/protobuf/signer/v1/service.proto @@ -63,8 +63,8 @@ message SignTransactionTapscriptResponse { } message SignMessageRequest { - bytes message = 1; + string message = 1; } message SignMessageResponse { - bytes signature = 1; + string signature = 1; } \ No newline at end of file diff --git a/internal/infrastructure/signer/client.go b/internal/infrastructure/signer/client.go index 5e9daf3d1..b481448e1 100644 --- a/internal/infrastructure/signer/client.go +++ b/internal/infrastructure/signer/client.go @@ -104,10 +104,14 @@ func (c *signerClient) SignMessage( ctx context.Context, message []byte, ) ([]byte, error) { resp, err := c.client.SignMessage(ctx, &signerv1.SignMessageRequest{ - Message: message, + Message: hex.EncodeToString(message), }) if err != nil { return nil, err } - return resp.GetSignature(), nil + sig, err := hex.DecodeString(resp.GetSignature()) + if err != nil { + return nil, fmt.Errorf("failed to decode signature hex: %w", err) + } + return sig, nil } diff --git a/pkg/arkd-wallet/interface/grpc/handlers/signer_handler.go b/pkg/arkd-wallet/interface/grpc/handlers/signer_handler.go index 856e0054b..c00a53bbd 100644 --- a/pkg/arkd-wallet/interface/grpc/handlers/signer_handler.go +++ b/pkg/arkd-wallet/interface/grpc/handlers/signer_handler.go @@ -2,9 +2,12 @@ package handlers import ( "context" + "encoding/hex" signerv1 "github.com/arkade-os/arkd/api-spec/protobuf/gen/signer/v1" application "github.com/arkade-os/arkd/pkg/arkd-wallet/core/application" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) type signerHandler struct { @@ -64,9 +67,13 @@ func (h *signerHandler) SignTransactionTapscript( func (h *signerHandler) SignMessage( ctx context.Context, req *signerv1.SignMessageRequest, ) (*signerv1.SignMessageResponse, error) { - signature, err := h.wallet.SignMessage(ctx, req.GetMessage()) + message, err := hex.DecodeString(req.GetMessage()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid message hex: %s", err) + } + signature, err := h.wallet.SignMessage(ctx, message) if err != nil { return nil, err } - return &signerv1.SignMessageResponse{Signature: signature}, nil + return &signerv1.SignMessageResponse{Signature: hex.EncodeToString(signature)}, nil } \ No newline at end of file From db3e733789afa84020350b325717f1f4b4c5ca2f Mon Sep 17 00:00:00 2001 From: Bob Smith <5396652+bitcoin-coder-bob@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:59:35 -0400 Subject: [PATCH 4/8] update proto and openapi.json to have comment for hex-encoded message to sign --- .../openapi/swagger/signer/v1/service.openapi.json | 6 ++++-- api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go | 2 +- api-spec/protobuf/gen/signer/v1/service.pb.go | 10 ++++++---- api-spec/protobuf/signer/v1/service.proto | 2 ++ 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/api-spec/openapi/swagger/signer/v1/service.openapi.json b/api-spec/openapi/swagger/signer/v1/service.openapi.json index 533bbf9fe..a33f20c51 100644 --- a/api-spec/openapi/swagger/signer/v1/service.openapi.json +++ b/api-spec/openapi/swagger/signer/v1/service.openapi.json @@ -229,7 +229,8 @@ "type": "object", "properties": { "message": { - "type": "string" + "type": "string", + "description": "hex-encoded message to sign" } } }, @@ -238,7 +239,8 @@ "type": "object", "properties": { "signature": { - "type": "string" + "type": "string", + "description": "hex-encoded Schnorr signature" } } }, diff --git a/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go b/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go index 397f10840..eb96f485c 100644 --- a/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go +++ b/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go @@ -125,7 +125,7 @@ func request_IndexerService_GetConnectors_0(ctx context.Context, marshaler gatew var ( query_params_IndexerService_GetVtxoTree_0 = gateway.QueryParameterParseOptions{ - Filter: trie.New("vout", "batch_outpoint.txid", "batch_outpoint.vout", "txid"), + Filter: trie.New("txid", "vout", "batch_outpoint.txid", "batch_outpoint.vout"), } ) diff --git a/api-spec/protobuf/gen/signer/v1/service.pb.go b/api-spec/protobuf/gen/signer/v1/service.pb.go index 510d1c328..a225fcc6e 100644 --- a/api-spec/protobuf/gen/signer/v1/service.pb.go +++ b/api-spec/protobuf/gen/signer/v1/service.pb.go @@ -375,8 +375,9 @@ func (x *SignTransactionTapscriptResponse) GetSignedTx() string { } type SignMessageRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // hex-encoded message to sign + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -419,8 +420,9 @@ func (x *SignMessageRequest) GetMessage() string { } type SignMessageResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Signature string `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // hex-encoded Schnorr signature + Signature string `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } diff --git a/api-spec/protobuf/signer/v1/service.proto b/api-spec/protobuf/signer/v1/service.proto index 966a4da09..14035c7d2 100644 --- a/api-spec/protobuf/signer/v1/service.proto +++ b/api-spec/protobuf/signer/v1/service.proto @@ -63,8 +63,10 @@ message SignTransactionTapscriptResponse { } message SignMessageRequest { + // hex-encoded message to sign string message = 1; } message SignMessageResponse { + // hex-encoded Schnorr signature string signature = 1; } \ No newline at end of file From fa47e2516f808f4ddef6e27c2a84ad29a16bf214 Mon Sep 17 00:00:00 2001 From: Bob Smith <5396652+bitcoin-coder-bob@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:22:01 -0400 Subject: [PATCH 5/8] enforce in SignMessage hex string pattern/length in proto --- .../openapi/swagger/signer/v1/service.openapi.json | 4 ++++ api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go | 2 +- api-spec/protobuf/gen/signer/v1/service.pb.go | 10 +++++----- api-spec/protobuf/signer/v1/service.proto | 10 ++++++++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/api-spec/openapi/swagger/signer/v1/service.openapi.json b/api-spec/openapi/swagger/signer/v1/service.openapi.json index a33f20c51..514c8ca7a 100644 --- a/api-spec/openapi/swagger/signer/v1/service.openapi.json +++ b/api-spec/openapi/swagger/signer/v1/service.openapi.json @@ -229,6 +229,7 @@ "type": "object", "properties": { "message": { + "pattern": "^(?:[0-9a-fA-F]{2})*$", "type": "string", "description": "hex-encoded message to sign" } @@ -239,6 +240,9 @@ "type": "object", "properties": { "signature": { + "pattern": "^[0-9a-fA-F]{128}$", + "maxLength": 128, + "minLength": 128, "type": "string", "description": "hex-encoded Schnorr signature" } diff --git a/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go b/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go index eb96f485c..e6d7244ac 100644 --- a/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go +++ b/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go @@ -125,7 +125,7 @@ func request_IndexerService_GetConnectors_0(ctx context.Context, marshaler gatew var ( query_params_IndexerService_GetVtxoTree_0 = gateway.QueryParameterParseOptions{ - Filter: trie.New("txid", "vout", "batch_outpoint.txid", "batch_outpoint.vout"), + Filter: trie.New("batch_outpoint.vout", "batch_outpoint.txid", "txid", "vout"), } ) diff --git a/api-spec/protobuf/gen/signer/v1/service.pb.go b/api-spec/protobuf/gen/signer/v1/service.pb.go index a225fcc6e..e157af4cb 100644 --- a/api-spec/protobuf/gen/signer/v1/service.pb.go +++ b/api-spec/protobuf/gen/signer/v1/service.pb.go @@ -486,11 +486,11 @@ const file_signer_v1_service_proto_rawDesc = "" + "partial_tx\x18\x01 \x01(\tR\tpartialTx\x12#\n" + "\rinput_indexes\x18\x02 \x03(\x05R\finputIndexes\"?\n" + " SignTransactionTapscriptResponse\x12\x1b\n" + - "\tsigned_tx\x18\x01 \x01(\tR\bsignedTx\".\n" + - "\x12SignMessageRequest\x12\x18\n" + - "\amessage\x18\x01 \x01(\tR\amessage\"3\n" + - "\x13SignMessageResponse\x12\x1c\n" + - "\tsignature\x18\x01 \x01(\tR\tsignature2\xbf\x04\n" + + "\tsigned_tx\x18\x01 \x01(\tR\bsignedTx\"J\n" + + "\x12SignMessageRequest\x124\n" + + "\amessage\x18\x01 \x01(\tB\x1a\xbaJ\x17b\x15^(?:[0-9a-fA-F]{2})*$R\amessage\"T\n" + + "\x13SignMessageResponse\x12=\n" + + "\tsignature\x18\x01 \x01(\tB\x1f\xbaJ\x1cb\x12^[0-9a-fA-F]{128}$\xa0\x01\x80\x01\xa8\x01\x80\x01R\tsignature2\xbf\x04\n" + "\rSignerService\x12W\n" + "\tGetStatus\x12\x1b.signer.v1.GetStatusRequest\x1a\x1c.signer.v1.GetStatusResponse\"\x0f\xb2J\f\x12\n" + "/v1/status\x12W\n" + diff --git a/api-spec/protobuf/signer/v1/service.proto b/api-spec/protobuf/signer/v1/service.proto index 14035c7d2..1ffbdf2f3 100644 --- a/api-spec/protobuf/signer/v1/service.proto +++ b/api-spec/protobuf/signer/v1/service.proto @@ -64,9 +64,15 @@ message SignTransactionTapscriptResponse { message SignMessageRequest { // hex-encoded message to sign - string message = 1; + string message = 1 [(meshapi.gateway.openapi_field) = { + pattern: "^(?:[0-9a-fA-F]{2})*$" + }]; } message SignMessageResponse { // hex-encoded Schnorr signature - string signature = 1; + string signature = 1 [(meshapi.gateway.openapi_field) = { + pattern: "^[0-9a-fA-F]{128}$" + min_length: 128 + max_length: 128 + }]; } \ No newline at end of file From 343a8ae161a3e69eb7cc6aadf7a59071751f094f Mon Sep 17 00:00:00 2001 From: Bob Smith <5396652+bitcoin-coder-bob@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:42:14 -0400 Subject: [PATCH 6/8] move test_keys out of fixtures and into test file directly --- .../core/application/wallet/service_test.go | 34 ++++--------------- .../wallet/testdata/signmessage_fixtures.json | 25 ++++---------- 2 files changed, 13 insertions(+), 46 deletions(-) diff --git a/pkg/arkd-wallet/core/application/wallet/service_test.go b/pkg/arkd-wallet/core/application/wallet/service_test.go index e9cfeff41..8d814b47f 100644 --- a/pkg/arkd-wallet/core/application/wallet/service_test.go +++ b/pkg/arkd-wallet/core/application/wallet/service_test.go @@ -14,27 +14,19 @@ import ( ) type signMessageFixtures struct { - TestKeys []testKey `json:"test_keys"` - Valid []signMessageTestCase `json:"valid"` - Invalid []signMessageInvalidCase `json:"invalid"` + Valid []signMessageTestCase `json:"valid"` + Invalid []signMessageInvalidCase `json:"invalid"` } -type testKey struct { +type signMessageTestCase struct { Name string `json:"name"` + MessageHex string `json:"message_hex"` PrivateKeyHex string `json:"private_key_hex"` - PublicKeyHex string `json:"public_key_hex"` -} - -type signMessageTestCase struct { - Name string `json:"name"` - MessageHex string `json:"message_hex"` - KeyName string `json:"key_name"` } type signMessageInvalidCase struct { Name string `json:"name"` MessageHex string `json:"message_hex"` - KeyName string `json:"key_name"` ExpectedError string `json:"expected_error"` } @@ -50,20 +42,6 @@ func loadSignMessageFixtures(t *testing.T) *signMessageFixtures { return &f } -func getTestKey(t *testing.T, fixtures *signMessageFixtures, keyName string) *btcec.PrivateKey { - t.Helper() - for _, k := range fixtures.TestKeys { - if k.Name == keyName { - privKeyBytes, err := hex.DecodeString(k.PrivateKeyHex) - require.NoError(t, err) - privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes) - return privKey - } - } - t.Fatalf("test key not found: %s", keyName) - return nil -} - func TestSignMessage(t *testing.T) { fixtures := loadSignMessageFixtures(t) ctx := context.Background() @@ -71,7 +49,9 @@ func TestSignMessage(t *testing.T) { t.Run("valid", func(t *testing.T) { for _, tc := range fixtures.Valid { t.Run(tc.Name, func(t *testing.T) { - privKey := getTestKey(t, fixtures, tc.KeyName) + privKeyBytes, err := hex.DecodeString(tc.PrivateKeyHex) + require.NoError(t, err) + privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes) w := &wallet{ WalletOptions: WalletOptions{ diff --git a/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json b/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json index a02b08349..2502bee10 100644 --- a/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json +++ b/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json @@ -1,53 +1,40 @@ { - "test_keys": [ - { - "name": "key_1", - "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001", - "public_key_hex": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" - }, - { - "name": "key_2", - "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000002", - "public_key_hex": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" - } - ], "valid": [ { "name": "simple_message", "message_hex": "48656c6c6f20576f726c64", - "key_name": "key_1" + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001" }, { "name": "empty_message", "message_hex": "", - "key_name": "key_1" + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001" }, { "name": "32_byte_message", "message_hex": "0000000000000000000000000000000000000000000000000000000000000001", - "key_name": "key_1" + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001" }, { "name": "36_byte_auth_token_message", "message_hex": "000000000000000000000000000000000000000000000000000000000000000100000000", - "key_name": "key_1" + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001" }, { "name": "large_message", "message_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", - "key_name": "key_1" + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001" }, { "name": "message_with_different_key", "message_hex": "48656c6c6f20576f726c64", - "key_name": "key_2" + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000002" } ], "invalid": [ { "name": "no_signer_key", "message_hex": "48656c6c6f20576f726c64", - "key_name": "", "expected_error": "signer key not loaded" } ] From d5c296364ce7128e33901981c0a9aa0e237c8d07 Mon Sep 17 00:00:00 2001 From: Bob Smith <5396652+bitcoin-coder-bob@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:10:23 -0400 Subject: [PATCH 7/8] check decoded signature is 64 bytes in SignMessage client --- internal/infrastructure/signer/client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/infrastructure/signer/client.go b/internal/infrastructure/signer/client.go index b481448e1..45b6c5913 100644 --- a/internal/infrastructure/signer/client.go +++ b/internal/infrastructure/signer/client.go @@ -113,5 +113,8 @@ func (c *signerClient) SignMessage( if err != nil { return nil, fmt.Errorf("failed to decode signature hex: %w", err) } + if len(sig) != 64 { + return nil, fmt.Errorf("invalid signature length: expected 64 bytes, got %d", len(sig)) + } return sig, nil } From 49e2b8203ad204670be298d7dddf4f914dfe6636 Mon Sep 17 00:00:00 2001 From: Bob Smith <5396652+bitcoin-coder-bob@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:10:46 -0400 Subject: [PATCH 8/8] ErrSignerDisabled usage --- pkg/arkd-wallet/core/application/wallet/service.go | 2 +- .../core/application/wallet/testdata/signmessage_fixtures.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/arkd-wallet/core/application/wallet/service.go b/pkg/arkd-wallet/core/application/wallet/service.go index 96b29f8e6..3494b0dd2 100644 --- a/pkg/arkd-wallet/core/application/wallet/service.go +++ b/pkg/arkd-wallet/core/application/wallet/service.go @@ -729,7 +729,7 @@ func (w *wallet) LoadSignerKey(ctx context.Context, prvkey *btcec.PrivateKey) er func (w *wallet) SignMessage(ctx context.Context, message []byte) ([]byte, error) { if w.SignerKey == nil { - return nil, fmt.Errorf("signer key not loaded") + return nil, ErrSignerDisabled } msgHash := chainhash.HashB(message) diff --git a/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json b/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json index 2502bee10..aa42a515b 100644 --- a/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json +++ b/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json @@ -35,7 +35,7 @@ { "name": "no_signer_key", "message_hex": "48656c6c6f20576f726c64", - "expected_error": "signer key not loaded" + "expected_error": "signer not enabled" } ] }