From 2b121b7b40904946cf8cacc678ac22a657bee214 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Tue, 23 Dec 2025 10:02:49 -0600 Subject: [PATCH 1/4] switchrpc: update TrackOnion rpc error handling The new structure uses a top-level `oneof` to provide a compile-time distinction between a successful payment (preimage) and a failed one. Additional information on a failed attempt can be found in FailureDetails. We now also use a structured ForwardingFailure type for communicating the failure index and wire message from failures which occur during htlc forwarding downstream in the route. --- itest/lnd_sendonion_test.go | 47 +- lnrpc/switchrpc/switch.pb.go | 670 ++++++++++++++++++++++---- lnrpc/switchrpc/switch.proto | 78 ++- lnrpc/switchrpc/switch.swagger.json | 81 +++- lnrpc/switchrpc/switch_server.go | 262 +++++++--- lnrpc/switchrpc/switch_server_test.go | 312 ++++++++---- 6 files changed, 1128 insertions(+), 322 deletions(-) diff --git a/itest/lnd_sendonion_test.go b/itest/lnd_sendonion_test.go index 6fba408e98..449dcaf216 100644 --- a/itest/lnd_sendonion_test.go +++ b/itest/lnd_sendonion_test.go @@ -97,7 +97,7 @@ func testSendOnion(ht *lntest.HarnessTest) { HopPubkeys: onionResp.HopPubkeys, } trackResp := alice.RPC.TrackOnion(trackReq) - require.Equal(ht, invoices[0].RPreimage, trackResp.Preimage) + require.Equal(ht, invoices[0].RPreimage, trackResp.GetPreimage()) // The invoice should show as settled for Dave. ht.AssertInvoiceSettled(dave, invoices[0].PaymentAddr) @@ -207,7 +207,7 @@ func testSendOnionTwice(ht *lntest.HarnessTest) { HopPubkeys: onionResp.HopPubkeys, } trackResp := alice.RPC.TrackOnion(trackReq) - require.Equal(ht, preimage[:], trackResp.Preimage) + require.Equal(ht, preimage[:], trackResp.GetPreimage()) // Now that the original HTLC attempt has settled, we'll send the same // onion again with the same attempt ID. Confirm that this is also @@ -402,9 +402,6 @@ func testTrackOnion(ht *lntest.HarnessTest) { require.True(ht, resp.Success, "expected successful onion send") require.Empty(ht, resp.ErrorMessage, "unexpected failure to send onion") - serverErrorStr := "" - clientErrorStr := "" - // Track the payment providing all necessary information to delegate // error decryption to the server. We expect this to fail as Dave is not // expecting payment. @@ -415,10 +412,11 @@ func testTrackOnion(ht *lntest.HarnessTest) { HopPubkeys: onionResp.HopPubkeys, } trackResp := alice.RPC.TrackOnion(trackReq) - require.NotEmpty(ht, trackResp.ErrorMessage, - "expected onion tracking error") + serverFailure := trackResp.GetFailureDetails() + require.NotNil(ht, serverFailure, "expected onion tracking error") - serverErrorStr = trackResp.ErrorMessage + serverFwdFailure := serverFailure.GetForwardingFailure() + require.NotNil(ht, serverFwdFailure, "expected forwarding failure") // Now we'll track the same payment attempt, but we'll specify that // we want to handle the error decryption ourselves client side. @@ -427,16 +425,18 @@ func testTrackOnion(ht *lntest.HarnessTest) { PaymentHash: paymentHash, } trackResp = alice.RPC.TrackOnion(trackReq) - require.NotNil(ht, trackResp.EncryptedError, "expected encrypted error") + clientFailure := trackResp.GetFailureDetails() + require.NotNil(ht, clientFailure, "expected client tracking error") + + encryptedErrorBytes := clientFailure.GetEncryptedErrorData() + require.NotNil(ht, encryptedErrorBytes, "expected encrypted error") // Decrypt and inspect the error from the TrackOnion RPC response. sessionKey, _ := btcec.PrivKeyFromBytes(onionResp.SessionKey) var pubKeys []*btcec.PublicKey for _, keyBytes := range onionResp.HopPubkeys { pubKey, err := btcec.ParsePubKey(keyBytes) - if err != nil { - ht.Fatalf("Failed to parse public key: %v", err) - } + require.NoError(ht, err, "Failed to parse public key") pubKeys = append(pubKeys, pubKey) } @@ -450,14 +450,19 @@ func testTrackOnion(ht *lntest.HarnessTest) { } // Simulate an RPC client decrypting the onion error. - encryptedError := lnwire.OpaqueReason(trackResp.EncryptedError) - forwardingError, err := errorDecryptor.DecryptError(encryptedError) - require.Nil(ht, err, "unable to decrypt error") - - clientErrorStr = forwardingError.Error() + encryptedError := lnwire.OpaqueReason(encryptedErrorBytes) + clientFwdErr, err := errorDecryptor.DecryptError(encryptedError) + require.NoError(ht, err, "unable to decrypt error") + + // Finally, assert that the structured forwarding failure is the same + // whether it was decrypted on the server or on the client. + serverFwdErr, err := switchrpc.UnmarshallForwardingError( + serverFwdFailure, + ) + require.NoError(ht, err, "unable to decode server forwarding failure") - serverFwdErr, err := switchrpc.ParseForwardingError(serverErrorStr) - require.Nil(ht, err, "expected to parse forwarding error from server") - require.Equal(ht, serverFwdErr.Error(), clientErrorStr, "expect error "+ - "message to match whether handled by client or server") + require.Equal(ht, serverFwdFailure.FailureSourceIndex, + uint32(clientFwdErr.FailureSourceIdx), "source index mismatch") + require.Equal(ht, serverFwdErr.WireMessage(), + clientFwdErr.WireMessage(), "wire message mismatch") } diff --git a/lnrpc/switchrpc/switch.pb.go b/lnrpc/switchrpc/switch.pb.go index 7f65607580..042564a341 100644 --- a/lnrpc/switchrpc/switch.pb.go +++ b/lnrpc/switchrpc/switch.pb.go @@ -389,18 +389,14 @@ type TrackOnionResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The preimage obtained by making the payment. If this field is set, - // the payment succeeded. - Preimage []byte `protobuf:"bytes,1,opt,name=preimage,proto3" json:"preimage,omitempty"` - // In case of failure, this field will provide more information about the - // error. - ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` - // A code representing the type of error that occurred. This can be used - // to programmatically distinguish between different kinds of errors. - ErrorCode ErrorCode `protobuf:"varint,3,opt,name=error_code,json=errorCode,proto3,enum=switchrpc.ErrorCode" json:"error_code,omitempty"` - // If the caller provides no means to decrypt the error, then we'll defer - // error decryption on the server and return the encrypted error blob. - EncryptedError []byte `protobuf:"bytes,4,opt,name=encrypted_error,json=encryptedError,proto3" json:"encrypted_error,omitempty"` + // The final result of the payment attempt, which is either a preimage + // (success) or detailed failure information. + // + // Types that are assignable to Result: + // + // *TrackOnionResponse_Preimage + // *TrackOnionResponse_FailureDetails + Result isTrackOnionResponse_Result `protobuf_oneof:"result"` } func (x *TrackOnionResponse) Reset() { @@ -435,34 +431,383 @@ func (*TrackOnionResponse) Descriptor() ([]byte, []int) { return file_switchrpc_switch_proto_rawDescGZIP(), []int{3} } +func (m *TrackOnionResponse) GetResult() isTrackOnionResponse_Result { + if m != nil { + return m.Result + } + return nil +} + func (x *TrackOnionResponse) GetPreimage() []byte { - if x != nil { + if x, ok := x.GetResult().(*TrackOnionResponse_Preimage); ok { return x.Preimage } return nil } -func (x *TrackOnionResponse) GetErrorMessage() string { +func (x *TrackOnionResponse) GetFailureDetails() *FailureDetails { + if x, ok := x.GetResult().(*TrackOnionResponse_FailureDetails); ok { + return x.FailureDetails + } + return nil +} + +type isTrackOnionResponse_Result interface { + isTrackOnionResponse_Result() +} + +type TrackOnionResponse_Preimage struct { + // The preimage obtained by making the payment. If this field is set, + // the payment succeeded. + Preimage []byte `protobuf:"bytes,1,opt,name=preimage,proto3,oneof"` +} + +type TrackOnionResponse_FailureDetails struct { + // The application-level failure for the payment attempt. If this field + // is set, the payment attempt failed. + FailureDetails *FailureDetails `protobuf:"bytes,2,opt,name=failure_details,json=failureDetails,proto3,oneof"` +} + +func (*TrackOnionResponse_Preimage) isTrackOnionResponse_Result() {} + +func (*TrackOnionResponse_FailureDetails) isTrackOnionResponse_Result() {} + +// FailureDetails provides structured information about why a payment attempt +// failed on the network. This message is included in a successful +// TrackOnionResponse when the queried attempt ultimately failed. +type FailureDetails struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // A human-readable error_message for logging or display. + ErrorMessage string `protobuf:"bytes,1,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + // The failure contains specific, structured details about the error. + // + // Types that are assignable to Failure: + // + // *FailureDetails_ClearTextFailure + // *FailureDetails_ForwardingFailure + // *FailureDetails_EncryptedErrorData + // *FailureDetails_UnreadableFailure + // *FailureDetails_InternalError + Failure isFailureDetails_Failure `protobuf_oneof:"failure"` +} + +func (x *FailureDetails) Reset() { + *x = FailureDetails{} + if protoimpl.UnsafeEnabled { + mi := &file_switchrpc_switch_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FailureDetails) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FailureDetails) ProtoMessage() {} + +func (x *FailureDetails) ProtoReflect() protoreflect.Message { + mi := &file_switchrpc_switch_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FailureDetails.ProtoReflect.Descriptor instead. +func (*FailureDetails) Descriptor() ([]byte, []int) { + return file_switchrpc_switch_proto_rawDescGZIP(), []int{4} +} + +func (x *FailureDetails) GetErrorMessage() string { if x != nil { return x.ErrorMessage } return "" } -func (x *TrackOnionResponse) GetErrorCode() ErrorCode { +func (m *FailureDetails) GetFailure() isFailureDetails_Failure { + if m != nil { + return m.Failure + } + return nil +} + +func (x *FailureDetails) GetClearTextFailure() *ClearTextFailure { + if x, ok := x.GetFailure().(*FailureDetails_ClearTextFailure); ok { + return x.ClearTextFailure + } + return nil +} + +func (x *FailureDetails) GetForwardingFailure() *ForwardingFailure { + if x, ok := x.GetFailure().(*FailureDetails_ForwardingFailure); ok { + return x.ForwardingFailure + } + return nil +} + +func (x *FailureDetails) GetEncryptedErrorData() []byte { + if x, ok := x.GetFailure().(*FailureDetails_EncryptedErrorData); ok { + return x.EncryptedErrorData + } + return nil +} + +func (x *FailureDetails) GetUnreadableFailure() *UnreadableFailure { + if x, ok := x.GetFailure().(*FailureDetails_UnreadableFailure); ok { + return x.UnreadableFailure + } + return nil +} + +func (x *FailureDetails) GetInternalError() *InternalError { + if x, ok := x.GetFailure().(*FailureDetails_InternalError); ok { + return x.InternalError + } + return nil +} + +type isFailureDetails_Failure interface { + isFailureDetails_Failure() +} + +type FailureDetails_ClearTextFailure struct { + // clear_text_failure is included when the failure originates locally + // (at our node) or is a fully decrypted, known failure from an upstream + // node. It contains the raw lnwire.FailureMessage. + ClearTextFailure *ClearTextFailure `protobuf:"bytes,2,opt,name=clear_text_failure,json=clearTextFailure,proto3,oneof"` +} + +type FailureDetails_ForwardingFailure struct { + // forwarding_failure is included when the HTLC fails at a remote node + // in the payment path and includes the index of the failing node. + ForwardingFailure *ForwardingFailure `protobuf:"bytes,3,opt,name=forwarding_failure,json=forwardingFailure,proto3,oneof"` +} + +type FailureDetails_EncryptedErrorData struct { + // encrypted_error_data is returned when the server could not decrypt + // the onion error blob, deferring decryption to the client. + EncryptedErrorData []byte `protobuf:"bytes,4,opt,name=encrypted_error_data,json=encryptedErrorData,proto3,oneof"` +} + +type FailureDetails_UnreadableFailure struct { + // The failure message from a remote peer could not be decrypted. + UnreadableFailure *UnreadableFailure `protobuf:"bytes,5,opt,name=unreadable_failure,json=unreadableFailure,proto3,oneof"` +} + +type FailureDetails_InternalError struct { + // An internal error occurred. + InternalError *InternalError `protobuf:"bytes,6,opt,name=internal_error,json=internalError,proto3,oneof"` +} + +func (*FailureDetails_ClearTextFailure) isFailureDetails_Failure() {} + +func (*FailureDetails_ForwardingFailure) isFailureDetails_Failure() {} + +func (*FailureDetails_EncryptedErrorData) isFailureDetails_Failure() {} + +func (*FailureDetails_UnreadableFailure) isFailureDetails_Failure() {} + +func (*FailureDetails_InternalError) isFailureDetails_Failure() {} + +// ForwardingFailure represents an HTLC failure that occurred at a specific +// hop within the payment route. +type ForwardingFailure struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // failure_source_index is the 0-based index of the node within the payment + // route that reported the failure. Index 0 refers to the local node. + FailureSourceIndex uint32 `protobuf:"varint,1,opt,name=failure_source_index,json=failureSourceIndex,proto3" json:"failure_source_index,omitempty"` + // The raw, serialized `lnwire.FailureMessage` reported by the failing node. + WireMessage []byte `protobuf:"bytes,2,opt,name=wire_message,json=wireMessage,proto3" json:"wire_message,omitempty"` +} + +func (x *ForwardingFailure) Reset() { + *x = ForwardingFailure{} + if protoimpl.UnsafeEnabled { + mi := &file_switchrpc_switch_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ForwardingFailure) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ForwardingFailure) ProtoMessage() {} + +func (x *ForwardingFailure) ProtoReflect() protoreflect.Message { + mi := &file_switchrpc_switch_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ForwardingFailure.ProtoReflect.Descriptor instead. +func (*ForwardingFailure) Descriptor() ([]byte, []int) { + return file_switchrpc_switch_proto_rawDescGZIP(), []int{5} +} + +func (x *ForwardingFailure) GetFailureSourceIndex() uint32 { if x != nil { - return x.ErrorCode + return x.FailureSourceIndex } - return ErrorCode_UNSPECIFIED + return 0 } -func (x *TrackOnionResponse) GetEncryptedError() []byte { +func (x *ForwardingFailure) GetWireMessage() []byte { if x != nil { - return x.EncryptedError + return x.WireMessage } return nil } +// ClearTextFailure is included when the failure originates locally or is a +// fully decrypted, known failure from an upstream node. It contains the raw +// lnwire.FailureMessage. +type ClearTextFailure struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The raw, serialized `lnwire.FailureMessage`. + WireMessage []byte `protobuf:"bytes,1,opt,name=wire_message,json=wireMessage,proto3" json:"wire_message,omitempty"` +} + +func (x *ClearTextFailure) Reset() { + *x = ClearTextFailure{} + if protoimpl.UnsafeEnabled { + mi := &file_switchrpc_switch_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClearTextFailure) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClearTextFailure) ProtoMessage() {} + +func (x *ClearTextFailure) ProtoReflect() protoreflect.Message { + mi := &file_switchrpc_switch_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClearTextFailure.ProtoReflect.Descriptor instead. +func (*ClearTextFailure) Descriptor() ([]byte, []int) { + return file_switchrpc_switch_proto_rawDescGZIP(), []int{6} +} + +func (x *ClearTextFailure) GetWireMessage() []byte { + if x != nil { + return x.WireMessage + } + return nil +} + +// InternalError indicates that an unexpected internal error occurred. +type InternalError struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *InternalError) Reset() { + *x = InternalError{} + if protoimpl.UnsafeEnabled { + mi := &file_switchrpc_switch_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InternalError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InternalError) ProtoMessage() {} + +func (x *InternalError) ProtoReflect() protoreflect.Message { + mi := &file_switchrpc_switch_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InternalError.ProtoReflect.Descriptor instead. +func (*InternalError) Descriptor() ([]byte, []int) { + return file_switchrpc_switch_proto_rawDescGZIP(), []int{7} +} + +// UnreadableFailure indicates that the failure message from the network was +// malformed or otherwise unreadable. +type UnreadableFailure struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *UnreadableFailure) Reset() { + *x = UnreadableFailure{} + if protoimpl.UnsafeEnabled { + mi := &file_switchrpc_switch_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UnreadableFailure) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnreadableFailure) ProtoMessage() {} + +func (x *UnreadableFailure) ProtoReflect() protoreflect.Message { + mi := &file_switchrpc_switch_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnreadableFailure.ProtoReflect.Descriptor instead. +func (*UnreadableFailure) Descriptor() ([]byte, []int) { + return file_switchrpc_switch_proto_rawDescGZIP(), []int{8} +} + // BuildOnionRequest includes the necessary information to construct a Sphinx // onion packet. type BuildOnionRequest struct { @@ -482,7 +827,7 @@ type BuildOnionRequest struct { func (x *BuildOnionRequest) Reset() { *x = BuildOnionRequest{} if protoimpl.UnsafeEnabled { - mi := &file_switchrpc_switch_proto_msgTypes[4] + mi := &file_switchrpc_switch_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -495,7 +840,7 @@ func (x *BuildOnionRequest) String() string { func (*BuildOnionRequest) ProtoMessage() {} func (x *BuildOnionRequest) ProtoReflect() protoreflect.Message { - mi := &file_switchrpc_switch_proto_msgTypes[4] + mi := &file_switchrpc_switch_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -508,7 +853,7 @@ func (x *BuildOnionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BuildOnionRequest.ProtoReflect.Descriptor instead. func (*BuildOnionRequest) Descriptor() ([]byte, []int) { - return file_switchrpc_switch_proto_rawDescGZIP(), []int{4} + return file_switchrpc_switch_proto_rawDescGZIP(), []int{9} } func (x *BuildOnionRequest) GetRoute() *lnrpc.Route { @@ -550,7 +895,7 @@ type BuildOnionResponse struct { func (x *BuildOnionResponse) Reset() { *x = BuildOnionResponse{} if protoimpl.UnsafeEnabled { - mi := &file_switchrpc_switch_proto_msgTypes[5] + mi := &file_switchrpc_switch_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -563,7 +908,7 @@ func (x *BuildOnionResponse) String() string { func (*BuildOnionResponse) ProtoMessage() {} func (x *BuildOnionResponse) ProtoReflect() protoreflect.Message { - mi := &file_switchrpc_switch_proto_msgTypes[5] + mi := &file_switchrpc_switch_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -576,7 +921,7 @@ func (x *BuildOnionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BuildOnionResponse.ProtoReflect.Descriptor instead. func (*BuildOnionResponse) Descriptor() ([]byte, []int) { - return file_switchrpc_switch_proto_rawDescGZIP(), []int{5} + return file_switchrpc_switch_proto_rawDescGZIP(), []int{10} } func (x *BuildOnionResponse) GetOnionBlob() []byte { @@ -658,66 +1003,101 @@ var file_switchrpc_switch_proto_rawDesc = []byte{ 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x6f, 0x70, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x6f, 0x70, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x73, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0xb3, 0x01, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, + 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x82, 0x01, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, + 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, - 0x33, 0x0a, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, - 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x43, 0x6f, 0x64, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x65, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x90, 0x01, - 0x0a, 0x11, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x05, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x52, 0x05, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, - 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x24, 0x0a, 0x0b, 0x73, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, - 0x00, 0x52, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, - 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, - 0x22, 0x75, 0x0a, 0x12, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x6e, 0x69, 0x6f, 0x6e, 0x5f, - 0x62, 0x6c, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x6f, 0x6e, 0x69, 0x6f, - 0x6e, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x6f, 0x70, 0x5f, 0x70, 0x75, - 0x62, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x6f, 0x70, - 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, 0x2a, 0xc5, 0x01, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, - 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, - 0x54, 0x5f, 0x49, 0x44, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x01, - 0x12, 0x14, 0x0a, 0x10, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x45, - 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x4c, 0x45, 0x41, 0x52, 0x5f, - 0x54, 0x45, 0x58, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, - 0x55, 0x4e, 0x52, 0x45, 0x41, 0x44, 0x41, 0x42, 0x4c, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, - 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x10, 0x04, 0x12, 0x12, 0x0a, 0x0e, - 0x44, 0x55, 0x50, 0x4c, 0x49, 0x43, 0x41, 0x54, 0x45, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x10, 0x05, - 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x4f, 0x5f, 0x4c, 0x49, 0x4e, 0x4b, 0x10, 0x06, 0x12, 0x12, 0x0a, - 0x0e, 0x53, 0x57, 0x49, 0x54, 0x43, 0x48, 0x5f, 0x45, 0x58, 0x49, 0x54, 0x49, 0x4e, 0x47, 0x10, - 0x07, 0x12, 0x0c, 0x0a, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x08, 0x32, - 0xe6, 0x01, 0x0a, 0x06, 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x12, 0x46, 0x0a, 0x09, 0x53, 0x65, - 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, - 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, - 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, - 0x12, 0x1c, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, - 0x63, 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, - 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, + 0x48, 0x00, 0x52, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x44, 0x0a, 0x0f, + 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, + 0x63, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, + 0x48, 0x00, 0x52, 0x0e, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, + 0x6c, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0xa2, 0x03, 0x0a, + 0x0e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, + 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x12, 0x4b, 0x0a, 0x12, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x5f, 0x74, 0x65, + 0x78, 0x74, 0x5f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x65, + 0x61, 0x72, 0x54, 0x65, 0x78, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x48, 0x00, 0x52, + 0x10, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x54, 0x65, 0x78, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, + 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, + 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, + 0x64, 0x69, 0x6e, 0x67, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x48, 0x00, 0x52, 0x11, 0x66, + 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, + 0x12, 0x32, 0x0a, 0x14, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, + 0x52, 0x12, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x44, 0x61, 0x74, 0x61, 0x12, 0x4d, 0x0a, 0x12, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x61, 0x62, + 0x6c, 0x65, 0x5f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x6e, 0x72, + 0x65, 0x61, 0x64, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x48, 0x00, + 0x52, 0x11, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x69, 0x6c, + 0x75, 0x72, 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x73, 0x77, + 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x42, 0x09, 0x0a, 0x07, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, + 0x65, 0x22, 0x68, 0x0a, 0x11, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x46, + 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, 0x30, 0x0a, 0x14, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, + 0x65, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x12, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x69, 0x72, 0x65, + 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, + 0x77, 0x69, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x35, 0x0a, 0x10, 0x43, + 0x6c, 0x65, 0x61, 0x72, 0x54, 0x65, 0x78, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, + 0x21, 0x0a, 0x0c, 0x77, 0x69, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x77, 0x69, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x22, 0x13, 0x0a, 0x11, 0x55, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x61, 0x62, 0x6c, + 0x65, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x22, 0x90, 0x01, 0x0a, 0x11, 0x42, 0x75, 0x69, + 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, + 0x0a, 0x05, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, + 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x05, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, + 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x24, 0x0a, 0x0b, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x0a, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x75, 0x0a, 0x12, 0x42, + 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x6e, 0x69, 0x6f, 0x6e, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x6f, 0x6e, 0x69, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, 0x62, + 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, + 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x6f, 0x70, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x6f, 0x70, 0x50, 0x75, 0x62, 0x6b, 0x65, + 0x79, 0x73, 0x2a, 0xc5, 0x01, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, + 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x49, 0x44, 0x5f, + 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10, 0x46, + 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, + 0x02, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x4c, 0x45, 0x41, 0x52, 0x5f, 0x54, 0x45, 0x58, 0x54, 0x5f, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x55, 0x4e, 0x52, 0x45, 0x41, + 0x44, 0x41, 0x42, 0x4c, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x4d, 0x45, + 0x53, 0x53, 0x41, 0x47, 0x45, 0x10, 0x04, 0x12, 0x12, 0x0a, 0x0e, 0x44, 0x55, 0x50, 0x4c, 0x49, + 0x43, 0x41, 0x54, 0x45, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x10, 0x05, 0x12, 0x0b, 0x0a, 0x07, 0x4e, + 0x4f, 0x5f, 0x4c, 0x49, 0x4e, 0x4b, 0x10, 0x06, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x57, 0x49, 0x54, + 0x43, 0x48, 0x5f, 0x45, 0x58, 0x49, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x0c, 0x0a, 0x08, + 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x08, 0x32, 0xe6, 0x01, 0x0a, 0x06, 0x53, + 0x77, 0x69, 0x74, 0x63, 0x68, 0x12, 0x46, 0x0a, 0x09, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, + 0x6f, 0x6e, 0x12, 0x1b, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x53, + 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, - 0x0a, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x73, 0x77, - 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, 0x69, + 0x0a, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x73, 0x77, + 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x73, 0x77, 0x69, 0x74, - 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, - 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, - 0x63, 0x2f, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x42, 0x75, 0x69, 0x6c, + 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, + 0x70, 0x63, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, + 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x77, 0x69, + 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -733,34 +1113,43 @@ func file_switchrpc_switch_proto_rawDescGZIP() []byte { } var file_switchrpc_switch_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_switchrpc_switch_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_switchrpc_switch_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_switchrpc_switch_proto_goTypes = []interface{}{ (ErrorCode)(0), // 0: switchrpc.ErrorCode (*SendOnionRequest)(nil), // 1: switchrpc.SendOnionRequest (*SendOnionResponse)(nil), // 2: switchrpc.SendOnionResponse (*TrackOnionRequest)(nil), // 3: switchrpc.TrackOnionRequest (*TrackOnionResponse)(nil), // 4: switchrpc.TrackOnionResponse - (*BuildOnionRequest)(nil), // 5: switchrpc.BuildOnionRequest - (*BuildOnionResponse)(nil), // 6: switchrpc.BuildOnionResponse - nil, // 7: switchrpc.SendOnionRequest.CustomRecordsEntry - (*lnrpc.Route)(nil), // 8: lnrpc.Route + (*FailureDetails)(nil), // 5: switchrpc.FailureDetails + (*ForwardingFailure)(nil), // 6: switchrpc.ForwardingFailure + (*ClearTextFailure)(nil), // 7: switchrpc.ClearTextFailure + (*InternalError)(nil), // 8: switchrpc.InternalError + (*UnreadableFailure)(nil), // 9: switchrpc.UnreadableFailure + (*BuildOnionRequest)(nil), // 10: switchrpc.BuildOnionRequest + (*BuildOnionResponse)(nil), // 11: switchrpc.BuildOnionResponse + nil, // 12: switchrpc.SendOnionRequest.CustomRecordsEntry + (*lnrpc.Route)(nil), // 13: lnrpc.Route } var file_switchrpc_switch_proto_depIdxs = []int32{ - 7, // 0: switchrpc.SendOnionRequest.custom_records:type_name -> switchrpc.SendOnionRequest.CustomRecordsEntry - 0, // 1: switchrpc.SendOnionResponse.error_code:type_name -> switchrpc.ErrorCode - 0, // 2: switchrpc.TrackOnionResponse.error_code:type_name -> switchrpc.ErrorCode - 8, // 3: switchrpc.BuildOnionRequest.route:type_name -> lnrpc.Route - 1, // 4: switchrpc.Switch.SendOnion:input_type -> switchrpc.SendOnionRequest - 3, // 5: switchrpc.Switch.TrackOnion:input_type -> switchrpc.TrackOnionRequest - 5, // 6: switchrpc.Switch.BuildOnion:input_type -> switchrpc.BuildOnionRequest - 2, // 7: switchrpc.Switch.SendOnion:output_type -> switchrpc.SendOnionResponse - 4, // 8: switchrpc.Switch.TrackOnion:output_type -> switchrpc.TrackOnionResponse - 6, // 9: switchrpc.Switch.BuildOnion:output_type -> switchrpc.BuildOnionResponse - 7, // [7:10] is the sub-list for method output_type - 4, // [4:7] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 12, // 0: switchrpc.SendOnionRequest.custom_records:type_name -> switchrpc.SendOnionRequest.CustomRecordsEntry + 0, // 1: switchrpc.SendOnionResponse.error_code:type_name -> switchrpc.ErrorCode + 5, // 2: switchrpc.TrackOnionResponse.failure_details:type_name -> switchrpc.FailureDetails + 7, // 3: switchrpc.FailureDetails.clear_text_failure:type_name -> switchrpc.ClearTextFailure + 6, // 4: switchrpc.FailureDetails.forwarding_failure:type_name -> switchrpc.ForwardingFailure + 9, // 5: switchrpc.FailureDetails.unreadable_failure:type_name -> switchrpc.UnreadableFailure + 8, // 6: switchrpc.FailureDetails.internal_error:type_name -> switchrpc.InternalError + 13, // 7: switchrpc.BuildOnionRequest.route:type_name -> lnrpc.Route + 1, // 8: switchrpc.Switch.SendOnion:input_type -> switchrpc.SendOnionRequest + 3, // 9: switchrpc.Switch.TrackOnion:input_type -> switchrpc.TrackOnionRequest + 10, // 10: switchrpc.Switch.BuildOnion:input_type -> switchrpc.BuildOnionRequest + 2, // 11: switchrpc.Switch.SendOnion:output_type -> switchrpc.SendOnionResponse + 4, // 12: switchrpc.Switch.TrackOnion:output_type -> switchrpc.TrackOnionResponse + 11, // 13: switchrpc.Switch.BuildOnion:output_type -> switchrpc.BuildOnionResponse + 11, // [11:14] is the sub-list for method output_type + 8, // [8:11] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_switchrpc_switch_proto_init() } @@ -818,7 +1207,7 @@ func file_switchrpc_switch_proto_init() { } } file_switchrpc_switch_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BuildOnionRequest); i { + switch v := v.(*FailureDetails); i { case 0: return &v.state case 1: @@ -830,6 +1219,66 @@ func file_switchrpc_switch_proto_init() { } } file_switchrpc_switch_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ForwardingFailure); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_switchrpc_switch_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClearTextFailure); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_switchrpc_switch_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InternalError); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_switchrpc_switch_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UnreadableFailure); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_switchrpc_switch_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BuildOnionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_switchrpc_switch_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BuildOnionResponse); i { case 0: return &v.state @@ -844,14 +1293,25 @@ func file_switchrpc_switch_proto_init() { } file_switchrpc_switch_proto_msgTypes[0].OneofWrappers = []interface{}{} file_switchrpc_switch_proto_msgTypes[2].OneofWrappers = []interface{}{} - file_switchrpc_switch_proto_msgTypes[4].OneofWrappers = []interface{}{} + file_switchrpc_switch_proto_msgTypes[3].OneofWrappers = []interface{}{ + (*TrackOnionResponse_Preimage)(nil), + (*TrackOnionResponse_FailureDetails)(nil), + } + file_switchrpc_switch_proto_msgTypes[4].OneofWrappers = []interface{}{ + (*FailureDetails_ClearTextFailure)(nil), + (*FailureDetails_ForwardingFailure)(nil), + (*FailureDetails_EncryptedErrorData)(nil), + (*FailureDetails_UnreadableFailure)(nil), + (*FailureDetails_InternalError)(nil), + } + file_switchrpc_switch_proto_msgTypes[9].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_switchrpc_switch_proto_rawDesc, NumEnums: 1, - NumMessages: 7, + NumMessages: 12, NumExtensions: 0, NumServices: 1, }, diff --git a/lnrpc/switchrpc/switch.proto b/lnrpc/switchrpc/switch.proto index bfff1c0c0a..c1ae20224e 100644 --- a/lnrpc/switchrpc/switch.proto +++ b/lnrpc/switchrpc/switch.proto @@ -185,21 +185,75 @@ message TrackOnionRequest { } message TrackOnionResponse { - // The preimage obtained by making the payment. If this field is set, - // the payment succeeded. - bytes preimage = 1; + // The final result of the payment attempt, which is either a preimage + // (success) or detailed failure information. + oneof result { + // The preimage obtained by making the payment. If this field is set, + // the payment succeeded. + bytes preimage = 1; + + // The application-level failure for the payment attempt. If this field + // is set, the payment attempt failed. + FailureDetails failure_details = 2; + } +} - // In case of failure, this field will provide more information about the - // error. - string error_message = 2; +// FailureDetails provides structured information about why a payment attempt +// failed on the network. This message is included in a successful +// TrackOnionResponse when the queried attempt ultimately failed. +message FailureDetails { + // A human-readable error_message for logging or display. + string error_message = 1; + + // The failure contains specific, structured details about the error. + oneof failure { + // clear_text_failure is included when the failure originates locally + // (at our node) or is a fully decrypted, known failure from an upstream + // node. It contains the raw lnwire.FailureMessage. + ClearTextFailure clear_text_failure = 2; + + // forwarding_failure is included when the HTLC fails at a remote node + // in the payment path and includes the index of the failing node. + ForwardingFailure forwarding_failure = 3; + + // encrypted_error_data is returned when the server could not decrypt + // the onion error blob, deferring decryption to the client. + bytes encrypted_error_data = 4; + + // The failure message from a remote peer could not be decrypted. + UnreadableFailure unreadable_failure = 5; + + // An internal error occurred. + InternalError internal_error = 6; + } +} - // A code representing the type of error that occurred. This can be used - // to programmatically distinguish between different kinds of errors. - ErrorCode error_code = 3; +// ForwardingFailure represents an HTLC failure that occurred at a specific +// hop within the payment route. +message ForwardingFailure { + // failure_source_index is the 0-based index of the node within the payment + // route that reported the failure. Index 0 refers to the local node. + uint32 failure_source_index = 1; + + // The raw, serialized `lnwire.FailureMessage` reported by the failing node. + bytes wire_message = 2; +} + +// ClearTextFailure is included when the failure originates locally or is a +// fully decrypted, known failure from an upstream node. It contains the raw +// lnwire.FailureMessage. +message ClearTextFailure { + // The raw, serialized `lnwire.FailureMessage`. + bytes wire_message = 1; +} + +// InternalError indicates that an unexpected internal error occurred. +message InternalError { +} - // If the caller provides no means to decrypt the error, then we'll defer - // error decryption on the server and return the encrypted error blob. - bytes encrypted_error = 4; +// UnreadableFailure indicates that the failure message from the network was +// malformed or otherwise unreadable. +message UnreadableFailure { } // BuildOnionRequest includes the necessary information to construct a Sphinx diff --git a/lnrpc/switchrpc/switch.swagger.json b/lnrpc/switchrpc/switch.swagger.json index 1c4523e9c7..58058f44c5 100644 --- a/lnrpc/switchrpc/switch.swagger.json +++ b/lnrpc/switchrpc/switch.swagger.json @@ -351,6 +351,17 @@ }, "description": "BuildOnionResponse contains the constructed onion packet." }, + "switchrpcClearTextFailure": { + "type": "object", + "properties": { + "wire_message": { + "type": "string", + "format": "byte", + "description": "The raw, serialized `lnwire.FailureMessage`." + } + }, + "description": "ClearTextFailure is included when the failure originates locally or is a\nfully decrypted, known failure from an upstream node. It contains the raw\nlnwire.FailureMessage." + }, "switchrpcErrorCode": { "type": "string", "enum": [ @@ -367,6 +378,57 @@ "default": "UNSPECIFIED", "description": " - UNSPECIFIED: Default value for unset errors.\n - PAYMENT_ID_NOT_FOUND: Payment ID was not found.\n - FORWARDING_ERROR: Error occurred during forwarding.\n - CLEAR_TEXT_ERROR: Clear text error.\n - UNREADABLE_FAILURE_MESSAGE: Failure message could not be read.\n - DUPLICATE_HTLC: An HTLC with same ID is already in flight.\n - NO_LINK: No link available for payment.\n - SWITCH_EXITING: HTLC switch is exiting.\n - INTERNAL: Opaque internal server error." }, + "switchrpcFailureDetails": { + "type": "object", + "properties": { + "error_message": { + "type": "string", + "description": "A human-readable error_message for logging or display." + }, + "clear_text_failure": { + "$ref": "#/definitions/switchrpcClearTextFailure", + "description": "clear_text_failure is included when the failure originates locally\n(at our node) or is a fully decrypted, known failure from an upstream\nnode. It contains the raw lnwire.FailureMessage." + }, + "forwarding_failure": { + "$ref": "#/definitions/switchrpcForwardingFailure", + "description": "forwarding_failure is included when the HTLC fails at a remote node\nin the payment path and includes the index of the failing node." + }, + "encrypted_error_data": { + "type": "string", + "format": "byte", + "description": "encrypted_error_data is returned when the server could not decrypt\nthe onion error blob, deferring decryption to the client." + }, + "unreadable_failure": { + "$ref": "#/definitions/switchrpcUnreadableFailure", + "description": "The failure message from a remote peer could not be decrypted." + }, + "internal_error": { + "$ref": "#/definitions/switchrpcInternalError", + "description": "An internal error occurred." + } + }, + "description": "FailureDetails provides structured information about why a payment attempt\nfailed on the network. This message is included in a successful\nTrackOnionResponse when the queried attempt ultimately failed." + }, + "switchrpcForwardingFailure": { + "type": "object", + "properties": { + "failure_source_index": { + "type": "integer", + "format": "int64", + "description": "failure_source_index is the 0-based index of the node within the payment\nroute that reported the failure. Index 0 refers to the local node." + }, + "wire_message": { + "type": "string", + "format": "byte", + "description": "The raw, serialized `lnwire.FailureMessage` reported by the failing node." + } + }, + "description": "ForwardingFailure represents an HTLC failure that occurred at a specific\nhop within the payment route." + }, + "switchrpcInternalError": { + "type": "object", + "description": "InternalError indicates that an unexpected internal error occurred." + }, "switchrpcSendOnionRequest": { "type": "object", "properties": { @@ -478,20 +540,15 @@ "format": "byte", "description": "The preimage obtained by making the payment. If this field is set,\nthe payment succeeded." }, - "error_message": { - "type": "string", - "description": "In case of failure, this field will provide more information about the\nerror." - }, - "error_code": { - "$ref": "#/definitions/switchrpcErrorCode", - "description": "A code representing the type of error that occurred. This can be used\nto programmatically distinguish between different kinds of errors." - }, - "encrypted_error": { - "type": "string", - "format": "byte", - "description": "If the caller provides no means to decrypt the error, then we'll defer\nerror decryption on the server and return the encrypted error blob." + "failure_details": { + "$ref": "#/definitions/switchrpcFailureDetails", + "description": "The application-level failure for the payment attempt. If this field\nis set, the payment attempt failed." } } + }, + "switchrpcUnreadableFailure": { + "type": "object", + "description": "UnreadableFailure indicates that the failure message from the network was\nmalformed or otherwise unreadable." } } } diff --git a/lnrpc/switchrpc/switch_server.go b/lnrpc/switchrpc/switch_server.go index b72506da31..a42b167d75 100644 --- a/lnrpc/switchrpc/switch_server.go +++ b/lnrpc/switchrpc/switch_server.go @@ -12,8 +12,6 @@ import ( "math/big" "os" "path/filepath" - "strconv" - "strings" "github.com/btcsuite/btcd/btcec/v2" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" @@ -600,15 +598,19 @@ func (s *Server) TrackOnion(ctx context.Context, req.AttemptId, hash, errorDecryptor, ) if err != nil { - message, code := translateErrorForRPC(err) - log.Errorf("GetAttemptResult failed for attempt_id=%d of "+ - " payment=%x: %v", req.AttemptId, hash, message) + " payment=%x: %v", req.AttemptId, hash, err) - return &TrackOnionResponse{ - ErrorCode: code, - ErrorMessage: message, - }, nil + // If the payment ID is not found, we return a NotFound error. + if errors.Is(err, htlcswitch.ErrPaymentIDNotFound) { + return nil, status.Errorf(codes.NotFound, + "payment with attempt ID %d not found", + req.AttemptId) + } + + // For other errors, we return an internal error. + return nil, status.Errorf(codes.Internal, + "GetAttemptResult failed: %v", err) } // The switch knows about this payment, we'll wait for a result to be @@ -621,12 +623,10 @@ func (s *Server) TrackOnion(ctx context.Context, select { case result, ok = <-resultChan: if !ok { - // This channel is closed when the Switch shuts down. - return &TrackOnionResponse{ - ErrorCode: ErrorCode_SWITCH_EXITING, - ErrorMessage: htlcswitch.ErrSwitchExiting. - Error(), - }, nil + // This channel is closed when the Switch shuts down. We + // return a gRPC error to the client. + return nil, status.Error(codes.Unavailable, + htlcswitch.ErrSwitchExiting.Error()) } case <-ctx.Done(): @@ -635,44 +635,49 @@ func (s *Server) TrackOnion(ctx context.Context, return nil, status.FromContextError(ctx.Err()).Err() } - // The attempt result arrived so the HTLC is no longer in-flight. + // The attempt result arrived so the HTLC is no longer in-flight. If + // the payment failed, we build a structured response for the client. if result.Error != nil { - message, code := translateErrorForRPC(result.Error) - log.Errorf("Payment via onion failed for payment=%v: %v", - hash, message) + hash, result.Error) - return &TrackOnionResponse{ - ErrorCode: code, - ErrorMessage: message, - }, nil + details := marshallFailureDetails(result.Error) + + return newTrackOnionFailureResponse(details), nil } + // If the server was unable to decrypt the error, it will be returned + // as an encrypted byte slice. We populate the response accordingly. if len(result.EncryptedError) > 0 { - log.Errorf("Payment via onion failed for payment=%v", hash) + log.Errorf("Payment via onion failed for payment=%v with "+ + "encrypted error", hash) - return &TrackOnionResponse{ - EncryptedError: result.EncryptedError, - }, nil + details := &FailureDetails{ + Failure: &FailureDetails_EncryptedErrorData{ + EncryptedErrorData: result.EncryptedError, + }, + } + + return newTrackOnionFailureResponse(details), nil } // If we have reached this point, we expect a valid preimage for a // successful payment. if result.Preimage == (lntypes.Preimage{}) { - log.Errorf("Payment %v completed without a valid preimage or "+ - "error", hash) + log.Criticalf("Payment %v completed without a valid preimage "+ + "or error", hash) - return &TrackOnionResponse{ - ErrorCode: ErrorCode_INTERNAL, - ErrorMessage: ErrAmbiguousPaymentState.Error(), - }, nil + return nil, status.Error(codes.Internal, + ErrAmbiguousPaymentState.Error()) } log.Debugf("Received preimage via onion attempt_id=%d for payment=%v", req.AttemptId, hash) return &TrackOnionResponse{ - Preimage: result.Preimage[:], + Result: &TrackOnionResponse_Preimage{ + Preimage: result.Preimage[:], + }, }, nil } @@ -827,7 +832,6 @@ func (s *Server) BuildOnion(_ context.Context, func translateErrorForRPC(err error) (string, ErrorCode) { var ( clearTextErr htlcswitch.ClearTextError - fwdErr *htlcswitch.ForwardingError ) switch { @@ -845,20 +849,6 @@ func translateErrorForRPC(err error) (string, ErrorCode) { return err.Error(), ErrorCode_SWITCH_EXITING case errors.As(err, &clearTextErr): - // If this is a forwarding error, we'll handle it specially. - if errors.As(err, &fwdErr) { - encodedError, encodeErr := encodeForwardingError(fwdErr) - if encodeErr != nil { - return fmt.Sprintf("failed to encode wire "+ - "message: %v", encodeErr), - ErrorCode_INTERNAL - } - - return encodedError, - ErrorCode_FORWARDING_ERROR - } - - // Otherwise, we'll just encode the clear text error. var buf bytes.Buffer encodeErr := lnwire.EncodeFailure( &buf, clearTextErr.WireMessage(), 0, @@ -877,48 +867,162 @@ func translateErrorForRPC(err error) (string, ErrorCode) { } } -// encodeForwardingError converts a forwarding error from the switch to the -// format we can package for delivery to SendOnion rpc clients. We preserve the -// failure message from the wire as well as the index along the route where the -// failure occurred. -func encodeForwardingError(e *htlcswitch.ForwardingError) (string, error) { - var buf bytes.Buffer - err := lnwire.EncodeFailure(&buf, e.WireMessage(), 0) - if err != nil { - return "", fmt.Errorf("failed to encode wire message: %w", err) +// newTrackOnionFailureResponse is a helper function that wraps a +// PaymentFailureDetails message in a TrackOnionResponse. +func newTrackOnionFailureResponse( + details *FailureDetails) *TrackOnionResponse { + + return &TrackOnionResponse{ + Result: &TrackOnionResponse_FailureDetails{ + FailureDetails: details, + }, + } +} + +// marshallFailureDetails creates the FailureDetails message for the +// TrackOnion response body. +func marshallFailureDetails(err error) *FailureDetails { + var ( + clearTextErr htlcswitch.ClearTextError + fwdErr *htlcswitch.ForwardingError + ) + + details := &FailureDetails{ + ErrorMessage: err.Error(), + } + + switch { + case errors.As(err, &clearTextErr): + var buf bytes.Buffer + + encodeErr := lnwire.EncodeFailure( + &buf, clearTextErr.WireMessage(), 0, + ) + if encodeErr != nil { + log.Errorf("failed to encode wire message: %v", + encodeErr) + details.Failure = &FailureDetails_InternalError{ + InternalError: &InternalError{}, + } + + return details + } + + if errors.As(err, &fwdErr) { + details.Failure = &FailureDetails_ForwardingFailure{ + ForwardingFailure: &ForwardingFailure{ + FailureSourceIndex: uint32( + fwdErr.FailureSourceIdx, + ), + WireMessage: buf.Bytes(), + }, + } + } else { + details.Failure = &FailureDetails_ClearTextFailure{ + ClearTextFailure: &ClearTextFailure{ + WireMessage: buf.Bytes(), + }, + } + } + + // NOTE: ErrPaymentIDNotFound and ErrSwitchExiting are handled at + // the transport level and will not reach this function. + case errors.Is(err, htlcswitch.ErrUnreadableFailureMessage): + details.Failure = &FailureDetails_UnreadableFailure{ + UnreadableFailure: &UnreadableFailure{}, + } + + // All other unexpected errors will be mapped to a generic internal + // error. The specific reason is still available in the top-level + // error_message. + default: + details.Failure = &FailureDetails_InternalError{ + InternalError: &InternalError{}, + } + } + + return details +} + +// UnmarshallFailureDetails translates a FailureDetails message from a +// TrackOnion response into a concrete Go error. It handles all cases of the +// 'oneof failure' field. +func UnmarshallFailureDetails(details *FailureDetails, + deobfuscator htlcswitch.ErrorDecrypter) (error, error) { + + if details == nil { + return nil, errors.New("cannot unmarshall nil FailureDetails") + } + + // Use a type switch on the 'oneof failure' field to handle the primary + // structured error cases. + switch failure := details.Failure.(type) { + case *FailureDetails_ForwardingFailure: + return UnmarshallForwardingError(failure.ForwardingFailure) + + case *FailureDetails_ClearTextFailure: + return UnmarshallLinkError(failure.ClearTextFailure) + + case *FailureDetails_EncryptedErrorData: + if deobfuscator == nil { + return htlcswitch.ErrUnreadableFailureMessage, nil + } + + // The client provides the decryption key/logic. + return deobfuscator.DecryptError(failure.EncryptedErrorData) + + case *FailureDetails_UnreadableFailure: + return htlcswitch.ErrUnreadableFailureMessage, nil + + case *FailureDetails_InternalError: + // The specific reason is in the top-level message. + return errors.New(details.ErrorMessage), nil } - return fmt.Sprintf("%d@%s", e.FailureSourceIdx, - hex.EncodeToString(buf.Bytes())), nil + // Fallback for safety, though the oneof should always be populated + // on a failure response. + return nil, fmt.Errorf("unknown or empty failure reason in "+ + "response: %v", details.ErrorMessage) } -// ParseForwardingError converts an error from the format in SendOnion rpc -// protos to a forwarding error type. -func ParseForwardingError(errStr string) (*htlcswitch.ForwardingError, error) { - parts := strings.SplitN(errStr, "@", 2) - if len(parts) != 2 { - return nil, fmt.Errorf("invalid forwarding error format: %s", - errStr) +// UnmarshallForwardingError converts a protobuf ForwardingFailure message into +// an htlcswitch.ForwardingError. +func UnmarshallForwardingError(f *ForwardingFailure) ( + *htlcswitch.ForwardingError, error) { + + if f == nil { + return nil, fmt.Errorf("cannot parse nil ForwardingFailure") } - idx, err := strconv.Atoi(parts[0]) + wireMsg, err := UnmarshallFailureMessage(f.WireMessage) if err != nil { - return nil, fmt.Errorf("invalid forwarding error index: %s", - errStr) + return nil, fmt.Errorf("failed to decode wire message: %w", err) } - wireMsgBytes, err := hex.DecodeString(parts[1]) - if err != nil { - return nil, fmt.Errorf("invalid forwarding error wire "+ - "message: %s", errStr) + return htlcswitch.NewForwardingError( + wireMsg, int(f.FailureSourceIndex), + ), nil +} + +// UnmarshallLinkError converts a protobuf ClearTextFailure message into the an +// htlcswitch.LinkError. +func UnmarshallLinkError(f *ClearTextFailure) (*htlcswitch.LinkError, error) { + if f == nil { + return nil, fmt.Errorf("cannot parse nil ClearTextFailure") } - r := bytes.NewReader(wireMsgBytes) - wireMsg, err := lnwire.DecodeFailure(r, 0) + wireMsg, err := UnmarshallFailureMessage(f.WireMessage) if err != nil { - return nil, fmt.Errorf("failed to decode wire message: %w", - err) + return nil, fmt.Errorf("failed to decode wire message: %w", err) } - return htlcswitch.NewForwardingError(wireMsg, idx), nil + return htlcswitch.NewLinkError(wireMsg), nil +} + +// UnmarshallFailureMessage decodes a raw wire message byte slice into a rich +// lnwire.FailureMessage object. +func UnmarshallFailureMessage(wireMsg []byte) (lnwire.FailureMessage, error) { + r := bytes.NewReader(wireMsg) + + return lnwire.DecodeFailure(r, 0) } diff --git a/lnrpc/switchrpc/switch_server_test.go b/lnrpc/switchrpc/switch_server_test.go index d0dd7fde69..5acffe433c 100644 --- a/lnrpc/switchrpc/switch_server_test.go +++ b/lnrpc/switchrpc/switch_server_test.go @@ -8,7 +8,6 @@ import ( "context" "encoding/hex" "errors" - "fmt" "testing" "github.com/btcsuite/btcd/btcec/v2" @@ -217,8 +216,9 @@ func TestTrackOnion(t *testing.T) { // call. expectedErrCode codes.Code - // expectedResponse is the expected response from the RPC call. - expectedResponse *TrackOnionResponse + // checkResponse is a function that asserts the response from the + // RPC call. + checkResponse func(*testing.T, *TrackOnionResponse) }{ { name: "payment success", @@ -230,12 +230,12 @@ func TestTrackOnion(t *testing.T) { } }, getCtx: t.Context, - expectedResponse: &TrackOnionResponse{ - Preimage: preimageBytes, + checkResponse: func(t *testing.T, resp *TrackOnionResponse) { + require.Equal(t, preimageBytes, resp.GetPreimage()) }, }, { - name: "payment failed", + name: "payment failed with generic internal error", setup: func(t *testing.T, m *mockPayer, req *TrackOnionRequest) { @@ -244,24 +244,63 @@ func TestTrackOnion(t *testing.T) { } }, getCtx: t.Context, - expectedResponse: &TrackOnionResponse{ - ErrorMessage: "test error", - ErrorCode: ErrorCode_INTERNAL, + checkResponse: func(t *testing.T, resp *TrackOnionResponse) { + details := resp.GetFailureDetails() + require.NotNil(t, details) + require.NotNil(t, details.GetInternalError()) + require.Contains(t, details.ErrorMessage, "test error") }, }, { - name: "payment not found", + name: "payment failed with clear text error", setup: func(t *testing.T, m *mockPayer, req *TrackOnionRequest) { - m.getResultErr = htlcswitch.ErrPaymentIDNotFound + wireMsg := lnwire.NewTemporaryChannelFailure(nil) + linkErr := htlcswitch.NewLinkError(wireMsg) + m.getResultResult = &htlcswitch.PaymentResult{ + Error: linkErr, + } }, getCtx: t.Context, - expectedResponse: &TrackOnionResponse{ - ErrorMessage: htlcswitch.ErrPaymentIDNotFound.Error(), - ErrorCode: ErrorCode_PAYMENT_ID_NOT_FOUND, + checkResponse: func(t *testing.T, resp *TrackOnionResponse) { + details := resp.GetFailureDetails() + require.NotNil(t, details) + require.NotNil(t, details.GetClearTextFailure()) + require.Empty(t, details.GetForwardingFailure()) }, }, + { + name: "payment failed with forwarding error", + setup: func(t *testing.T, m *mockPayer, + req *TrackOnionRequest) { + + wireMsg := lnwire.NewTemporaryChannelFailure(nil) + fwdErr := htlcswitch.NewForwardingError(wireMsg, 1) + m.getResultResult = &htlcswitch.PaymentResult{ + Error: fwdErr, + } + }, + getCtx: t.Context, + checkResponse: func(t *testing.T, resp *TrackOnionResponse) { + details := resp.GetFailureDetails() + require.NotNil(t, details) + require.NotNil(t, details.GetForwardingFailure()) + require.Empty(t, details.GetClearTextFailure()) + require.Equal(t, uint32(1), + details.GetForwardingFailure().FailureSourceIndex) + }, + }, + { + name: "payment not found", + setup: func(t *testing.T, m *mockPayer, + req *TrackOnionRequest) { + + m.getResultErr = htlcswitch.ErrPaymentIDNotFound + }, + getCtx: t.Context, + expectedErrCode: codes.NotFound, + }, { name: "invalid payment hash", setup: func(t *testing.T, m *mockPayer, @@ -310,8 +349,11 @@ func TestTrackOnion(t *testing.T) { } }, getCtx: t.Context, - expectedResponse: &TrackOnionResponse{ - EncryptedError: []byte("encrypted error"), + checkResponse: func(t *testing.T, resp *TrackOnionResponse) { + details := resp.GetFailureDetails() + require.NotNil(t, details) + require.Equal(t, []byte("encrypted error"), + details.GetEncryptedErrorData()) }, }, { @@ -326,11 +368,8 @@ func TestTrackOnion(t *testing.T) { close(closedChan) m.resultChan = closedChan }, - getCtx: t.Context, - expectedResponse: &TrackOnionResponse{ - ErrorMessage: htlcswitch.ErrSwitchExiting.Error(), - ErrorCode: ErrorCode_SWITCH_EXITING, - }, + getCtx: t.Context, + expectedErrCode: codes.Unavailable, }, { name: "ambiguous result", @@ -338,18 +377,14 @@ func TestTrackOnion(t *testing.T) { req *TrackOnionRequest) { m.getResultResult = &htlcswitch.PaymentResult{ - // This is the critical part: a result - // with no error, and a zero-value - // preimage. + // A result with no error, and a zero + // valuepreimage. Error: nil, Preimage: lntypes.Preimage{}, } }, - getCtx: t.Context, - expectedResponse: &TrackOnionResponse{ - ErrorMessage: ErrAmbiguousPaymentState.Error(), - ErrorCode: ErrorCode_INTERNAL, - }, + getCtx: t.Context, + expectedErrCode: codes.Internal, }, } @@ -387,7 +422,7 @@ func TestTrackOnion(t *testing.T) { // If no gRPC error was expected, check the response. require.NoError(t, err) - require.Equal(t, tc.expectedResponse, resp) + tc.checkResponse(t, resp) }) } } @@ -571,11 +606,7 @@ func TestBuildOnion(t *testing.T) { func TestTranslateErrorForRPC(t *testing.T) { t.Parallel() - failureIndex := 1 mockWireMsg := lnwire.NewTemporaryChannelFailure(nil) - mockForwardingErr := htlcswitch.NewForwardingError( - mockWireMsg, failureIndex, - ) mockClearTextErr := htlcswitch.NewLinkError(mockWireMsg) var buf bytes.Buffer @@ -609,12 +640,6 @@ func TestTranslateErrorForRPC(t *testing.T) { expectedMsg: htlcswitch.ErrSwitchExiting.Error(), expectedCode: ErrorCode_SWITCH_EXITING, }, - { - name: "ForwardingError", - err: mockForwardingErr, - expectedMsg: fmt.Sprintf("%d@%s", failureIndex, encodedMsg), - expectedCode: ErrorCode_FORWARDING_ERROR, - }, { name: "ClearTextError", err: mockClearTextErr, @@ -638,87 +663,188 @@ func TestTranslateErrorForRPC(t *testing.T) { } } -// TestParseForwardingError tests the ParseForwardingError function. -func TestParseForwardingError(t *testing.T) { +// TestMarshallFailureDetails tests the conversion of internal errors types +// produced by the Switch into the wire/rpc representation. +func TestMarshallFailureDetails(t *testing.T) { t.Parallel() mockWireMsg := lnwire.NewTemporaryChannelFailure(nil) + mockLinkErr := htlcswitch.NewLinkError(mockWireMsg) + mockFwdErr := htlcswitch.NewForwardingError(mockWireMsg, 1) - var buf bytes.Buffer - err := lnwire.EncodeFailure(&buf, mockWireMsg, 0) - require.NoError(t, err) - - encodedMsg := hex.EncodeToString(buf.Bytes()) - - tests := []struct { - name string - errStr string - expectedIdx int - expectedWire lnwire.FailureMessage - expectsError bool + //nolint:ll + testCases := []struct { + name string + err error + expectedDetails *FailureDetails }{ + { - name: "Valid ForwardingError", - errStr: fmt.Sprintf("1@%s", encodedMsg), - expectedIdx: 1, - expectedWire: mockWireMsg, - expectsError: false, + name: "unreadable", + err: htlcswitch.ErrUnreadableFailureMessage, + expectedDetails: &FailureDetails{ + ErrorMessage: htlcswitch.ErrUnreadableFailureMessage.Error(), + Failure: &FailureDetails_UnreadableFailure{ + UnreadableFailure: &UnreadableFailure{}, + }, + }, }, { - name: "Invalid Format", - errStr: "invalid_format", - expectsError: true, + name: "clear text error", + err: mockLinkErr, + expectedDetails: &FailureDetails{ + ErrorMessage: mockLinkErr.Error(), + Failure: &FailureDetails_ClearTextFailure{ + ClearTextFailure: &ClearTextFailure{}, + }, + }, }, { - name: "Invalid Index", - errStr: "invalid@" + encodedMsg, - expectsError: true, + name: "forwarding error", + err: mockFwdErr, + expectedDetails: &FailureDetails{ + ErrorMessage: mockFwdErr.Error(), + Failure: &FailureDetails_ForwardingFailure{ + ForwardingFailure: &ForwardingFailure{ + FailureSourceIndex: 1, + }, + }, + }, }, { - name: "Invalid Wire Message", - errStr: "1@invalid", - expectsError: true, + name: "internal error", + err: errors.New("some unexpected error"), + expectedDetails: &FailureDetails{ + ErrorMessage: "some unexpected error", + Failure: &FailureDetails_InternalError{ + InternalError: &InternalError{}, + }, + }, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fwdErr, err := ParseForwardingError(tt.errStr) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + details := marshallFailureDetails(tc.err) - if tt.expectsError { - require.Error(t, err) + require.Contains(t, details.ErrorMessage, + tc.expectedDetails.ErrorMessage) + + if tc.expectedDetails.Failure == nil { + require.Nil(t, details.Failure) return } - require.NoError(t, err) - require.Equal( - t, tt.expectedIdx, fwdErr.FailureSourceIdx, - ) - require.Equal(t, tt.expectedWire, fwdErr.WireMessage()) + // For clear text and forwarding errors, we expect the + // wire message to be encoded correctly. + switch failure := details.Failure.(type) { + case *FailureDetails_ForwardingFailure: + require.NotNil(t, failure.ForwardingFailure) + require.Equal( + t, + tc.expectedDetails. + GetForwardingFailure(). + FailureSourceIndex, + failure.ForwardingFailure. + FailureSourceIndex, + ) + + decoded, err := UnmarshallFailureMessage( + failure.ForwardingFailure.WireMessage, + ) + require.NoError(t, err) + require.Equal(t, mockWireMsg, decoded) + + case *FailureDetails_ClearTextFailure: + require.NotNil(t, failure.ClearTextFailure) + + decoded, err := UnmarshallFailureMessage( + failure.ClearTextFailure.WireMessage, + ) + require.NoError(t, err) + require.Equal(t, mockWireMsg, decoded) + + case *FailureDetails_UnreadableFailure: + require.NotNil(t, failure.UnreadableFailure) + + case *FailureDetails_InternalError: + require.NotNil(t, failure.InternalError) + + default: + t.Fatalf("unexpected failure type: %T", + details.Failure) + } }) } } -// TestForwardingErrorEncodeDecode tests the encoding and decoding of a -// forwarding error. -func TestForwardingErrorEncodeDecode(t *testing.T) { +// TestUnmarshallFailureDetails tests the client helper for unmarshalling a +// TrackOnion FailureDetails message. This is a round-trip test that ensures the +// client helper can correctly decode the exact message that the server-side +// logic produces. +func TestUnmarshallFailureDetails(t *testing.T) { t.Parallel() - mockWireMsg := lnwire.NewTemporaryChannelFailure(nil) - mockForwardingErr := htlcswitch.NewForwardingError(mockWireMsg, 1) + // Create mock errors to be marshalled. + wireMsg := lnwire.NewTemporaryChannelFailure(nil) + linkErr := htlcswitch.NewLinkError(wireMsg) + fwdErr := htlcswitch.NewForwardingError(wireMsg, 1) + + testCases := []struct { + name string + originalErr error + }{ + { + name: "forwarding failure", + originalErr: fwdErr, + }, + { + name: "clear text failure", + originalErr: linkErr, + }, + { + name: "unreadable failure", + originalErr: htlcswitch.ErrUnreadableFailureMessage, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Use the server-side helper to create the + // FailureDetails message. + details := marshallFailureDetails(tc.originalErr) + + // Use the client-side helper to translate it back to a + // Go error. + translatedErr, err := UnmarshallFailureDetails( + details, nil, + ) + require.NoError(t, err) - // Encode the forwarding error. - encodedError, _ := translateErrorForRPC(mockForwardingErr) + // Confirm that the final error is of the same type as + // the original. + require.IsType(t, tc.originalErr, translatedErr) + require.ErrorContains( + t, translatedErr, tc.originalErr.Error(), + ) + }) + } - // Decode the forwarding error. - decodedError, err := ParseForwardingError(encodedError) - require.NoError(t, err, "decoding failed") + // Test the fallback case where the oneof is not populated. This + // simulates a backward-compatibility scenario or a server-side bug. + // We expect the unmarshaller to return an error in this case which + // more clearly indicates to the rpc client that the htlc status is + // unknown. + t.Run("empty failure oneof", func(t *testing.T) { + details := &FailureDetails{ + ErrorMessage: "simulated server bug", + Failure: nil, + } - // Assert the decoded error matches the original. - require.Equal(t, mockForwardingErr.FailureSourceIdx, - decodedError.FailureSourceIdx) - require.Equal(t, mockForwardingErr.WireMessage(), - decodedError.WireMessage()) + translatedErr, err := UnmarshallFailureDetails(details, nil) + require.Error(t, err) + require.Nil(t, translatedErr) + }) } // TestBuildErrorDecryptor tests the buildErrorDecryptor function. From 576257ca64978cccba622ddc53a0c3785455af6a Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Wed, 7 Jan 2026 16:01:55 -0500 Subject: [PATCH 2/4] docs: update v0.21 release notes --- docs/release-notes/release-notes-0.21.0.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index 1c45c884fc..d326292464 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -109,6 +109,17 @@ ## RPC Updates +* The `switchrpc.TrackOnion` RPC has been + [overhauled](https://github.com/lightningnetwork/lnd/pull/10472) to provide a + more robust and type-safe error handling mechanism. The `TrackOnionResponse` + message now uses a top-level `oneof` to enforce a compile-time guarantee that + a response contains either a `preimage` (for success) or structured + `FailureDetails` (for a payment failure). This replaces the previous + string-based error reporting. Application-level payment failures are now + clearly separated from RPC-level failures (e.g., attempt not found), which are + communicated via standard gRPC status codes. This is a **breaking change** for + any clients of the `TrackOnion` RPC. + ## lncli Updates ## Breaking Changes From c595112bd2a9c14c49b46477f07e3621d639bd16 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Tue, 3 Feb 2026 09:53:54 -0500 Subject: [PATCH 3/4] switchrpc: update SendOnion error handling Update both proto and handler to communicate error information via gRPC status details. --- itest/lnd_sendonion_test.go | 40 +- lnrpc/switchrpc/mock.go | 11 + lnrpc/switchrpc/switch.pb.go | 531 ++++++++++++++------------ lnrpc/switchrpc/switch.proto | 67 ++-- lnrpc/switchrpc/switch.swagger.json | 31 +- lnrpc/switchrpc/switch_server.go | 181 +++++++-- lnrpc/switchrpc/switch_server_test.go | 458 +++++++++++++++++----- lntest/rpc/switch.go | 9 +- 8 files changed, 875 insertions(+), 453 deletions(-) diff --git a/itest/lnd_sendonion_test.go b/itest/lnd_sendonion_test.go index 449dcaf216..34789ea182 100644 --- a/itest/lnd_sendonion_test.go +++ b/itest/lnd_sendonion_test.go @@ -1,7 +1,6 @@ package itest import ( - "context" "sync" "github.com/btcsuite/btcd/btcec/v2" @@ -12,7 +11,6 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/switchrpc" "github.com/lightningnetwork/lnd/lntest" - "github.com/lightningnetwork/lnd/lntest/rpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" @@ -84,9 +82,8 @@ func testSendOnion(ht *lntest.HarnessTest) { AttemptId: 1, } - resp := alice.RPC.SendOnion(sendReq) - require.True(ht, resp.Success, "expected successful onion send") - require.Empty(ht, resp.ErrorMessage, "unexpected failure to send onion") + err := alice.RPC.SendOnion(sendReq) + require.NoError(ht, err, "expected successful onion send") // Query for the result of the payment via onion and confirm that it // succeeded. @@ -168,9 +165,8 @@ func testSendOnionTwice(ht *lntest.HarnessTest) { OnionBlob: onionResp.OnionBlob, AttemptId: 1, } - resp := alice.RPC.SendOnion(sendReq) - require.True(ht, resp.Success, "expected successful onion send") - require.Empty(ht, resp.ErrorMessage, "unexpected failure to send onion") + err := alice.RPC.SendOnion(sendReq) + require.NoError(ht, err, "expected successful onion send") // Assert that the HTLC reaches Dave. invoiceStream := dave.RPC.SubscribeSingleInvoice(payHash[:]) @@ -179,12 +175,7 @@ func testSendOnionTwice(ht *lntest.HarnessTest) { // While the first onion is still in-flight, we'll send the same onion // again with the same attempt ID. This should error as our Switch will // detect duplicate ADDs for *in-flight* HTLCs. - ctxt, cancel := context.WithTimeout( - context.Background(), rpc.DefaultTimeout, - ) - defer cancel() - - _, err := alice.RPC.Switch.SendOnion(ctxt, sendReq) + err = alice.RPC.SendOnion(sendReq) require.Error(ht, err, "expected failure on onion send") // Check that we get the expected gRPC error. @@ -212,11 +203,7 @@ func testSendOnionTwice(ht *lntest.HarnessTest) { // Now that the original HTLC attempt has settled, we'll send the same // onion again with the same attempt ID. Confirm that this is also // prevented. - ctxt, cancel = context.WithTimeout(context.Background(), - rpc.DefaultTimeout) - defer cancel() - - _, err = alice.RPC.Switch.SendOnion(ctxt, sendReq) + err = alice.RPC.SendOnion(sendReq) require.Error(ht, err, "expected failure on onion send") // Check that we get the expected gRPC error. @@ -285,20 +272,14 @@ func testSendOnionConcurrency(ht *lntest.HarnessTest) { wg.Add(numConcurrentRequests) // Use channels to collect the results from each goroutine. - resultsChan := make(chan error, - numConcurrentRequests) + resultsChan := make(chan error, numConcurrentRequests) // Launch all requests concurrently to simulate a retry storm. for i := 0; i < numConcurrentRequests; i++ { go func() { defer wg.Done() - ctxt, cancel := context.WithTimeout( - context.Background(), - rpc.DefaultTimeout, - ) - defer cancel() - _, err := alice.RPC.Switch.SendOnion(ctxt, sendReq) + err := alice.RPC.SendOnion(sendReq) resultsChan <- err }() } @@ -398,9 +379,8 @@ func testTrackOnion(ht *lntest.HarnessTest) { AttemptId: 1, } - resp := alice.RPC.SendOnion(sendReq) - require.True(ht, resp.Success, "expected successful onion send") - require.Empty(ht, resp.ErrorMessage, "unexpected failure to send onion") + err := alice.RPC.SendOnion(sendReq) + require.NoError(ht, err, "expected successful onion send") // Track the payment providing all necessary information to delegate // error decryption to the server. We expect this to fail as Dave is not diff --git a/lnrpc/switchrpc/mock.go b/lnrpc/switchrpc/mock.go index 95d727f60d..a20beb0179 100644 --- a/lnrpc/switchrpc/mock.go +++ b/lnrpc/switchrpc/mock.go @@ -91,3 +91,14 @@ func (m *mockAttemptStore) FailPendingAttempt(attemptID uint64, return m.failErr } + +// mockErrorDecrypter is a mock implementation of htlcswitch.ErrorDecrypter. +type mockErrorDecrypter struct { + decryptedErr htlcswitch.ForwardingError +} + +func (m *mockErrorDecrypter) DecryptError( + err lnwire.OpaqueReason) (*htlcswitch.ForwardingError, error) { + + return &m.decryptedErr, nil +} diff --git a/lnrpc/switchrpc/switch.pb.go b/lnrpc/switchrpc/switch.pb.go index 042564a341..7a16a1c59c 100644 --- a/lnrpc/switchrpc/switch.pb.go +++ b/lnrpc/switchrpc/switch.pb.go @@ -26,47 +26,51 @@ type ErrorCode int32 const ( // Default value for unset errors. ErrorCode_UNSPECIFIED ErrorCode = 0 - // Payment ID was not found. - ErrorCode_PAYMENT_ID_NOT_FOUND ErrorCode = 1 - // Error occurred during forwarding. - ErrorCode_FORWARDING_ERROR ErrorCode = 2 - // Clear text error. - ErrorCode_CLEAR_TEXT_ERROR ErrorCode = 3 - // Failure message could not be read. + // An HTLC with the same ID has already been processed. For SendOnion, + // this is not a terminal error; it confirms the dispatch and the client + // can safely proceed to tracking. + ErrorCode_DUPLICATE_HTLC ErrorCode = 1 + // A definitive, local failure occurred for which a detailed lnwire + // message is available, but for which there is no more-specific error + // code. This is a forward-compatibility measure that allows clients to + // correctly identify new definitive failures. + ErrorCode_CLEAR_TEXT_ERROR ErrorCode = 2 + // The state of the HTLC dispatch is unknown because of an internal + // server error. The client MUST retry the exact same request to resolve + // this ambiguity and prevent a potential duplicate payment. + // + // This typically occurs when the server fails during the durable write of + // the idempotency anchor for the payment attempt (InitAttempt). + ErrorCode_HTLC_STATUS_UNKNOWN ErrorCode = 3 + // The failure message from a remote peer could not be decrypted. The + // client must use the encrypted_error_data to decrypt it themselves. ErrorCode_UNREADABLE_FAILURE_MESSAGE ErrorCode = 4 - // An HTLC with same ID is already in flight. - ErrorCode_DUPLICATE_HTLC ErrorCode = 5 - // No link available for payment. - ErrorCode_NO_LINK ErrorCode = 6 - // HTLC switch is exiting. - ErrorCode_SWITCH_EXITING ErrorCode = 7 - // Opaque internal server error. - ErrorCode_INTERNAL ErrorCode = 8 + // The server is in the process of shutting down. This is a transient + // error; the client should retry after a delay. + ErrorCode_SWITCH_EXITING ErrorCode = 5 + // A generic, internal server error occurred. + ErrorCode_INTERNAL ErrorCode = 6 ) // Enum value maps for ErrorCode. var ( ErrorCode_name = map[int32]string{ 0: "UNSPECIFIED", - 1: "PAYMENT_ID_NOT_FOUND", - 2: "FORWARDING_ERROR", - 3: "CLEAR_TEXT_ERROR", + 1: "DUPLICATE_HTLC", + 2: "CLEAR_TEXT_ERROR", + 3: "HTLC_STATUS_UNKNOWN", 4: "UNREADABLE_FAILURE_MESSAGE", - 5: "DUPLICATE_HTLC", - 6: "NO_LINK", - 7: "SWITCH_EXITING", - 8: "INTERNAL", + 5: "SWITCH_EXITING", + 6: "INTERNAL", } ErrorCode_value = map[string]int32{ "UNSPECIFIED": 0, - "PAYMENT_ID_NOT_FOUND": 1, - "FORWARDING_ERROR": 2, - "CLEAR_TEXT_ERROR": 3, + "DUPLICATE_HTLC": 1, + "CLEAR_TEXT_ERROR": 2, + "HTLC_STATUS_UNKNOWN": 3, "UNREADABLE_FAILURE_MESSAGE": 4, - "DUPLICATE_HTLC": 5, - "NO_LINK": 6, - "SWITCH_EXITING": 7, - "INTERNAL": 8, + "SWITCH_EXITING": 5, + "INTERNAL": 6, } ) @@ -236,20 +240,13 @@ func (x *SendOnionRequest) GetExtraData() []byte { return nil } +// SendOnionResponse is an empty message. A gRPC OK status indicates successful +// dispatch. Failures are communicated via the gRPC status error's details, +// which will contain a SendOnionFailureDetails message. type SendOnionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - - // Indicates if the onion was successfully sent or not. - // Equivalent to `error_code == ERROR_CODE_UNSPECIFIED`. - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` - // In case of failure, this field will provide more information about the - // error. - ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` - // A code representing the type of error that occurred. This can be used - // to programmatically distinguish between different kinds of errors. - ErrorCode ErrorCode `protobuf:"varint,3,opt,name=error_code,json=errorCode,proto3,enum=switchrpc.ErrorCode" json:"error_code,omitempty"` } func (x *SendOnionResponse) Reset() { @@ -284,25 +281,74 @@ func (*SendOnionResponse) Descriptor() ([]byte, []int) { return file_switchrpc_switch_proto_rawDescGZIP(), []int{1} } -func (x *SendOnionResponse) GetSuccess() bool { +// SendOnionFailureDetails provides structured details for a synchronous +// dispatch failure. It is attached to a non-OK gRPC status. +type SendOnionFailureDetails struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // A stable, machine-readable code for the dispatch failure. + ErrorCode ErrorCode `protobuf:"varint,1,opt,name=error_code,json=errorCode,proto3,enum=switchrpc.ErrorCode" json:"error_code,omitempty"` + // A human-readable message for logging and debugging. + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + // The specific details of a definitive, local failure. This will be + // populated if the error_code is CLEAR_TEXT_ERROR. This corresponds to + // an htlcswitch.ClearTextError. + ClearTextFailure *ClearTextFailure `protobuf:"bytes,3,opt,name=clear_text_failure,json=clearTextFailure,proto3" json:"clear_text_failure,omitempty"` +} + +func (x *SendOnionFailureDetails) Reset() { + *x = SendOnionFailureDetails{} + if protoimpl.UnsafeEnabled { + mi := &file_switchrpc_switch_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SendOnionFailureDetails) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendOnionFailureDetails) ProtoMessage() {} + +func (x *SendOnionFailureDetails) ProtoReflect() protoreflect.Message { + mi := &file_switchrpc_switch_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendOnionFailureDetails.ProtoReflect.Descriptor instead. +func (*SendOnionFailureDetails) Descriptor() ([]byte, []int) { + return file_switchrpc_switch_proto_rawDescGZIP(), []int{2} +} + +func (x *SendOnionFailureDetails) GetErrorCode() ErrorCode { if x != nil { - return x.Success + return x.ErrorCode } - return false + return ErrorCode_UNSPECIFIED } -func (x *SendOnionResponse) GetErrorMessage() string { +func (x *SendOnionFailureDetails) GetErrorMessage() string { if x != nil { return x.ErrorMessage } return "" } -func (x *SendOnionResponse) GetErrorCode() ErrorCode { +func (x *SendOnionFailureDetails) GetClearTextFailure() *ClearTextFailure { if x != nil { - return x.ErrorCode + return x.ClearTextFailure } - return ErrorCode_UNSPECIFIED + return nil } type TrackOnionRequest struct { @@ -327,7 +373,7 @@ type TrackOnionRequest struct { func (x *TrackOnionRequest) Reset() { *x = TrackOnionRequest{} if protoimpl.UnsafeEnabled { - mi := &file_switchrpc_switch_proto_msgTypes[2] + mi := &file_switchrpc_switch_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -340,7 +386,7 @@ func (x *TrackOnionRequest) String() string { func (*TrackOnionRequest) ProtoMessage() {} func (x *TrackOnionRequest) ProtoReflect() protoreflect.Message { - mi := &file_switchrpc_switch_proto_msgTypes[2] + mi := &file_switchrpc_switch_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -353,7 +399,7 @@ func (x *TrackOnionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackOnionRequest.ProtoReflect.Descriptor instead. func (*TrackOnionRequest) Descriptor() ([]byte, []int) { - return file_switchrpc_switch_proto_rawDescGZIP(), []int{2} + return file_switchrpc_switch_proto_rawDescGZIP(), []int{3} } func (x *TrackOnionRequest) GetPaymentHash() []byte { @@ -402,7 +448,7 @@ type TrackOnionResponse struct { func (x *TrackOnionResponse) Reset() { *x = TrackOnionResponse{} if protoimpl.UnsafeEnabled { - mi := &file_switchrpc_switch_proto_msgTypes[3] + mi := &file_switchrpc_switch_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -415,7 +461,7 @@ func (x *TrackOnionResponse) String() string { func (*TrackOnionResponse) ProtoMessage() {} func (x *TrackOnionResponse) ProtoReflect() protoreflect.Message { - mi := &file_switchrpc_switch_proto_msgTypes[3] + mi := &file_switchrpc_switch_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -428,7 +474,7 @@ func (x *TrackOnionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackOnionResponse.ProtoReflect.Descriptor instead. func (*TrackOnionResponse) Descriptor() ([]byte, []int) { - return file_switchrpc_switch_proto_rawDescGZIP(), []int{3} + return file_switchrpc_switch_proto_rawDescGZIP(), []int{4} } func (m *TrackOnionResponse) GetResult() isTrackOnionResponse_Result { @@ -497,7 +543,7 @@ type FailureDetails struct { func (x *FailureDetails) Reset() { *x = FailureDetails{} if protoimpl.UnsafeEnabled { - mi := &file_switchrpc_switch_proto_msgTypes[4] + mi := &file_switchrpc_switch_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -510,7 +556,7 @@ func (x *FailureDetails) String() string { func (*FailureDetails) ProtoMessage() {} func (x *FailureDetails) ProtoReflect() protoreflect.Message { - mi := &file_switchrpc_switch_proto_msgTypes[4] + mi := &file_switchrpc_switch_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -523,7 +569,7 @@ func (x *FailureDetails) ProtoReflect() protoreflect.Message { // Deprecated: Use FailureDetails.ProtoReflect.Descriptor instead. func (*FailureDetails) Descriptor() ([]byte, []int) { - return file_switchrpc_switch_proto_rawDescGZIP(), []int{4} + return file_switchrpc_switch_proto_rawDescGZIP(), []int{5} } func (x *FailureDetails) GetErrorMessage() string { @@ -635,7 +681,7 @@ type ForwardingFailure struct { func (x *ForwardingFailure) Reset() { *x = ForwardingFailure{} if protoimpl.UnsafeEnabled { - mi := &file_switchrpc_switch_proto_msgTypes[5] + mi := &file_switchrpc_switch_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -648,7 +694,7 @@ func (x *ForwardingFailure) String() string { func (*ForwardingFailure) ProtoMessage() {} func (x *ForwardingFailure) ProtoReflect() protoreflect.Message { - mi := &file_switchrpc_switch_proto_msgTypes[5] + mi := &file_switchrpc_switch_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -661,7 +707,7 @@ func (x *ForwardingFailure) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardingFailure.ProtoReflect.Descriptor instead. func (*ForwardingFailure) Descriptor() ([]byte, []int) { - return file_switchrpc_switch_proto_rawDescGZIP(), []int{5} + return file_switchrpc_switch_proto_rawDescGZIP(), []int{6} } func (x *ForwardingFailure) GetFailureSourceIndex() uint32 { @@ -693,7 +739,7 @@ type ClearTextFailure struct { func (x *ClearTextFailure) Reset() { *x = ClearTextFailure{} if protoimpl.UnsafeEnabled { - mi := &file_switchrpc_switch_proto_msgTypes[6] + mi := &file_switchrpc_switch_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -706,7 +752,7 @@ func (x *ClearTextFailure) String() string { func (*ClearTextFailure) ProtoMessage() {} func (x *ClearTextFailure) ProtoReflect() protoreflect.Message { - mi := &file_switchrpc_switch_proto_msgTypes[6] + mi := &file_switchrpc_switch_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -719,7 +765,7 @@ func (x *ClearTextFailure) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearTextFailure.ProtoReflect.Descriptor instead. func (*ClearTextFailure) Descriptor() ([]byte, []int) { - return file_switchrpc_switch_proto_rawDescGZIP(), []int{6} + return file_switchrpc_switch_proto_rawDescGZIP(), []int{7} } func (x *ClearTextFailure) GetWireMessage() []byte { @@ -739,7 +785,7 @@ type InternalError struct { func (x *InternalError) Reset() { *x = InternalError{} if protoimpl.UnsafeEnabled { - mi := &file_switchrpc_switch_proto_msgTypes[7] + mi := &file_switchrpc_switch_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -752,7 +798,7 @@ func (x *InternalError) String() string { func (*InternalError) ProtoMessage() {} func (x *InternalError) ProtoReflect() protoreflect.Message { - mi := &file_switchrpc_switch_proto_msgTypes[7] + mi := &file_switchrpc_switch_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -765,7 +811,7 @@ func (x *InternalError) ProtoReflect() protoreflect.Message { // Deprecated: Use InternalError.ProtoReflect.Descriptor instead. func (*InternalError) Descriptor() ([]byte, []int) { - return file_switchrpc_switch_proto_rawDescGZIP(), []int{7} + return file_switchrpc_switch_proto_rawDescGZIP(), []int{8} } // UnreadableFailure indicates that the failure message from the network was @@ -779,7 +825,7 @@ type UnreadableFailure struct { func (x *UnreadableFailure) Reset() { *x = UnreadableFailure{} if protoimpl.UnsafeEnabled { - mi := &file_switchrpc_switch_proto_msgTypes[8] + mi := &file_switchrpc_switch_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -792,7 +838,7 @@ func (x *UnreadableFailure) String() string { func (*UnreadableFailure) ProtoMessage() {} func (x *UnreadableFailure) ProtoReflect() protoreflect.Message { - mi := &file_switchrpc_switch_proto_msgTypes[8] + mi := &file_switchrpc_switch_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -805,7 +851,7 @@ func (x *UnreadableFailure) ProtoReflect() protoreflect.Message { // Deprecated: Use UnreadableFailure.ProtoReflect.Descriptor instead. func (*UnreadableFailure) Descriptor() ([]byte, []int) { - return file_switchrpc_switch_proto_rawDescGZIP(), []int{8} + return file_switchrpc_switch_proto_rawDescGZIP(), []int{9} } // BuildOnionRequest includes the necessary information to construct a Sphinx @@ -827,7 +873,7 @@ type BuildOnionRequest struct { func (x *BuildOnionRequest) Reset() { *x = BuildOnionRequest{} if protoimpl.UnsafeEnabled { - mi := &file_switchrpc_switch_proto_msgTypes[9] + mi := &file_switchrpc_switch_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -840,7 +886,7 @@ func (x *BuildOnionRequest) String() string { func (*BuildOnionRequest) ProtoMessage() {} func (x *BuildOnionRequest) ProtoReflect() protoreflect.Message { - mi := &file_switchrpc_switch_proto_msgTypes[9] + mi := &file_switchrpc_switch_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -853,7 +899,7 @@ func (x *BuildOnionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BuildOnionRequest.ProtoReflect.Descriptor instead. func (*BuildOnionRequest) Descriptor() ([]byte, []int) { - return file_switchrpc_switch_proto_rawDescGZIP(), []int{9} + return file_switchrpc_switch_proto_rawDescGZIP(), []int{10} } func (x *BuildOnionRequest) GetRoute() *lnrpc.Route { @@ -895,7 +941,7 @@ type BuildOnionResponse struct { func (x *BuildOnionResponse) Reset() { *x = BuildOnionResponse{} if protoimpl.UnsafeEnabled { - mi := &file_switchrpc_switch_proto_msgTypes[10] + mi := &file_switchrpc_switch_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -908,7 +954,7 @@ func (x *BuildOnionResponse) String() string { func (*BuildOnionResponse) ProtoMessage() {} func (x *BuildOnionResponse) ProtoReflect() protoreflect.Message { - mi := &file_switchrpc_switch_proto_msgTypes[10] + mi := &file_switchrpc_switch_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -921,7 +967,7 @@ func (x *BuildOnionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BuildOnionResponse.ProtoReflect.Descriptor instead. func (*BuildOnionResponse) Descriptor() ([]byte, []int) { - return file_switchrpc_switch_proto_rawDescGZIP(), []int{10} + return file_switchrpc_switch_proto_rawDescGZIP(), []int{11} } func (x *BuildOnionResponse) GetOnionBlob() []byte { @@ -983,121 +1029,124 @@ var file_switchrpc_switch_proto_rawDesc = []byte{ 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x62, 0x6c, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x42, - 0x0d, 0x0a, 0x0b, 0x5f, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x22, 0x87, - 0x01, 0x0a, 0x11, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, - 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x12, 0x33, 0x0a, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, - 0x72, 0x70, 0x63, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x09, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x22, 0xac, 0x01, 0x0a, 0x11, 0x54, 0x72, 0x61, - 0x63, 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, - 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, + 0x0d, 0x0a, 0x0b, 0x5f, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x22, 0x13, + 0x0a, 0x11, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0xbe, 0x01, 0x0a, 0x17, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, + 0x6e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, + 0x33, 0x0a, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x49, 0x0a, 0x12, 0x63, 0x6c, 0x65, + 0x61, 0x72, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, + 0x63, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x54, 0x65, 0x78, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, + 0x72, 0x65, 0x52, 0x10, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x54, 0x65, 0x78, 0x74, 0x46, 0x61, 0x69, + 0x6c, 0x75, 0x72, 0x65, 0x22, 0xac, 0x01, 0x0a, 0x11, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x4f, 0x6e, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, + 0x0a, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x09, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0b, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0c, 0x48, 0x00, 0x52, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x88, + 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x6f, 0x70, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x6f, 0x70, 0x50, 0x75, 0x62, 0x6b, + 0x65, 0x79, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, + 0x6b, 0x65, 0x79, 0x22, 0x82, 0x01, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x4f, 0x6e, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x08, 0x70, 0x72, + 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x08, + 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x44, 0x0a, 0x0f, 0x66, 0x61, 0x69, 0x6c, + 0x75, 0x72, 0x65, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x61, + 0x69, 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x48, 0x00, 0x52, 0x0e, + 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x42, 0x08, + 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0xa2, 0x03, 0x0a, 0x0e, 0x46, 0x61, 0x69, + 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x12, 0x4b, 0x0a, 0x12, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x66, + 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x73, + 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x54, 0x65, + 0x78, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x48, 0x00, 0x52, 0x10, 0x63, 0x6c, 0x65, + 0x61, 0x72, 0x54, 0x65, 0x78, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, 0x4d, 0x0a, + 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x66, 0x61, 0x69, 0x6c, + 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x73, 0x77, 0x69, 0x74, + 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, + 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x48, 0x00, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, 0x32, 0x0a, 0x14, + 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x12, 0x65, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x44, 0x61, 0x74, 0x61, + 0x12, 0x4d, 0x0a, 0x12, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, + 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x73, + 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x61, + 0x62, 0x6c, 0x65, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x48, 0x00, 0x52, 0x11, 0x75, 0x6e, + 0x72, 0x65, 0x61, 0x64, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, + 0x41, 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, + 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x48, 0x00, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x42, 0x09, 0x0a, 0x07, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x22, 0x68, 0x0a, + 0x11, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x46, 0x61, 0x69, 0x6c, 0x75, + 0x72, 0x65, 0x12, 0x30, 0x0a, 0x14, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x12, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x69, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x77, 0x69, 0x72, 0x65, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x35, 0x0a, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, + 0x54, 0x65, 0x78, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x77, + 0x69, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x0b, 0x77, 0x69, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x0f, + 0x0a, 0x0d, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, + 0x13, 0x0a, 0x11, 0x55, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x69, + 0x6c, 0x75, 0x72, 0x65, 0x22, 0x90, 0x01, 0x0a, 0x11, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x05, 0x72, 0x6f, + 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x05, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x21, + 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, - 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x49, 0x64, - 0x12, 0x24, 0x0a, 0x0b, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x6f, 0x70, 0x5f, 0x70, 0x75, - 0x62, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x6f, 0x70, - 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x73, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x82, 0x01, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, - 0x0a, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x48, 0x00, 0x52, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x44, 0x0a, 0x0f, - 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, - 0x63, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, - 0x48, 0x00, 0x52, 0x0e, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, - 0x6c, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0xa2, 0x03, 0x0a, - 0x0e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, - 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x12, 0x4b, 0x0a, 0x12, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x5f, 0x74, 0x65, - 0x78, 0x74, 0x5f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1b, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x65, - 0x61, 0x72, 0x54, 0x65, 0x78, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x48, 0x00, 0x52, - 0x10, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x54, 0x65, 0x78, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, - 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, - 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, - 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, - 0x64, 0x69, 0x6e, 0x67, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x48, 0x00, 0x52, 0x11, 0x66, - 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, - 0x12, 0x32, 0x0a, 0x14, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, - 0x52, 0x12, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x44, 0x61, 0x74, 0x61, 0x12, 0x4d, 0x0a, 0x12, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x61, 0x62, - 0x6c, 0x65, 0x5f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x6e, 0x72, - 0x65, 0x61, 0x64, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x48, 0x00, - 0x52, 0x11, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x61, 0x69, 0x6c, - 0x75, 0x72, 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x73, 0x77, - 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x42, 0x09, 0x0a, 0x07, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, - 0x65, 0x22, 0x68, 0x0a, 0x11, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x46, - 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, 0x30, 0x0a, 0x14, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, - 0x65, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x12, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x53, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x69, 0x72, 0x65, - 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, - 0x77, 0x69, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x35, 0x0a, 0x10, 0x43, - 0x6c, 0x65, 0x61, 0x72, 0x54, 0x65, 0x78, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x12, - 0x21, 0x0a, 0x0c, 0x77, 0x69, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x77, 0x69, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x45, 0x72, - 0x72, 0x6f, 0x72, 0x22, 0x13, 0x0a, 0x11, 0x55, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x61, 0x62, 0x6c, - 0x65, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x22, 0x90, 0x01, 0x0a, 0x11, 0x42, 0x75, 0x69, - 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, - 0x0a, 0x05, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, - 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x05, 0x72, 0x6f, 0x75, - 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, - 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x24, 0x0a, 0x0b, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x0a, 0x73, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, - 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x75, 0x0a, 0x12, 0x42, - 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x6e, 0x69, 0x6f, 0x6e, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x6f, 0x6e, 0x69, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, 0x62, - 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, - 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x6f, 0x70, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x6f, 0x70, 0x50, 0x75, 0x62, 0x6b, 0x65, - 0x79, 0x73, 0x2a, 0xc5, 0x01, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, - 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, - 0x00, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x49, 0x44, 0x5f, - 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10, 0x46, - 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x49, 0x4e, 0x47, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, - 0x02, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x4c, 0x45, 0x41, 0x52, 0x5f, 0x54, 0x45, 0x58, 0x54, 0x5f, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x55, 0x4e, 0x52, 0x45, 0x41, - 0x44, 0x41, 0x42, 0x4c, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x4d, 0x45, - 0x53, 0x53, 0x41, 0x47, 0x45, 0x10, 0x04, 0x12, 0x12, 0x0a, 0x0e, 0x44, 0x55, 0x50, 0x4c, 0x49, - 0x43, 0x41, 0x54, 0x45, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x10, 0x05, 0x12, 0x0b, 0x0a, 0x07, 0x4e, - 0x4f, 0x5f, 0x4c, 0x49, 0x4e, 0x4b, 0x10, 0x06, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x57, 0x49, 0x54, - 0x43, 0x48, 0x5f, 0x45, 0x58, 0x49, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x0c, 0x0a, 0x08, - 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x08, 0x32, 0xe6, 0x01, 0x0a, 0x06, 0x53, - 0x77, 0x69, 0x74, 0x63, 0x68, 0x12, 0x46, 0x0a, 0x09, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, - 0x6f, 0x6e, 0x12, 0x1b, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1c, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, - 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, - 0x0a, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x73, 0x77, - 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x4f, 0x6e, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x73, 0x77, 0x69, 0x74, - 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x42, 0x75, 0x69, 0x6c, - 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, - 0x70, 0x63, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, - 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x77, 0x69, - 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x68, 0x12, 0x24, 0x0a, 0x0b, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x75, 0x0a, 0x12, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, + 0x0a, 0x6f, 0x6e, 0x69, 0x6f, 0x6e, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x09, 0x6f, 0x6e, 0x69, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x1f, 0x0a, 0x0b, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, + 0x0b, 0x68, 0x6f, 0x70, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x6f, 0x70, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, 0x2a, 0xa1, + 0x01, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x0f, 0x0a, 0x0b, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x12, 0x0a, + 0x0e, 0x44, 0x55, 0x50, 0x4c, 0x49, 0x43, 0x41, 0x54, 0x45, 0x5f, 0x48, 0x54, 0x4c, 0x43, 0x10, + 0x01, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x4c, 0x45, 0x41, 0x52, 0x5f, 0x54, 0x45, 0x58, 0x54, 0x5f, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x17, 0x0a, 0x13, 0x48, 0x54, 0x4c, 0x43, 0x5f, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, + 0x12, 0x1e, 0x0a, 0x1a, 0x55, 0x4e, 0x52, 0x45, 0x41, 0x44, 0x41, 0x42, 0x4c, 0x45, 0x5f, 0x46, + 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x10, 0x04, + 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x57, 0x49, 0x54, 0x43, 0x48, 0x5f, 0x45, 0x58, 0x49, 0x54, 0x49, + 0x4e, 0x47, 0x10, 0x05, 0x12, 0x0c, 0x0a, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, + 0x10, 0x06, 0x32, 0xe6, 0x01, 0x0a, 0x06, 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x12, 0x46, 0x0a, + 0x09, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x2e, 0x73, 0x77, 0x69, + 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, + 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x4f, 0x6e, + 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, + 0x54, 0x72, 0x61, 0x63, 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1d, 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, + 0x61, 0x63, 0x6b, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x49, 0x0a, 0x0a, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x12, 0x1c, + 0x2e, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x73, + 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x4f, 0x6e, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, + 0x69, 0x6e, 0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, + 0x6e, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x77, 0x69, 0x74, 0x63, 0x68, 0x72, 0x70, 0x63, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1113,43 +1162,45 @@ func file_switchrpc_switch_proto_rawDescGZIP() []byte { } var file_switchrpc_switch_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_switchrpc_switch_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_switchrpc_switch_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_switchrpc_switch_proto_goTypes = []interface{}{ - (ErrorCode)(0), // 0: switchrpc.ErrorCode - (*SendOnionRequest)(nil), // 1: switchrpc.SendOnionRequest - (*SendOnionResponse)(nil), // 2: switchrpc.SendOnionResponse - (*TrackOnionRequest)(nil), // 3: switchrpc.TrackOnionRequest - (*TrackOnionResponse)(nil), // 4: switchrpc.TrackOnionResponse - (*FailureDetails)(nil), // 5: switchrpc.FailureDetails - (*ForwardingFailure)(nil), // 6: switchrpc.ForwardingFailure - (*ClearTextFailure)(nil), // 7: switchrpc.ClearTextFailure - (*InternalError)(nil), // 8: switchrpc.InternalError - (*UnreadableFailure)(nil), // 9: switchrpc.UnreadableFailure - (*BuildOnionRequest)(nil), // 10: switchrpc.BuildOnionRequest - (*BuildOnionResponse)(nil), // 11: switchrpc.BuildOnionResponse - nil, // 12: switchrpc.SendOnionRequest.CustomRecordsEntry - (*lnrpc.Route)(nil), // 13: lnrpc.Route + (ErrorCode)(0), // 0: switchrpc.ErrorCode + (*SendOnionRequest)(nil), // 1: switchrpc.SendOnionRequest + (*SendOnionResponse)(nil), // 2: switchrpc.SendOnionResponse + (*SendOnionFailureDetails)(nil), // 3: switchrpc.SendOnionFailureDetails + (*TrackOnionRequest)(nil), // 4: switchrpc.TrackOnionRequest + (*TrackOnionResponse)(nil), // 5: switchrpc.TrackOnionResponse + (*FailureDetails)(nil), // 6: switchrpc.FailureDetails + (*ForwardingFailure)(nil), // 7: switchrpc.ForwardingFailure + (*ClearTextFailure)(nil), // 8: switchrpc.ClearTextFailure + (*InternalError)(nil), // 9: switchrpc.InternalError + (*UnreadableFailure)(nil), // 10: switchrpc.UnreadableFailure + (*BuildOnionRequest)(nil), // 11: switchrpc.BuildOnionRequest + (*BuildOnionResponse)(nil), // 12: switchrpc.BuildOnionResponse + nil, // 13: switchrpc.SendOnionRequest.CustomRecordsEntry + (*lnrpc.Route)(nil), // 14: lnrpc.Route } var file_switchrpc_switch_proto_depIdxs = []int32{ - 12, // 0: switchrpc.SendOnionRequest.custom_records:type_name -> switchrpc.SendOnionRequest.CustomRecordsEntry - 0, // 1: switchrpc.SendOnionResponse.error_code:type_name -> switchrpc.ErrorCode - 5, // 2: switchrpc.TrackOnionResponse.failure_details:type_name -> switchrpc.FailureDetails - 7, // 3: switchrpc.FailureDetails.clear_text_failure:type_name -> switchrpc.ClearTextFailure - 6, // 4: switchrpc.FailureDetails.forwarding_failure:type_name -> switchrpc.ForwardingFailure - 9, // 5: switchrpc.FailureDetails.unreadable_failure:type_name -> switchrpc.UnreadableFailure - 8, // 6: switchrpc.FailureDetails.internal_error:type_name -> switchrpc.InternalError - 13, // 7: switchrpc.BuildOnionRequest.route:type_name -> lnrpc.Route - 1, // 8: switchrpc.Switch.SendOnion:input_type -> switchrpc.SendOnionRequest - 3, // 9: switchrpc.Switch.TrackOnion:input_type -> switchrpc.TrackOnionRequest - 10, // 10: switchrpc.Switch.BuildOnion:input_type -> switchrpc.BuildOnionRequest - 2, // 11: switchrpc.Switch.SendOnion:output_type -> switchrpc.SendOnionResponse - 4, // 12: switchrpc.Switch.TrackOnion:output_type -> switchrpc.TrackOnionResponse - 11, // 13: switchrpc.Switch.BuildOnion:output_type -> switchrpc.BuildOnionResponse - 11, // [11:14] is the sub-list for method output_type - 8, // [8:11] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 13, // 0: switchrpc.SendOnionRequest.custom_records:type_name -> switchrpc.SendOnionRequest.CustomRecordsEntry + 0, // 1: switchrpc.SendOnionFailureDetails.error_code:type_name -> switchrpc.ErrorCode + 8, // 2: switchrpc.SendOnionFailureDetails.clear_text_failure:type_name -> switchrpc.ClearTextFailure + 6, // 3: switchrpc.TrackOnionResponse.failure_details:type_name -> switchrpc.FailureDetails + 8, // 4: switchrpc.FailureDetails.clear_text_failure:type_name -> switchrpc.ClearTextFailure + 7, // 5: switchrpc.FailureDetails.forwarding_failure:type_name -> switchrpc.ForwardingFailure + 10, // 6: switchrpc.FailureDetails.unreadable_failure:type_name -> switchrpc.UnreadableFailure + 9, // 7: switchrpc.FailureDetails.internal_error:type_name -> switchrpc.InternalError + 14, // 8: switchrpc.BuildOnionRequest.route:type_name -> lnrpc.Route + 1, // 9: switchrpc.Switch.SendOnion:input_type -> switchrpc.SendOnionRequest + 4, // 10: switchrpc.Switch.TrackOnion:input_type -> switchrpc.TrackOnionRequest + 11, // 11: switchrpc.Switch.BuildOnion:input_type -> switchrpc.BuildOnionRequest + 2, // 12: switchrpc.Switch.SendOnion:output_type -> switchrpc.SendOnionResponse + 5, // 13: switchrpc.Switch.TrackOnion:output_type -> switchrpc.TrackOnionResponse + 12, // 14: switchrpc.Switch.BuildOnion:output_type -> switchrpc.BuildOnionResponse + 12, // [12:15] is the sub-list for method output_type + 9, // [9:12] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_switchrpc_switch_proto_init() } @@ -1183,7 +1234,7 @@ func file_switchrpc_switch_proto_init() { } } file_switchrpc_switch_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackOnionRequest); i { + switch v := v.(*SendOnionFailureDetails); i { case 0: return &v.state case 1: @@ -1195,7 +1246,7 @@ func file_switchrpc_switch_proto_init() { } } file_switchrpc_switch_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrackOnionResponse); i { + switch v := v.(*TrackOnionRequest); i { case 0: return &v.state case 1: @@ -1207,7 +1258,7 @@ func file_switchrpc_switch_proto_init() { } } file_switchrpc_switch_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FailureDetails); i { + switch v := v.(*TrackOnionResponse); i { case 0: return &v.state case 1: @@ -1219,7 +1270,7 @@ func file_switchrpc_switch_proto_init() { } } file_switchrpc_switch_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardingFailure); i { + switch v := v.(*FailureDetails); i { case 0: return &v.state case 1: @@ -1231,7 +1282,7 @@ func file_switchrpc_switch_proto_init() { } } file_switchrpc_switch_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ClearTextFailure); i { + switch v := v.(*ForwardingFailure); i { case 0: return &v.state case 1: @@ -1243,7 +1294,7 @@ func file_switchrpc_switch_proto_init() { } } file_switchrpc_switch_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*InternalError); i { + switch v := v.(*ClearTextFailure); i { case 0: return &v.state case 1: @@ -1255,7 +1306,7 @@ func file_switchrpc_switch_proto_init() { } } file_switchrpc_switch_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UnreadableFailure); i { + switch v := v.(*InternalError); i { case 0: return &v.state case 1: @@ -1267,7 +1318,7 @@ func file_switchrpc_switch_proto_init() { } } file_switchrpc_switch_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BuildOnionRequest); i { + switch v := v.(*UnreadableFailure); i { case 0: return &v.state case 1: @@ -1279,6 +1330,18 @@ func file_switchrpc_switch_proto_init() { } } file_switchrpc_switch_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BuildOnionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_switchrpc_switch_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BuildOnionResponse); i { case 0: return &v.state @@ -1292,26 +1355,26 @@ func file_switchrpc_switch_proto_init() { } } file_switchrpc_switch_proto_msgTypes[0].OneofWrappers = []interface{}{} - file_switchrpc_switch_proto_msgTypes[2].OneofWrappers = []interface{}{} - file_switchrpc_switch_proto_msgTypes[3].OneofWrappers = []interface{}{ + file_switchrpc_switch_proto_msgTypes[3].OneofWrappers = []interface{}{} + file_switchrpc_switch_proto_msgTypes[4].OneofWrappers = []interface{}{ (*TrackOnionResponse_Preimage)(nil), (*TrackOnionResponse_FailureDetails)(nil), } - file_switchrpc_switch_proto_msgTypes[4].OneofWrappers = []interface{}{ + file_switchrpc_switch_proto_msgTypes[5].OneofWrappers = []interface{}{ (*FailureDetails_ClearTextFailure)(nil), (*FailureDetails_ForwardingFailure)(nil), (*FailureDetails_EncryptedErrorData)(nil), (*FailureDetails_UnreadableFailure)(nil), (*FailureDetails_InternalError)(nil), } - file_switchrpc_switch_proto_msgTypes[9].OneofWrappers = []interface{}{} + file_switchrpc_switch_proto_msgTypes[10].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_switchrpc_switch_proto_rawDesc, NumEnums: 1, - NumMessages: 12, + NumMessages: 13, NumExtensions: 0, NumServices: 1, }, diff --git a/lnrpc/switchrpc/switch.proto b/lnrpc/switchrpc/switch.proto index c1ae20224e..65a18e4dd6 100644 --- a/lnrpc/switchrpc/switch.proto +++ b/lnrpc/switchrpc/switch.proto @@ -105,18 +105,25 @@ message SendOnionRequest { optional bytes extra_data = 10; } +// SendOnionResponse is an empty message. A gRPC OK status indicates successful +// dispatch. Failures are communicated via the gRPC status error's details, +// which will contain a SendOnionFailureDetails message. message SendOnionResponse { - // Indicates if the onion was successfully sent or not. - // Equivalent to `error_code == ERROR_CODE_UNSPECIFIED`. - bool success = 1; +} + +// SendOnionFailureDetails provides structured details for a synchronous +// dispatch failure. It is attached to a non-OK gRPC status. +message SendOnionFailureDetails { + // A stable, machine-readable code for the dispatch failure. + ErrorCode error_code = 1; - // In case of failure, this field will provide more information about the - // error. + // A human-readable message for logging and debugging. string error_message = 2; - // A code representing the type of error that occurred. This can be used - // to programmatically distinguish between different kinds of errors. - ErrorCode error_code = 3; + // The specific details of a definitive, local failure. This will be + // populated if the error_code is CLEAR_TEXT_ERROR. This corresponds to + // an htlcswitch.ClearTextError. + ClearTextFailure clear_text_failure = 3; } enum ErrorCode { @@ -126,44 +133,46 @@ enum ErrorCode { UNSPECIFIED = 0; /* - Payment ID was not found. + An HTLC with the same ID has already been processed. For SendOnion, + this is not a terminal error; it confirms the dispatch and the client + can safely proceed to tracking. */ - PAYMENT_ID_NOT_FOUND = 1; + DUPLICATE_HTLC = 1; /* - Error occurred during forwarding. + A definitive, local failure occurred for which a detailed lnwire + message is available, but for which there is no more-specific error + code. This is a forward-compatibility measure that allows clients to + correctly identify new definitive failures. */ - FORWARDING_ERROR = 2; + CLEAR_TEXT_ERROR = 2; /* - Clear text error. - */ - CLEAR_TEXT_ERROR = 3; + The state of the HTLC dispatch is unknown because of an internal + server error. The client MUST retry the exact same request to resolve + this ambiguity and prevent a potential duplicate payment. - /* - Failure message could not be read. + This typically occurs when the server fails during the durable write of + the idempotency anchor for the payment attempt (InitAttempt). */ - UNREADABLE_FAILURE_MESSAGE = 4; + HTLC_STATUS_UNKNOWN = 3; /* - An HTLC with same ID is already in flight. + The failure message from a remote peer could not be decrypted. The + client must use the encrypted_error_data to decrypt it themselves. */ - DUPLICATE_HTLC = 5; - - /* - No link available for payment. - */ - NO_LINK = 6; + UNREADABLE_FAILURE_MESSAGE = 4; /* - HTLC switch is exiting. + The server is in the process of shutting down. This is a transient + error; the client should retry after a delay. */ - SWITCH_EXITING = 7; + SWITCH_EXITING = 5; /* - Opaque internal server error. + A generic, internal server error occurred. */ - INTERNAL = 8; + INTERNAL = 6; } message TrackOnionRequest { diff --git a/lnrpc/switchrpc/switch.swagger.json b/lnrpc/switchrpc/switch.swagger.json index 58058f44c5..793de92d2e 100644 --- a/lnrpc/switchrpc/switch.swagger.json +++ b/lnrpc/switchrpc/switch.swagger.json @@ -362,22 +362,6 @@ }, "description": "ClearTextFailure is included when the failure originates locally or is a\nfully decrypted, known failure from an upstream node. It contains the raw\nlnwire.FailureMessage." }, - "switchrpcErrorCode": { - "type": "string", - "enum": [ - "UNSPECIFIED", - "PAYMENT_ID_NOT_FOUND", - "FORWARDING_ERROR", - "CLEAR_TEXT_ERROR", - "UNREADABLE_FAILURE_MESSAGE", - "DUPLICATE_HTLC", - "NO_LINK", - "SWITCH_EXITING", - "INTERNAL" - ], - "default": "UNSPECIFIED", - "description": " - UNSPECIFIED: Default value for unset errors.\n - PAYMENT_ID_NOT_FOUND: Payment ID was not found.\n - FORWARDING_ERROR: Error occurred during forwarding.\n - CLEAR_TEXT_ERROR: Clear text error.\n - UNREADABLE_FAILURE_MESSAGE: Failure message could not be read.\n - DUPLICATE_HTLC: An HTLC with same ID is already in flight.\n - NO_LINK: No link available for payment.\n - SWITCH_EXITING: HTLC switch is exiting.\n - INTERNAL: Opaque internal server error." - }, "switchrpcFailureDetails": { "type": "object", "properties": { @@ -489,20 +473,7 @@ }, "switchrpcSendOnionResponse": { "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "Indicates if the onion was successfully sent or not.\nEquivalent to `error_code == ERROR_CODE_UNSPECIFIED`." - }, - "error_message": { - "type": "string", - "description": "In case of failure, this field will provide more information about the\nerror." - }, - "error_code": { - "$ref": "#/definitions/switchrpcErrorCode", - "description": "A code representing the type of error that occurred. This can be used\nto programmatically distinguish between different kinds of errors." - } - } + "description": "SendOnionResponse is an empty message. A gRPC OK status indicates successful\ndispatch. Failures are communicated via the gRPC status error's details,\nwhich will contain a SendOnionFailureDetails message." }, "switchrpcTrackOnionRequest": { "type": "object", diff --git a/lnrpc/switchrpc/switch_server.go b/lnrpc/switchrpc/switch_server.go index a42b167d75..fefb87a0da 100644 --- a/lnrpc/switchrpc/switch_server.go +++ b/lnrpc/switchrpc/switch_server.go @@ -6,7 +6,6 @@ package switchrpc import ( "bytes" "context" - "encoding/hex" "errors" "fmt" "math/big" @@ -77,6 +76,10 @@ var ( // completes in an ambiguous state: no error and no valid preimage. ErrAmbiguousPaymentState = errors.New("payment completed in an " + "ambiguous state: no error and no valid preimage") + + // ErrUnknown is returned when a client is unable to unmarshall an + // error from a gRPC status. + ErrUnknown = errors.New("unable to unmarshall error") ) // ServerShell is a shell struct holding a reference to the actual sub-server. @@ -307,14 +310,10 @@ func (s *Server) SendOnion(_ context.Context, // If we receive an initialization error, we'll return the error // directly to the caller so they can handle the ambiguity. - // - // TODO(calvin): actually transport the error signal across the - // rpc boundary possibly using grpc st.WithDetails(). log.Errorf("Unable to initialize attempt id=%d: %v", attemptID, err) - return nil, status.Errorf(codes.Unavailable, "unable to "+ - "initialize attempt id=%d: %v", attemptID, err) + return nil, marshallSendOnionError(err) } // Perform all RPC-level pre-dispatch checks. @@ -352,17 +351,11 @@ func (s *Server) SendOnion(_ context.Context, // Translate the internal dispatch error into a gRPC status // with rich details for the client. - message, code := translateErrorForRPC(dispatchErr) - - return &SendOnionResponse{ - Success: false, - ErrorMessage: message, - ErrorCode: code, - }, nil + return nil, marshallSendOnionError(dispatchErr) } // The onion attempt was successfully dispatched. - return &SendOnionResponse{Success: true}, nil + return &SendOnionResponse{}, nil } // failPendingAttempt is a helper which transitions the given attempt from an @@ -827,44 +820,154 @@ func (s *Server) BuildOnion(_ context.Context, }, nil } -// translateErrorForRPC converts an error from the underlying HTLC switch to -// a form that we can package for delivery to SendOnion rpc clients. -func translateErrorForRPC(err error) (string, ErrorCode) { - var ( - clearTextErr htlcswitch.ClearTextError - ) - - switch { - case errors.Is(err, htlcswitch.ErrPaymentIDNotFound): - return err.Error(), ErrorCode_PAYMENT_ID_NOT_FOUND - - case errors.Is(err, htlcswitch.ErrDuplicateAdd): - return err.Error(), ErrorCode_DUPLICATE_HTLC +// marshallSendOnionError translates an error from the underlying HTLC switch +// into a gRPC status error with rich, fine-grained details. +func marshallSendOnionError(err error) error { + var clearTextErr htlcswitch.ClearTextError - case errors.Is(err, htlcswitch.ErrUnreadableFailureMessage): - return err.Error(), - ErrorCode_UNREADABLE_FAILURE_MESSAGE - - case errors.Is(err, htlcswitch.ErrSwitchExiting): - return err.Error(), ErrorCode_SWITCH_EXITING + details := &SendOnionFailureDetails{ + ErrorMessage: err.Error(), + } + var rpcCode codes.Code + switch { case errors.As(err, &clearTextErr): + // We have a clear text error. We can now extract the + // underlying wire message. var buf bytes.Buffer encodeErr := lnwire.EncodeFailure( &buf, clearTextErr.WireMessage(), 0, ) if encodeErr != nil { - return fmt.Sprintf("failed to encode wire "+ - "message: %v", encodeErr), - ErrorCode_INTERNAL + return status.Errorf(codes.Internal, + "failed to encode wire message: %v", + encodeErr) + } + + details.ClearTextFailure = &ClearTextFailure{ + WireMessage: buf.Bytes(), } + details.ErrorCode = ErrorCode_CLEAR_TEXT_ERROR + rpcCode = codes.FailedPrecondition - return hex.EncodeToString(buf.Bytes()), - ErrorCode_CLEAR_TEXT_ERROR + case errors.Is(err, htlcswitch.ErrUnreadableFailureMessage): + details.ErrorCode = ErrorCode_UNREADABLE_FAILURE_MESSAGE + rpcCode = codes.Internal + + case errors.Is(err, htlcswitch.ErrDuplicateAdd): + details.ErrorCode = ErrorCode_DUPLICATE_HTLC + rpcCode = codes.AlreadyExists + + case errors.Is(err, htlcswitch.ErrSwitchExiting): + details.ErrorCode = ErrorCode_SWITCH_EXITING + rpcCode = codes.Unavailable + + // Handle the critical ambiguous failure from InitAttempt. This error + // type signals that we are unsure if the idempotency anchor was + // written, and cannot provide acknowledgement on whether or not an htlc + // for the given attempt ID has been processed by the server. We signal + // this with a specific ErrorCode and a top-level gRPC status of + // Unavailable. The client MUST retry to resolve the ambiguity. + case errors.Is(err, htlcswitch.ErrAmbiguousAttemptInit): + details.ErrorCode = ErrorCode_HTLC_STATUS_UNKNOWN + rpcCode = codes.Unavailable default: - return err.Error(), ErrorCode_INTERNAL + rpcCode = codes.Internal } + + // All definitive failures that are translated by this function occurred + // after the idempotency key was written. We can generally classify them + // as a failed precondition for the dispatch to succeed. + st := status.New(rpcCode, err.Error()) + stWithDetails, attachErr := st.WithDetails(details) + if attachErr != nil { + log.Warnf("Unable to attach details to SendOnion error: %v", + attachErr) + return st.Err() + } + + return stWithDetails.Err() +} + +// UnmarshallSendOnionError inspects a gRPC error from a SendOnion call, +// extracts the rich failure details, and translates it into a concrete Go +// error. It returns the specific translated error if details are found, +// otherwise it returns a generic error. +func UnmarshallSendOnionError(rpcErr error) error { + st, ok := status.FromError(rpcErr) + if !ok { + // Not a gRPC status error, return as is. + return rpcErr + } + + // Search for the specific failure details message within the status. + for _, detail := range st.Details() { + if failure, ok := detail.(*SendOnionFailureDetails); ok { + // We found the details. Now translate them into the + // appropriate Go error type. + + // First, check for a specific structured error. This is + // the most detailed information we can get. + if failure.ClearTextFailure != nil { + // This is the most common case for a definitive + // failure. + linkErr, err := UnmarshallLinkError( + failure.ClearTextFailure, + ) + if err != nil { + return err + } + + return linkErr + } + + // If no structured error is present, check for a + // specific error code. + switch failure.ErrorCode { + case ErrorCode_DUPLICATE_HTLC: + return htlcswitch.ErrDuplicateAdd + case ErrorCode_UNREADABLE_FAILURE_MESSAGE: + return htlcswitch.ErrUnreadableFailureMessage + case ErrorCode_SWITCH_EXITING: + return htlcswitch.ErrSwitchExiting + case ErrorCode_HTLC_STATUS_UNKNOWN: + return htlcswitch.ErrAmbiguousAttemptInit + } + + // Fallback to the generic error message if no + // structured failure or specific code is present. + return fmt.Errorf("%w: %s", ErrUnknown, + failure.ErrorMessage) + } + } + + // No details were found, return the original gRPC status error + // wrapped in our sentinel error. + return fmt.Errorf("%w: %w", ErrUnknown, rpcErr) +} + +// GetSendOnionFailureDetails inspects a gRPC error from a SendOnion call and +// extracts the rich failure details, if present. It returns the details struct +// directly, allowing the caller to inspect all fields. It returns nil if no +// such details are found. +func GetSendOnionFailureDetails(rpcErr error) *SendOnionFailureDetails { + st, ok := status.FromError(rpcErr) + if !ok { + // Not a gRPC status error. + return nil + } + + // Search for the specific failure details message within the status. + for _, detail := range st.Details() { + if failure, ok := detail.(*SendOnionFailureDetails); ok { + // We found the details, return them directly. + return failure + } + } + + // No details were found. + return nil } // newTrackOnionFailureResponse is a helper function that wraps a diff --git a/lnrpc/switchrpc/switch_server_test.go b/lnrpc/switchrpc/switch_server_test.go index 5acffe433c..71fac98ada 100644 --- a/lnrpc/switchrpc/switch_server_test.go +++ b/lnrpc/switchrpc/switch_server_test.go @@ -6,8 +6,8 @@ package switchrpc import ( "bytes" "context" - "encoding/hex" "errors" + "fmt" "testing" "github.com/btcsuite/btcd/btcec/v2" @@ -17,8 +17,8 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" "github.com/stretchr/testify/require" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // TestSendOnion is a unit test that rigorously verifies the behavior of the @@ -57,8 +57,9 @@ func TestSendOnion(t *testing.T) { // call. expectedErrCode codes.Code - // expectedResponse is the expected response from the RPC call. - expectedResponse *SendOnionResponse + // checkFailureDetails is a function that asserts the contents + // of the SendOnionFailureDetails message. + checkFailureDetails func(*testing.T, *SendOnionFailureDetails) }{ { name: "valid request", @@ -70,7 +71,7 @@ func TestSendOnion(t *testing.T) { require.True(t, ok) payer.sendErr = nil }, - expectedResponse: &SendOnionResponse{Success: true}, + expectedErrCode: codes.OK, }, { name: "missing onion blob", @@ -118,21 +119,20 @@ func TestSendOnion(t *testing.T) { require.True(t, ok) payer.sendErr = errors.New("internal error") }, - expectedResponse: &SendOnionResponse{ - Success: false, - ErrorMessage: "internal error", - ErrorCode: ErrorCode_INTERNAL, + expectedErrCode: codes.Internal, + checkFailureDetails: func(t *testing.T, + details *SendOnionFailureDetails) { + + require.Contains(t, details.ErrorMessage, + "internal error") }, }, { - // The ErrDuplicateAdd error is the means by which an - // rpc client is safe to retry the SendOnion rpc until - // an explicit acknowledgement of htlc dispatch can be - // received from the server. The ability to retry and - // rely on duplicate prevention is useful under - // scenarios where the status of htlc dispatch is - // uncertain (eg: network timeout or after restart). - name: "dispatcher duplicate htlc error", + // The ErrPaymentIDAlreadyExists error is the means by + // which an rpc client is safe to retry the SendOnion + // RPC until an explicit acknowledgement of HTLC + // dispatch can be received from the server. + name: "idempotency anchor fails", setup: func(t *testing.T, s *Server, req *SendOnionRequest) { @@ -144,6 +144,51 @@ func TestSendOnion(t *testing.T) { }, expectedErrCode: codes.AlreadyExists, }, + { + name: "ambiguous attempt init error", + setup: func(t *testing.T, s *Server, + req *SendOnionRequest) { + + store, ok := s.cfg.AttemptStore.(*mockAttemptStore) + require.True(t, ok) + store.initErr = htlcswitch.ErrAmbiguousAttemptInit + }, + expectedErrCode: codes.Unavailable, + checkFailureDetails: func(t *testing.T, + details *SendOnionFailureDetails) { + + require.Equal(t, ErrorCode_HTLC_STATUS_UNKNOWN, + details.ErrorCode) + }, + }, + { + name: "clear text error", + setup: func(t *testing.T, s *Server, + req *SendOnionRequest) { + + wireMsg := lnwire.NewTemporaryChannelFailure(nil) + linkErr := htlcswitch.NewLinkError(wireMsg) + + payer, ok := s.cfg.HtlcDispatcher.(*mockPayer) + require.True(t, ok) + payer.sendErr = linkErr + }, + expectedErrCode: codes.FailedPrecondition, + checkFailureDetails: func(t *testing.T, + details *SendOnionFailureDetails) { + + require.Equal(t, ErrorCode_CLEAR_TEXT_ERROR, + details.ErrorCode) + + failure := details.GetClearTextFailure() + require.NotNil(t, failure) + + _, err := UnmarshallFailureMessage( + failure.WireMessage, + ) + require.NoError(t, err) + }, + }, } for _, tc := range testCases { @@ -167,9 +212,19 @@ func TestSendOnion(t *testing.T) { resp, err := server.SendOnion(t.Context(), req) - // Check for gRPC level errors. - if tc.expectedErrCode != codes.OK { - require.Error(t, err) + // If we expected OK, assert a nil error and non-nil + // response. + if tc.expectedErrCode == codes.OK { + require.NoError(t, err) + require.NotNil(t, resp) + return + } + + // Otherwise, we expect a gRPC status error. + require.Error(t, err) + + // If we don't need to check details, we're done. + if tc.checkFailureDetails == nil { s, ok := status.FromError(err) require.True(t, ok) require.Equal(t, tc.expectedErrCode, s.Code()) @@ -177,13 +232,36 @@ func TestSendOnion(t *testing.T) { return } - // If no gRPC error was expected, check the response. - require.NoError(t, err) - require.Equal(t, tc.expectedResponse, resp) + // Otherwise confirm the failure details are as + // expected. + details := requireSendOnionFailureDetails( + t, err, tc.expectedErrCode, + ) + tc.checkFailureDetails(t, details) }) } } +// requireSendOnionFailureDetails is a test helper that asserts a SendOnion call +// failed with a specific gRPC status code and extracts the embedded +// SendOnionFailureDetails message. +func requireSendOnionFailureDetails(t *testing.T, err error, + expectedRPCCode codes.Code) *SendOnionFailureDetails { + + s, ok := status.FromError(err) + require.True(t, ok, "expected gRPC status error") + require.Equal(t, expectedRPCCode, s.Code(), + "unexpected gRPC status code") + + details := s.Details() + require.Len(t, details, 1, "expected one failure detail") + + failureDetails, ok := details[0].(*SendOnionFailureDetails) + require.True(t, ok, "expected SendOnionFailureDetails") + + return failureDetails +} + // TestTrackOnion is a unit test that rigorously verifies the behavior of the // TrackOnion RPC handler in isolation. func TestTrackOnion(t *testing.T) { @@ -602,67 +680,6 @@ func TestBuildOnion(t *testing.T) { } } -// TestTranslateErrorForRPC tests the TranslateErrorForRPC function. -func TestTranslateErrorForRPC(t *testing.T) { - t.Parallel() - - mockWireMsg := lnwire.NewTemporaryChannelFailure(nil) - mockClearTextErr := htlcswitch.NewLinkError(mockWireMsg) - - var buf bytes.Buffer - err := lnwire.EncodeFailure(&buf, mockWireMsg, 0) - require.NoError(t, err) - - encodedMsg := hex.EncodeToString(buf.Bytes()) - - //nolint:ll - tests := []struct { - name string - err error - expectedMsg string - expectedCode ErrorCode - }{ - { - name: "ErrPaymentIDNotFound", - err: htlcswitch.ErrPaymentIDNotFound, - expectedMsg: htlcswitch.ErrPaymentIDNotFound.Error(), - expectedCode: ErrorCode_PAYMENT_ID_NOT_FOUND, - }, - { - name: "ErrUnreadableFailureMessage", - err: htlcswitch.ErrUnreadableFailureMessage, - expectedMsg: htlcswitch.ErrUnreadableFailureMessage.Error(), - expectedCode: ErrorCode_UNREADABLE_FAILURE_MESSAGE, - }, - { - name: "ErrSwitchExiting", - err: htlcswitch.ErrSwitchExiting, - expectedMsg: htlcswitch.ErrSwitchExiting.Error(), - expectedCode: ErrorCode_SWITCH_EXITING, - }, - { - name: "ClearTextError", - err: mockClearTextErr, - expectedMsg: encodedMsg, - expectedCode: ErrorCode_CLEAR_TEXT_ERROR, - }, - { - name: "Unknown Error", - err: errors.New("some unexpected error"), - expectedMsg: "some unexpected error", - expectedCode: ErrorCode_INTERNAL, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - msg, code := translateErrorForRPC(tt.err) - require.Contains(t, msg, tt.expectedMsg) - require.Equal(t, tt.expectedCode, code) - }) - } -} - // TestMarshallFailureDetails tests the conversion of internal errors types // produced by the Switch into the wire/rpc representation. func TestMarshallFailureDetails(t *testing.T) { @@ -678,7 +695,6 @@ func TestMarshallFailureDetails(t *testing.T) { err error expectedDetails *FailureDetails }{ - { name: "unreadable", err: htlcswitch.ErrUnreadableFailureMessage, @@ -689,6 +705,7 @@ func TestMarshallFailureDetails(t *testing.T) { }, }, }, + { name: "clear text error", err: mockLinkErr, @@ -790,6 +807,12 @@ func TestUnmarshallFailureDetails(t *testing.T) { linkErr := htlcswitch.NewLinkError(wireMsg) fwdErr := htlcswitch.NewForwardingError(wireMsg, 1) + // Create a forwarding error to be returned by the mock decrypter. + // Mock error decrypter that always returns a specific error. + mockErrorDecrypter := &mockErrorDecrypter{ + decryptedErr: *fwdErr, + } + testCases := []struct { name string originalErr error @@ -810,6 +833,8 @@ func TestUnmarshallFailureDetails(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + t.Parallel() + // Use the server-side helper to create the // FailureDetails message. details := marshallFailureDetails(tc.originalErr) @@ -845,6 +870,66 @@ func TestUnmarshallFailureDetails(t *testing.T) { require.Error(t, err) require.Nil(t, translatedErr) }) + + // Add a test case for nil FailureDetails input. + t.Run("nil failure details", func(t *testing.T) { + t.Parallel() + + _, err := UnmarshallFailureDetails(nil, nil) + require.Error(t, err) + require.EqualError(t, err, + "cannot unmarshall nil FailureDetails") + }) + + t.Run("unreadable failure message fallback", func(t *testing.T) { + t.Parallel() + + details := &FailureDetails{ + Failure: &FailureDetails_UnreadableFailure{ + UnreadableFailure: &UnreadableFailure{}, + }, + ErrorMessage: htlcswitch. + ErrUnreadableFailureMessage.Error(), + } + + translatedErr, err := UnmarshallFailureDetails(details, nil) + require.NoError(t, err) + require.ErrorIs(t, translatedErr, + htlcswitch.ErrUnreadableFailureMessage) + }) + + t.Run("generic error message fallback", func(t *testing.T) { + t.Parallel() + + expectedMsg := "some generic error" + details := &FailureDetails{ + Failure: &FailureDetails_InternalError{ + InternalError: &InternalError{}, + }, + ErrorMessage: expectedMsg, + } + + translatedErr, err := UnmarshallFailureDetails(details, nil) + require.NoError(t, err) + require.EqualError(t, translatedErr, expectedMsg) + }) + + t.Run("encrypted error with decryptor", func(t *testing.T) { + t.Parallel() + + encryptedData := []byte("some encrypted data") + details := &FailureDetails{ + Failure: &FailureDetails_EncryptedErrorData{ + EncryptedErrorData: encryptedData, + }, + } + + translatedErr, err := UnmarshallFailureDetails( + details, mockErrorDecrypter, + ) + require.NoError(t, err) + require.Equal(t, fwdErr, translatedErr) + }) } // TestBuildErrorDecryptor tests the buildErrorDecryptor function. @@ -932,3 +1017,204 @@ func TestBuildErrorDecryptor(t *testing.T) { }) } } + +// TestUnmarshallSendOnionError tests the client helper for unmarshalling a +// SendOnion error. This is a round-trip test that ensures the client helper can +// correctly decode the exact error that the server-side +// logic produces. +func TestUnmarshallSendOnionError(t *testing.T) { + t.Parallel() + + // Create mock errors to be marshalled by the server-side helper. + wireMsg := lnwire.NewTemporaryChannelFailure(nil) + linkErr := htlcswitch.NewLinkError(wireMsg) + exitErr := htlcswitch.ErrSwitchExiting + internalErr := errors.New("internal error") + dbFailure := errors.New("db failure") + ambiguousInitErr := fmt.Errorf("%w: %s", + htlcswitch.ErrAmbiguousAttemptInit, dbFailure.Error()) + + testCases := []struct { + name string + originalErr error + + // isSpecific denotes if we expect to unmarshall a specific Go + // error type, vs a generic one that wraps ErrUnknown. + isSpecific bool + }{ + { + name: "clear text error", + originalErr: linkErr, + isSpecific: true, + }, + { + name: "switch exiting", + originalErr: exitErr, + isSpecific: true, + }, + { + name: "duplicate htlc", + originalErr: htlcswitch.ErrDuplicateAdd, + isSpecific: true, + }, + { + name: "unreadable failure message", + originalErr: htlcswitch.ErrUnreadableFailureMessage, + isSpecific: true, + }, + { + name: "failed attempt initialization", + originalErr: ambiguousInitErr, + isSpecific: true, + }, + { + name: "internal error", + originalErr: internalErr, + isSpecific: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Use the server-side helper to create the gRPC status + // error. + rpcErr := marshallSendOnionError(tc.originalErr) + + // Use the client-side helper to translate it back. + translatedErr := UnmarshallSendOnionError(rpcErr) + require.Error(t, translatedErr) + + // Based on the type of the original error, we either + // expect a specific Go error type back, or a generic + // error that wraps our sentinel. + if tc.isSpecific { + if errors.Is(tc.originalErr, + htlcswitch.ErrAmbiguousAttemptInit) { + // For the ambiguous error, we assert + // that the translated error is the + // htlcswitch.ErrAmbiguousAttemptInit + // sentinel. + require.ErrorIs(t, translatedErr, + htlcswitch. + ErrAmbiguousAttemptInit) + + return + } + + require.Equal(t, tc.originalErr, translatedErr) + } else { + require.ErrorIs(t, translatedErr, ErrUnknown) + require.Contains(t, translatedErr.Error(), + tc.originalErr.Error()) + } + }) + } +} + +// TestGetSendOnionFailureDetails verifies the behavior of the +// GetSendOnionFailureDetails helper. +func TestGetSendOnionFailureDetails(t *testing.T) { + t.Parallel() + + // Create a mock SendOnionFailureDetails struct. + mockDetails := &SendOnionFailureDetails{ + ErrorCode: ErrorCode_INTERNAL, + ErrorMessage: "mock error message", + } + + // Create a gRPC status error with the mock details. + gRPCErrorWithDetails, err := status.New( + codes.FailedPrecondition, "test error", + ).WithDetails(mockDetails) + require.NoError(t, err) + + // Create a mock wire message and encode it. + wireMsg := lnwire.NewTemporaryChannelFailure(nil) + var buf bytes.Buffer + err = lnwire.EncodeFailure(&buf, wireMsg, 0) + require.NoError(t, err) + + // Create details with a ClearTextFailure for a table test case. + mockDetailsWithClearText := &SendOnionFailureDetails{ + ErrorCode: ErrorCode_CLEAR_TEXT_ERROR, + ClearTextFailure: &ClearTextFailure{ + WireMessage: buf.Bytes(), + }, + } + + // Create a gRPC error with ClearTextFailure details. + gRPCErrorWithClearTextDetails, err := status.New( + codes.FailedPrecondition, "clear text", + ).WithDetails(mockDetailsWithClearText) + require.NoError(t, err) + + // Create a gRPC status error without any details. + gRPCErrorWithoutDetails := status.Error(codes.Internal, + "generic gRPC error") + + // Create a non-gRPC error. + nonGrpcError := errors.New("plain old error") + + testCases := []struct { + name string + err error + expectedDetails *SendOnionFailureDetails + }{ + { + name: "gRPC error with details", + err: gRPCErrorWithDetails.Err(), + expectedDetails: mockDetails, + }, + { + name: "gRPC error with clear text details", + err: gRPCErrorWithClearTextDetails.Err(), + expectedDetails: mockDetailsWithClearText, + }, + { + name: "gRPC error without details", + err: gRPCErrorWithoutDetails, + expectedDetails: nil, + }, + { + name: "non-gRPC error", + err: nonGrpcError, + expectedDetails: nil, + }, + { + name: "nil error", + err: nil, + expectedDetails: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := GetSendOnionFailureDetails(tc.err) + + // Confirm now details were provided if none were + // expected. + if tc.expectedDetails == nil { + require.Nil(t, result) + + return + } + + // Otherwise, confirm that the failure details are + // as expected. + require.NotNil(t, result) + require.Equal(t, tc.expectedDetails.ErrorCode, + result.ErrorCode) + require.Equal(t, tc.expectedDetails.ErrorMessage, + result.ErrorMessage) + + if tc.expectedDetails.GetClearTextFailure() != nil { + require.NotNil(t, result.ClearTextFailure) + require.Equal(t, tc.expectedDetails. + ClearTextFailure.WireMessage, + result.ClearTextFailure.WireMessage) + } else { + require.Nil(t, result.ClearTextFailure) + } + }) + } +} diff --git a/lntest/rpc/switch.go b/lntest/rpc/switch.go index e69b899a77..1e08dfe2b6 100644 --- a/lntest/rpc/switch.go +++ b/lntest/rpc/switch.go @@ -10,19 +10,18 @@ import ( // SwitchClient related RPCs. // ===================== -// SendOnion makes a RPC call to SendOnion and asserts. +// SendOnion makes a RPC call to SendOnion and returns the error. // //nolint:lll func (h *HarnessRPC) SendOnion( - req *switchrpc.SendOnionRequest) *switchrpc.SendOnionResponse { + req *switchrpc.SendOnionRequest) error { ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) defer cancel() - resp, err := h.Switch.SendOnion(ctxt, req) - h.NoError(err, "SendOnion") + _, err := h.Switch.SendOnion(ctxt, req) - return resp + return err } // TrackOnion makes a RPC call to TrackOnion and asserts. From c448dd46f1b22398f198b2d114938c4b7aead05e Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Tue, 3 Feb 2026 09:58:11 -0500 Subject: [PATCH 4/4] docs: update v0.21 release notes --- docs/release-notes/release-notes-0.21.0.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index d326292464..abfc32b0a6 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -120,6 +120,15 @@ communicated via standard gRPC status codes. This is a **breaking change** for any clients of the `TrackOnion` RPC. +* The `switchrpc.SendOnion` RPC has been overhauled to provide a more robust, + client-friendly, and forward-compatible API. Failures are no longer reported + in the response body but are instead communicated exclusively via gRPC status + codes with rich, structured `SendOnionFailureDetails` attached. The + `ErrorCode` enum has been redesigned to represent actionable client states, + and a new `CLEAR_TEXT_ERROR` code provides forward-compatibility for clients + when new definitive local errors are introduced. This is a **breaking change** + for any clients of the `SendOnion` RPC. + ## lncli Updates ## Breaking Changes