mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-05-30 01:30:11 +02:00
walletrpc: add sign/verify methods
Adding the grpc functionality to sign and verify messages with single addresses
This commit is contained in:
parent
266cd97573
commit
7b68289a7a
File diff suppressed because it is too large
Load Diff
@ -362,6 +362,74 @@ func local_request_WalletKit_ListAddresses_0(ctx context.Context, marshaler runt
|
||||
|
||||
}
|
||||
|
||||
func request_WalletKit_SignMessageWithAddr_0(ctx context.Context, marshaler runtime.Marshaler, client WalletKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq SignMessageWithAddrRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
newReader, berr := utilities.IOReaderFactory(req.Body)
|
||||
if berr != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
|
||||
}
|
||||
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.SignMessageWithAddr(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_WalletKit_SignMessageWithAddr_0(ctx context.Context, marshaler runtime.Marshaler, server WalletKitServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq SignMessageWithAddrRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
newReader, berr := utilities.IOReaderFactory(req.Body)
|
||||
if berr != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
|
||||
}
|
||||
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.SignMessageWithAddr(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_WalletKit_VerifyMessageWithAddr_0(ctx context.Context, marshaler runtime.Marshaler, client WalletKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq VerifyMessageWithAddrRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
newReader, berr := utilities.IOReaderFactory(req.Body)
|
||||
if berr != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
|
||||
}
|
||||
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.VerifyMessageWithAddr(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_WalletKit_VerifyMessageWithAddr_0(ctx context.Context, marshaler runtime.Marshaler, server WalletKitServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq VerifyMessageWithAddrRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
newReader, berr := utilities.IOReaderFactory(req.Body)
|
||||
if berr != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
|
||||
}
|
||||
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.VerifyMessageWithAddr(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_WalletKit_ImportAccount_0(ctx context.Context, marshaler runtime.Marshaler, client WalletKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ImportAccountRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
@ -1044,6 +1112,52 @@ func RegisterWalletKitHandlerServer(ctx context.Context, mux *runtime.ServeMux,
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_WalletKit_SignMessageWithAddr_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/walletrpc.WalletKit/SignMessageWithAddr", runtime.WithHTTPPathPattern("/v2/wallet/address/signmessage"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_WalletKit_SignMessageWithAddr_0(rctx, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_WalletKit_SignMessageWithAddr_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_WalletKit_VerifyMessageWithAddr_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/walletrpc.WalletKit/VerifyMessageWithAddr", runtime.WithHTTPPathPattern("/v2/wallet/address/verifymessage"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_WalletKit_VerifyMessageWithAddr_0(rctx, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_WalletKit_VerifyMessageWithAddr_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_WalletKit_ImportAccount_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@ -1584,6 +1698,46 @@ func RegisterWalletKitHandlerClient(ctx context.Context, mux *runtime.ServeMux,
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_WalletKit_SignMessageWithAddr_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/walletrpc.WalletKit/SignMessageWithAddr", runtime.WithHTTPPathPattern("/v2/wallet/address/signmessage"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_WalletKit_SignMessageWithAddr_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_WalletKit_SignMessageWithAddr_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_WalletKit_VerifyMessageWithAddr_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/walletrpc.WalletKit/VerifyMessageWithAddr", runtime.WithHTTPPathPattern("/v2/wallet/address/verifymessage"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_WalletKit_VerifyMessageWithAddr_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_WalletKit_VerifyMessageWithAddr_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_WalletKit_ImportAccount_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@ -1868,6 +2022,10 @@ var (
|
||||
|
||||
pattern_WalletKit_ListAddresses_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "wallet", "addresses"}, ""))
|
||||
|
||||
pattern_WalletKit_SignMessageWithAddr_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "address", "signmessage"}, ""))
|
||||
|
||||
pattern_WalletKit_VerifyMessageWithAddr_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "address", "verifymessage"}, ""))
|
||||
|
||||
pattern_WalletKit_ImportAccount_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "accounts", "import"}, ""))
|
||||
|
||||
pattern_WalletKit_ImportPublicKey_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "key", "import"}, ""))
|
||||
@ -1916,6 +2074,10 @@ var (
|
||||
|
||||
forward_WalletKit_ListAddresses_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_WalletKit_SignMessageWithAddr_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_WalletKit_VerifyMessageWithAddr_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_WalletKit_ImportAccount_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_WalletKit_ImportPublicKey_0 = runtime.ForwardResponseMessage
|
||||
|
@ -272,6 +272,56 @@ func RegisterWalletKitJSONCallbacks(registry map[string]func(ctx context.Context
|
||||
callback(string(respBytes), nil)
|
||||
}
|
||||
|
||||
registry["walletrpc.WalletKit.SignMessageWithAddr"] = func(ctx context.Context,
|
||||
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
|
||||
|
||||
req := &SignMessageWithAddrRequest{}
|
||||
err := marshaler.Unmarshal([]byte(reqJSON), req)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
|
||||
client := NewWalletKitClient(conn)
|
||||
resp, err := client.SignMessageWithAddr(ctx, req)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
|
||||
respBytes, err := marshaler.Marshal(resp)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
callback(string(respBytes), nil)
|
||||
}
|
||||
|
||||
registry["walletrpc.WalletKit.VerifyMessageWithAddr"] = func(ctx context.Context,
|
||||
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
|
||||
|
||||
req := &VerifyMessageWithAddrRequest{}
|
||||
err := marshaler.Unmarshal([]byte(reqJSON), req)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
|
||||
client := NewWalletKitClient(conn)
|
||||
resp, err := client.VerifyMessageWithAddr(ctx, req)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
|
||||
respBytes, err := marshaler.Marshal(resp)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
callback(string(respBytes), nil)
|
||||
}
|
||||
|
||||
registry["walletrpc.WalletKit.ImportAccount"] = func(ctx context.Context,
|
||||
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
|
||||
|
||||
|
@ -79,6 +79,47 @@ service WalletKit {
|
||||
*/
|
||||
rpc ListAddresses (ListAddressesRequest) returns (ListAddressesResponse);
|
||||
|
||||
/*
|
||||
SignMessageWithAddr returns the compact signature (base64 encoded) created
|
||||
with the private key of the provided address. This requires the address
|
||||
to be solely based on a public key lock (no scripts). Obviously the internal
|
||||
lnd wallet has to possess the private key of the address otherwise
|
||||
an error is returned.
|
||||
|
||||
This method aims to provide full compatibility with the bitcoin-core and
|
||||
btcd implementation. Bitcoin-core's algorithm is not specified in a
|
||||
BIP and only applicable for legacy addresses. This method enhances the
|
||||
signing for additional address types: P2WKH, NP2WKH, P2TR.
|
||||
For P2TR addresses this represents a special case. ECDSA is used to create
|
||||
a compact signature which makes the public key of the signature recoverable.
|
||||
*/
|
||||
rpc SignMessageWithAddr (SignMessageWithAddrRequest)
|
||||
returns (SignMessageWithAddrResponse);
|
||||
|
||||
/*
|
||||
VerifyMessageWithAddr returns the validity and the recovered public key of
|
||||
the provided compact signature (base64 encoded). The verification is
|
||||
twofold. First the validity of the signature itself is checked and then
|
||||
it is verified that the recovered public key of the signature equals
|
||||
the public key of the provided address. There is no dependence on the
|
||||
private key of the address therefore also external addresses are allowed
|
||||
to verify signatures.
|
||||
Supported address types are P2PKH, P2WKH, NP2WKH, P2TR.
|
||||
|
||||
This method is the counterpart of the related signing method
|
||||
(SignMessageWithAddr) and aims to provide full compatibility to
|
||||
bitcoin-core's implementation. Although bitcoin-core/btcd only provide
|
||||
this functionality for legacy addresses this function enhances it to
|
||||
the address types: P2PKH, P2WKH, NP2WKH, P2TR.
|
||||
|
||||
The verification for P2TR addresses is a special case and requires the
|
||||
ECDSA compact signature to compare the reovered public key to the internal
|
||||
taproot key. The compact ECDSA signature format was used because there
|
||||
are still no known compact signature schemes for schnorr signatures.
|
||||
*/
|
||||
rpc VerifyMessageWithAddr (VerifyMessageWithAddrRequest)
|
||||
returns (VerifyMessageWithAddrResponse);
|
||||
|
||||
/*
|
||||
ImportAccount imports an account backed by an account extended public key.
|
||||
The master key fingerprint denotes the fingerprint of the root key
|
||||
@ -497,6 +538,43 @@ message ListAddressesResponse {
|
||||
repeated AccountWithAddresses account_with_addresses = 1;
|
||||
}
|
||||
|
||||
message SignMessageWithAddrRequest {
|
||||
// The message to be signed. When using REST, this field must be encoded as
|
||||
// base64.
|
||||
bytes msg = 1;
|
||||
|
||||
// The address which will be used to look up the private key and sign the
|
||||
// corresponding message.
|
||||
string addr = 2;
|
||||
}
|
||||
|
||||
message SignMessageWithAddrResponse {
|
||||
// The compact ECDSA signature for the given message encoded in base64.
|
||||
string signature = 1;
|
||||
}
|
||||
|
||||
message VerifyMessageWithAddrRequest {
|
||||
// The message to be signed. When using REST, this field must be encoded as
|
||||
// base64.
|
||||
bytes msg = 1;
|
||||
|
||||
// The compact ECDSA signature to be verified over the given message
|
||||
// ecoded in base64.
|
||||
string signature = 2;
|
||||
|
||||
// The address which will be used to look up the public key and verify the
|
||||
// the signature.
|
||||
string addr = 3;
|
||||
}
|
||||
|
||||
message VerifyMessageWithAddrResponse {
|
||||
// Whether the signature was valid over the given message.
|
||||
bool valid = 1;
|
||||
|
||||
// The pubkey recovered from the signature.
|
||||
bytes pubkey = 2;
|
||||
}
|
||||
|
||||
message ImportAccountRequest {
|
||||
// A name to identify the account with.
|
||||
string name = 1;
|
||||
|
@ -130,6 +130,74 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/wallet/address/signmessage": {
|
||||
"post": {
|
||||
"summary": "SignMessageWithAddr returns the compact signature (base64 encoded) created\nwith the private key of the provided address. This requires the address\nto be solely based on a public key lock (no scripts). Obviously the internal\nlnd wallet has to possess the private key of the address otherwise\nan error is returned.",
|
||||
"description": "This method aims to provide full compatibility with the bitcoin-core and\nbtcd implementation. Bitcoin-core's algorithm is not specified in a\nBIP and only applicable for legacy addresses. This method enhances the\nsigning for additional address types: P2WKH, NP2WKH, P2TR.\nFor P2TR addresses this represents a special case. ECDSA is used to create\na compact signature which makes the public key of the signature recoverable.",
|
||||
"operationId": "WalletKit_SignMessageWithAddr",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/walletrpcSignMessageWithAddrResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/walletrpcSignMessageWithAddrRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"WalletKit"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/wallet/address/verifymessage": {
|
||||
"post": {
|
||||
"summary": "VerifyMessageWithAddr returns the validity and the recovered public key of\nthe provided compact signature (base64 encoded). The verification is\ntwofold. First the validity of the signature itself is checked and then\nit is verified that the recovered public key of the signature equals\nthe public key of the provided address. There is no dependence on the\nprivate key of the address therefore also external addresses are allowed\nto verify signatures.\nSupported address types are P2PKH, P2WKH, NP2WKH, P2TR.",
|
||||
"description": "This method is the counterpart of the related signing method\n(SignMessageWithAddr) and aims to provide full compatibility to\nbitcoin-core's implementation. Although bitcoin-core/btcd only provide\nthis functionality for legacy addresses this function enhances it to\nthe address types: P2PKH, P2WKH, NP2WKH, P2TR.\n\nThe verification for P2TR addresses is a special case and requires the\nECDSA compact signature to compare the reovered public key to the internal\ntaproot key. The compact ECDSA signature format was used because there\nare still no known compact signature schemes for schnorr signatures.",
|
||||
"operationId": "WalletKit_VerifyMessageWithAddr",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/walletrpcVerifyMessageWithAddrResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/walletrpcVerifyMessageWithAddrRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"WalletKit"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/wallet/addresses": {
|
||||
"get": {
|
||||
"summary": "ListAddresses retrieves all the addresses along with their balance. An\naccount name filter can be provided to filter through all of the\nwallet accounts and return the addresses of only those matching.",
|
||||
@ -1714,6 +1782,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"walletrpcSignMessageWithAddrRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "The message to be signed. When using REST, this field must be encoded as\nbase64."
|
||||
},
|
||||
"addr": {
|
||||
"type": "string",
|
||||
"description": "The address which will be used to look up the private key and sign the\ncorresponding message."
|
||||
}
|
||||
}
|
||||
},
|
||||
"walletrpcSignMessageWithAddrResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"signature": {
|
||||
"type": "string",
|
||||
"description": "The compact ECDSA signature for the given message encoded in base64."
|
||||
}
|
||||
}
|
||||
},
|
||||
"walletrpcSignPsbtRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -1846,6 +1937,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"walletrpcVerifyMessageWithAddrRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "The message to be signed. When using REST, this field must be encoded as\nbase64."
|
||||
},
|
||||
"signature": {
|
||||
"type": "string",
|
||||
"description": "The compact ECDSA signature to be verified over the given message\necoded in base64."
|
||||
},
|
||||
"addr": {
|
||||
"type": "string",
|
||||
"description": "The address which will be used to look up the public key and verify the\nthe signature."
|
||||
}
|
||||
}
|
||||
},
|
||||
"walletrpcVerifyMessageWithAddrResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"valid": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the signature was valid over the given message."
|
||||
},
|
||||
"pubkey": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "The pubkey recovered from the signature."
|
||||
}
|
||||
}
|
||||
},
|
||||
"walletrpcWitnessType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
@ -59,9 +59,15 @@ http:
|
||||
- selector: walletrpc.WalletKit.ListAccounts
|
||||
get: "/v2/wallet/accounts"
|
||||
- selector: walletrpc.WalletKit.RequiredReserve
|
||||
get: "/v2/wallet/reserve"
|
||||
get: "/v2/wallet/reserve"
|
||||
- selector: walletrpc.WalletKit.ListAddresses
|
||||
get: "/v2/wallet/addresses"
|
||||
- selector: walletrpc.WalletKit.ImportAccount
|
||||
post: "/v2/wallet/accounts/import"
|
||||
body: "*"
|
||||
- selector: walletrpc.WalletKit.SignMessageWithAddr
|
||||
post: "/v2/wallet/address/signmessage"
|
||||
body: "*"
|
||||
- selector: walletrpc.WalletKit.VerifyMessageWithAddr
|
||||
post: "/v2/wallet/address/verifymessage"
|
||||
body: "*"
|
||||
|
@ -57,6 +57,39 @@ type WalletKitClient interface {
|
||||
// account name filter can be provided to filter through all of the
|
||||
// wallet accounts and return the addresses of only those matching.
|
||||
ListAddresses(ctx context.Context, in *ListAddressesRequest, opts ...grpc.CallOption) (*ListAddressesResponse, error)
|
||||
// SignMessageWithAddr returns the compact signature (base64 encoded) created
|
||||
// with the private key of the provided address. This requires the address
|
||||
// to be solely based on a public key lock (no scripts). Obviously the internal
|
||||
// lnd wallet has to possess the private key of the address otherwise
|
||||
// an error is returned.
|
||||
//
|
||||
// This method aims to provide full compatibility with the bitcoin-core and
|
||||
// btcd implementation. Bitcoin-core's algorithm is not specified in a
|
||||
// BIP and only applicable for legacy addresses. This method enhances the
|
||||
// signing for additional address types: P2WKH, NP2WKH, P2TR.
|
||||
// For P2TR addresses this represents a special case. ECDSA is used to create
|
||||
// a compact signature which makes the public key of the signature recoverable.
|
||||
SignMessageWithAddr(ctx context.Context, in *SignMessageWithAddrRequest, opts ...grpc.CallOption) (*SignMessageWithAddrResponse, error)
|
||||
// VerifyMessageWithAddr returns the validity and the recovered public key of
|
||||
// the provided compact signature (base64 encoded). The verification is
|
||||
// twofold. First the validity of the signature itself is checked and then
|
||||
// it is verified that the recovered public key of the signature equals
|
||||
// the public key of the provided address. There is no dependence on the
|
||||
// private key of the address therefore also external addresses are allowed
|
||||
// to verify signatures.
|
||||
// Supported address types are P2PKH, P2WKH, NP2WKH, P2TR.
|
||||
//
|
||||
// This method is the counterpart of the related signing method
|
||||
// (SignMessageWithAddr) and aims to provide full compatibility to
|
||||
// bitcoin-core's implementation. Although bitcoin-core/btcd only provide
|
||||
// this functionality for legacy addresses this function enhances it to
|
||||
// the address types: P2PKH, P2WKH, NP2WKH, P2TR.
|
||||
//
|
||||
// The verification for P2TR addresses is a special case and requires the
|
||||
// ECDSA compact signature to compare the reovered public key to the internal
|
||||
// taproot key. The compact ECDSA signature format was used because there
|
||||
// are still no known compact signature schemes for schnorr signatures.
|
||||
VerifyMessageWithAddr(ctx context.Context, in *VerifyMessageWithAddrRequest, opts ...grpc.CallOption) (*VerifyMessageWithAddrResponse, error)
|
||||
// ImportAccount imports an account backed by an account extended public key.
|
||||
// The master key fingerprint denotes the fingerprint of the root key
|
||||
// corresponding to the account public key (also known as the key with
|
||||
@ -303,6 +336,24 @@ func (c *walletKitClient) ListAddresses(ctx context.Context, in *ListAddressesRe
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletKitClient) SignMessageWithAddr(ctx context.Context, in *SignMessageWithAddrRequest, opts ...grpc.CallOption) (*SignMessageWithAddrResponse, error) {
|
||||
out := new(SignMessageWithAddrResponse)
|
||||
err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/SignMessageWithAddr", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletKitClient) VerifyMessageWithAddr(ctx context.Context, in *VerifyMessageWithAddrRequest, opts ...grpc.CallOption) (*VerifyMessageWithAddrResponse, error) {
|
||||
out := new(VerifyMessageWithAddrResponse)
|
||||
err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/VerifyMessageWithAddr", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletKitClient) ImportAccount(ctx context.Context, in *ImportAccountRequest, opts ...grpc.CallOption) (*ImportAccountResponse, error) {
|
||||
out := new(ImportAccountResponse)
|
||||
err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/ImportAccount", in, out, opts...)
|
||||
@ -462,6 +513,39 @@ type WalletKitServer interface {
|
||||
// account name filter can be provided to filter through all of the
|
||||
// wallet accounts and return the addresses of only those matching.
|
||||
ListAddresses(context.Context, *ListAddressesRequest) (*ListAddressesResponse, error)
|
||||
// SignMessageWithAddr returns the compact signature (base64 encoded) created
|
||||
// with the private key of the provided address. This requires the address
|
||||
// to be solely based on a public key lock (no scripts). Obviously the internal
|
||||
// lnd wallet has to possess the private key of the address otherwise
|
||||
// an error is returned.
|
||||
//
|
||||
// This method aims to provide full compatibility with the bitcoin-core and
|
||||
// btcd implementation. Bitcoin-core's algorithm is not specified in a
|
||||
// BIP and only applicable for legacy addresses. This method enhances the
|
||||
// signing for additional address types: P2WKH, NP2WKH, P2TR.
|
||||
// For P2TR addresses this represents a special case. ECDSA is used to create
|
||||
// a compact signature which makes the public key of the signature recoverable.
|
||||
SignMessageWithAddr(context.Context, *SignMessageWithAddrRequest) (*SignMessageWithAddrResponse, error)
|
||||
// VerifyMessageWithAddr returns the validity and the recovered public key of
|
||||
// the provided compact signature (base64 encoded). The verification is
|
||||
// twofold. First the validity of the signature itself is checked and then
|
||||
// it is verified that the recovered public key of the signature equals
|
||||
// the public key of the provided address. There is no dependence on the
|
||||
// private key of the address therefore also external addresses are allowed
|
||||
// to verify signatures.
|
||||
// Supported address types are P2PKH, P2WKH, NP2WKH, P2TR.
|
||||
//
|
||||
// This method is the counterpart of the related signing method
|
||||
// (SignMessageWithAddr) and aims to provide full compatibility to
|
||||
// bitcoin-core's implementation. Although bitcoin-core/btcd only provide
|
||||
// this functionality for legacy addresses this function enhances it to
|
||||
// the address types: P2PKH, P2WKH, NP2WKH, P2TR.
|
||||
//
|
||||
// The verification for P2TR addresses is a special case and requires the
|
||||
// ECDSA compact signature to compare the reovered public key to the internal
|
||||
// taproot key. The compact ECDSA signature format was used because there
|
||||
// are still no known compact signature schemes for schnorr signatures.
|
||||
VerifyMessageWithAddr(context.Context, *VerifyMessageWithAddrRequest) (*VerifyMessageWithAddrResponse, error)
|
||||
// ImportAccount imports an account backed by an account extended public key.
|
||||
// The master key fingerprint denotes the fingerprint of the root key
|
||||
// corresponding to the account public key (also known as the key with
|
||||
@ -645,6 +729,12 @@ func (UnimplementedWalletKitServer) RequiredReserve(context.Context, *RequiredRe
|
||||
func (UnimplementedWalletKitServer) ListAddresses(context.Context, *ListAddressesRequest) (*ListAddressesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListAddresses not implemented")
|
||||
}
|
||||
func (UnimplementedWalletKitServer) SignMessageWithAddr(context.Context, *SignMessageWithAddrRequest) (*SignMessageWithAddrResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SignMessageWithAddr not implemented")
|
||||
}
|
||||
func (UnimplementedWalletKitServer) VerifyMessageWithAddr(context.Context, *VerifyMessageWithAddrRequest) (*VerifyMessageWithAddrResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VerifyMessageWithAddr not implemented")
|
||||
}
|
||||
func (UnimplementedWalletKitServer) ImportAccount(context.Context, *ImportAccountRequest) (*ImportAccountResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ImportAccount not implemented")
|
||||
}
|
||||
@ -877,6 +967,42 @@ func _WalletKit_ListAddresses_Handler(srv interface{}, ctx context.Context, dec
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletKit_SignMessageWithAddr_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SignMessageWithAddrRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletKitServer).SignMessageWithAddr(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/walletrpc.WalletKit/SignMessageWithAddr",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletKitServer).SignMessageWithAddr(ctx, req.(*SignMessageWithAddrRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletKit_VerifyMessageWithAddr_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(VerifyMessageWithAddrRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletKitServer).VerifyMessageWithAddr(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/walletrpc.WalletKit/VerifyMessageWithAddr",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletKitServer).VerifyMessageWithAddr(ctx, req.(*VerifyMessageWithAddrRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletKit_ImportAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ImportAccountRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -1158,6 +1284,14 @@ var WalletKit_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ListAddresses",
|
||||
Handler: _WalletKit_ListAddresses_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SignMessageWithAddr",
|
||||
Handler: _WalletKit_SignMessageWithAddr_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "VerifyMessageWithAddr",
|
||||
Handler: _WalletKit_VerifyMessageWithAddr_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ImportAccount",
|
||||
Handler: _WalletKit_ImportAccount_Handler,
|
||||
|
@ -6,6 +6,7 @@ package walletrpc
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -17,6 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
@ -126,6 +128,14 @@ var (
|
||||
Entity: "onchain",
|
||||
Action: "read",
|
||||
}},
|
||||
"/walletrpc.WalletKit/SignMessageWithAddr": {{
|
||||
Entity: "onchain",
|
||||
Action: "write",
|
||||
}},
|
||||
"/walletrpc.WalletKit/VerifyMessageWithAddr": {{
|
||||
Entity: "onchain",
|
||||
Action: "write",
|
||||
}},
|
||||
"/walletrpc.WalletKit/FundPsbt": {{
|
||||
Entity: "onchain",
|
||||
Action: "write",
|
||||
@ -1625,6 +1635,182 @@ func parseAddrType(addrType AddressType,
|
||||
}
|
||||
}
|
||||
|
||||
// msgSignaturePrefix is a prefix used to prevent inadvertently signing a
|
||||
// transaction or a signature. It is prepended in front of the message and
|
||||
// follows the same standard as bitcoin core and btcd.
|
||||
const msgSignaturePrefix = "Bitcoin Signed Message:\n"
|
||||
|
||||
// SignMessageWithAddr signs a message with the private key of the provided
|
||||
// address. The address needs to belong to the lnd wallet.
|
||||
func (w *WalletKit) SignMessageWithAddr(ctx context.Context,
|
||||
req *SignMessageWithAddrRequest) (*SignMessageWithAddrResponse, error) {
|
||||
|
||||
addr, err := btcutil.DecodeAddress(req.Addr, w.cfg.ChainParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode address: %w", err)
|
||||
}
|
||||
|
||||
if !addr.IsForNet(w.cfg.ChainParams) {
|
||||
return nil, fmt.Errorf("encoded address is for "+
|
||||
"the wrong network %s", req.Addr)
|
||||
}
|
||||
|
||||
// Fetch address infos from own wallet and check whether it belongs
|
||||
// to the lnd wallet.
|
||||
managedAddr, err := w.cfg.Wallet.AddressInfo(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("address could not be found in the "+
|
||||
"wallet database: %w", err)
|
||||
}
|
||||
|
||||
// Verifying by checking the interface type that the wallet knows about
|
||||
// the public and private keys so it can sign the message with the
|
||||
// private key of this address.
|
||||
pubKey, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("private key to address is unknown")
|
||||
}
|
||||
|
||||
digest, err := doubleHashMessage(msgSignaturePrefix, string(req.Msg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For all address types (P2WKH, NP2WKH,P2TR) the ECDSA compact signing
|
||||
// algorithm is used. For P2TR addresses this represents a special case.
|
||||
// ECDSA is used to create a compact signature which makes the public
|
||||
// key of the signature recoverable. For Schnorr no known compact
|
||||
// signing algorithm exists yet.
|
||||
privKey, err := pubKey.PrivKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no private key could be "+
|
||||
"fetched from wallet database: %w", err)
|
||||
}
|
||||
|
||||
sigBytes, err := ecdsa.SignCompact(privKey, digest, pubKey.Compressed())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create signature: %w", err)
|
||||
}
|
||||
|
||||
// Bitcoin signatures are base64 encoded (being compatible with
|
||||
// bitcoin-core and btcd).
|
||||
sig := base64.StdEncoding.EncodeToString(sigBytes)
|
||||
|
||||
return &SignMessageWithAddrResponse{
|
||||
Signature: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyMessageWithAddr verifies a signature on a message with a provided
|
||||
// address, it checks both the validity of the signature itself and then
|
||||
// verifies whether the signature corresponds to the public key of the
|
||||
// provided address. There is no dependence on the private key of the address
|
||||
// therefore also external addresses are allowed to verify signatures.
|
||||
// Supported address types are P2PKH, P2WKH, NP2WKH, P2TR.
|
||||
func (w *WalletKit) VerifyMessageWithAddr(ctx context.Context,
|
||||
req *VerifyMessageWithAddrRequest) (*VerifyMessageWithAddrResponse,
|
||||
error) {
|
||||
|
||||
sig, err := base64.StdEncoding.DecodeString(req.Signature)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed base64 encoding of "+
|
||||
"the signature: %w", err)
|
||||
}
|
||||
|
||||
digest, err := doubleHashMessage(msgSignaturePrefix, string(req.Msg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pk, wasCompressed, err := ecdsa.RecoverCompact(sig, digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to recover public key "+
|
||||
"from compact signature: %w", err)
|
||||
}
|
||||
|
||||
var serializedPubkey []byte
|
||||
if wasCompressed {
|
||||
serializedPubkey = pk.SerializeCompressed()
|
||||
} else {
|
||||
serializedPubkey = pk.SerializeUncompressed()
|
||||
}
|
||||
|
||||
addr, err := btcutil.DecodeAddress(req.Addr, w.cfg.ChainParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode address: %w", err)
|
||||
}
|
||||
|
||||
if !addr.IsForNet(w.cfg.ChainParams) {
|
||||
return nil, fmt.Errorf("encoded address is for"+
|
||||
"the wrong network %s", req.Addr)
|
||||
}
|
||||
|
||||
var (
|
||||
address btcutil.Address
|
||||
pubKeyHash = btcutil.Hash160(serializedPubkey)
|
||||
)
|
||||
|
||||
// Ensure the address is one of the supported types.
|
||||
switch addr.(type) {
|
||||
case *btcutil.AddressPubKeyHash:
|
||||
address, err = btcutil.NewAddressPubKeyHash(
|
||||
pubKeyHash, w.cfg.ChainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case *btcutil.AddressWitnessPubKeyHash:
|
||||
address, err = btcutil.NewAddressWitnessPubKeyHash(
|
||||
pubKeyHash, w.cfg.ChainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case *btcutil.AddressScriptHash:
|
||||
// Check if address is a Nested P2WKH (NP2WKH).
|
||||
address, err = btcutil.NewAddressWitnessPubKeyHash(
|
||||
pubKeyHash, w.cfg.ChainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
witnessScript, err := txscript.PayToAddrScript(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
address, err = btcutil.NewAddressScriptHashFromHash(
|
||||
btcutil.Hash160(witnessScript), w.cfg.ChainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case *btcutil.AddressTaproot:
|
||||
// Only addresses without a tapscript are allowed because
|
||||
// the verification is using the internal key.
|
||||
tapKey := txscript.ComputeTaprootKeyNoScript(pk)
|
||||
address, err = btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(tapKey),
|
||||
w.cfg.ChainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported address type")
|
||||
}
|
||||
|
||||
return &VerifyMessageWithAddrResponse{
|
||||
Valid: req.Addr == address.EncodeAddress(),
|
||||
Pubkey: serializedPubkey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ImportAccount imports an account backed by an account extended public key.
|
||||
// The master key fingerprint denotes the fingerprint of the root key
|
||||
// corresponding to the account public key (also known as the key with
|
||||
|
@ -1,10 +1,13 @@
|
||||
package walletrpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
)
|
||||
|
||||
@ -74,3 +77,22 @@ func parseDerivationPath(path string) ([]uint32, error) {
|
||||
}
|
||||
return indices, nil
|
||||
}
|
||||
|
||||
// doubleHashMessage creates the double hash (sha256) of a message
|
||||
// prepended with a specified prefix.
|
||||
func doubleHashMessage(prefix string, msg string) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
err := wire.WriteVarString(&buf, 0, prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = wire.WriteVarString(&buf, 0, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
digest := chainhash.DoubleHashB(buf.Bytes())
|
||||
|
||||
return digest, nil
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user