diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index 8dc5aba7c3..23d7102ec7 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -99,6 +99,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 diff --git a/itest/lnd_sendonion_test.go b/itest/lnd_sendonion_test.go index bff161eeaa..d1e67827ea 100644 --- a/itest/lnd_sendonion_test.go +++ b/itest/lnd_sendonion_test.go @@ -93,7 +93,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) @@ -200,7 +200,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. @@ -275,9 +275,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. @@ -288,10 +285,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. @@ -300,16 +298,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) } @@ -323,14 +323,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..137320cc61 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,30 +431,279 @@ 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 + + // The ErrorCode provides a high-level classification of the failure. + ErrorCode ErrorCode `protobuf:"varint,1,opt,name=error_code,json=errorCode,proto3,enum=switchrpc.ErrorCode" json:"error_code,omitempty"` + // A human-readable error_message for logging or display. + ErrorMessage string `protobuf:"bytes,2,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 + 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) GetErrorCode() ErrorCode { + if x != nil { + return x.ErrorCode + } + return ErrorCode_UNSPECIFIED +} + +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 +} + +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,3,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,4,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,5,opt,name=encrypted_error_data,json=encryptedErrorData,proto3,oneof"` +} + +func (*FailureDetails_ClearTextFailure) isFailureDetails_Failure() {} + +func (*FailureDetails_ForwardingFailure) isFailureDetails_Failure() {} + +func (*FailureDetails_EncryptedErrorData) 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 } @@ -482,7 +727,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[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -495,7 +740,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[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -508,7 +753,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{7} } func (x *BuildOnionRequest) GetRoute() *lnrpc.Route { @@ -550,7 +795,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[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -563,7 +808,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[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -576,7 +821,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{8} } func (x *BuildOnionResponse) GetOnionBlob() []byte { @@ -658,66 +903,93 @@ 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, + 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, 0xc5, 0x02, 0x0a, + 0x0e, 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, 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, - 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, + 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, 0x4b, 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, 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, 0x04, 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, 0x05, 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, 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, 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, } var ( @@ -733,34 +1005,40 @@ 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, 10) 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 + (*BuildOnionRequest)(nil), // 8: switchrpc.BuildOnionRequest + (*BuildOnionResponse)(nil), // 9: switchrpc.BuildOnionResponse + nil, // 10: switchrpc.SendOnionRequest.CustomRecordsEntry + (*lnrpc.Route)(nil), // 11: 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 + 10, // 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 + 0, // 3: switchrpc.FailureDetails.error_code:type_name -> switchrpc.ErrorCode + 7, // 4: switchrpc.FailureDetails.clear_text_failure:type_name -> switchrpc.ClearTextFailure + 6, // 5: switchrpc.FailureDetails.forwarding_failure:type_name -> switchrpc.ForwardingFailure + 11, // 6: switchrpc.BuildOnionRequest.route:type_name -> lnrpc.Route + 1, // 7: switchrpc.Switch.SendOnion:input_type -> switchrpc.SendOnionRequest + 3, // 8: switchrpc.Switch.TrackOnion:input_type -> switchrpc.TrackOnionRequest + 8, // 9: switchrpc.Switch.BuildOnion:input_type -> switchrpc.BuildOnionRequest + 2, // 10: switchrpc.Switch.SendOnion:output_type -> switchrpc.SendOnionResponse + 4, // 11: switchrpc.Switch.TrackOnion:output_type -> switchrpc.TrackOnionResponse + 9, // 12: switchrpc.Switch.BuildOnion:output_type -> switchrpc.BuildOnionResponse + 10, // [10:13] is the sub-list for method output_type + 7, // [7:10] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_switchrpc_switch_proto_init() } @@ -818,7 +1096,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 +1108,42 @@ 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.(*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[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BuildOnionResponse); i { case 0: return &v.state @@ -844,14 +1158,23 @@ 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), + } + file_switchrpc_switch_proto_msgTypes[7].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: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/lnrpc/switchrpc/switch.proto b/lnrpc/switchrpc/switch.proto index 06fa60fbb6..9de406121b 100644 --- a/lnrpc/switchrpc/switch.proto +++ b/lnrpc/switchrpc/switch.proto @@ -160,21 +160,63 @@ 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. +// 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 { + // The ErrorCode provides a high-level classification of the failure. + ErrorCode error_code = 1; + + // A human-readable error_message for logging or display. 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 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 = 3; + + // 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 = 4; + + // encrypted_error_data is returned when the server could not decrypt + // the onion error blob, deferring decryption to the client. + bytes encrypted_error_data = 5; + } +} + +// 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; +} - // 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; +// 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; } // BuildOnionRequest includes the necessary information to construct a Sphinx diff --git a/lnrpc/switchrpc/switch.swagger.json b/lnrpc/switchrpc/switch.swagger.json index 21cc6a9599..cd5c144bed 100644 --- a/lnrpc/switchrpc/switch.swagger.json +++ b/lnrpc/switchrpc/switch.swagger.json @@ -350,6 +350,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": [ @@ -366,6 +377,49 @@ "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_code": { + "$ref": "#/definitions/switchrpcErrorCode", + "description": "The ErrorCode provides a high-level classification of the failure." + }, + "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." + } + }, + "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." + }, "switchrpcSendOnionRequest": { "type": "object", "properties": { @@ -477,18 +531,9 @@ "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." } } } diff --git a/lnrpc/switchrpc/switch_server.go b/lnrpc/switchrpc/switch_server.go index 2f54a11908..5dcace3da6 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" @@ -435,15 +433,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 @@ -456,12 +458,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(): @@ -470,25 +470,30 @@ 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 @@ -497,17 +502,21 @@ func (s *Server) TrackOnion(ctx context.Context, log.Errorf("Payment %v completed without a valid preimage or "+ "error", hash) - return &TrackOnionResponse{ + details := &FailureDetails{ ErrorCode: ErrorCode_INTERNAL, ErrorMessage: ErrAmbiguousPaymentState.Error(), - }, nil + } + + return newTrackOnionFailureResponse(details), nil } 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 } @@ -662,7 +671,6 @@ func (s *Server) BuildOnion(_ context.Context, func translateErrorForRPC(err error) (string, ErrorCode) { var ( clearTextErr htlcswitch.ClearTextError - fwdErr *htlcswitch.ForwardingError ) switch { @@ -680,20 +688,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, @@ -712,48 +706,156 @@ 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, + }, } +} - return fmt.Sprintf("%d@%s", e.FailureSourceIdx, - hex.EncodeToString(buf.Bytes())), nil +// 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.Is(err, htlcswitch.ErrPaymentIDNotFound): + details.ErrorCode = ErrorCode_PAYMENT_ID_NOT_FOUND + + case errors.Is(err, htlcswitch.ErrUnreadableFailureMessage): + details.ErrorCode = ErrorCode_UNREADABLE_FAILURE_MESSAGE + + case errors.Is(err, htlcswitch.ErrSwitchExiting): + details.ErrorCode = ErrorCode_SWITCH_EXITING + + 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.ErrorCode = ErrorCode_INTERNAL + + 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(), + }, + } + } + + default: + details.ErrorCode = ErrorCode_INTERNAL + } + + return details } -// 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) +// UnmarshallFailureDetails translates a FailureDetails message from a +// TrackOnion response into a concrete Go error. It handles all cases of the +// 'oneof failure' field and falls back to the ErrorCode if needed. +func UnmarshallFailureDetails(details *FailureDetails, + deobfuscator htlcswitch.ErrorDecrypter) (error, error) { + + if details == nil { + return nil, errors.New("cannot unmarshall nil FailureDetails") } - idx, err := strconv.Atoi(parts[0]) - if err != nil { - return nil, fmt.Errorf("invalid forwarding error index: %s", - errStr) + // 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) + } + + // If the 'oneof' was not populated, fall back to the error code. + switch details.ErrorCode { + case ErrorCode_UNREADABLE_FAILURE_MESSAGE: + return htlcswitch.ErrUnreadableFailureMessage, nil + case ErrorCode_SWITCH_EXITING: + return htlcswitch.ErrSwitchExiting, nil + } + + // If all else fails, return the generic error message. + return errors.New(details.ErrorMessage), nil +} + +// 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") } - wireMsgBytes, err := hex.DecodeString(parts[1]) + wireMsg, err := UnmarshallFailureMessage(f.WireMessage) if err != nil { - return nil, fmt.Errorf("invalid forwarding error wire "+ - "message: %s", errStr) + return nil, fmt.Errorf("failed to decode wire message: %w", err) + } + + 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 87fcde057d..a3ebcc8163 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" @@ -221,8 +220,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", @@ -234,8 +234,8 @@ 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()) }, }, { @@ -248,9 +248,11 @@ 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.Equal(t, ErrorCode_INTERNAL, details.ErrorCode) + require.Contains(t, details.ErrorMessage, "test error") }, }, { @@ -260,11 +262,8 @@ func TestTrackOnion(t *testing.T) { m.getResultErr = htlcswitch.ErrPaymentIDNotFound }, - getCtx: t.Context, - expectedResponse: &TrackOnionResponse{ - ErrorMessage: htlcswitch.ErrPaymentIDNotFound.Error(), - ErrorCode: ErrorCode_PAYMENT_ID_NOT_FOUND, - }, + getCtx: t.Context, + expectedErrCode: codes.NotFound, }, { name: "invalid payment hash", @@ -314,8 +313,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()) }, }, { @@ -330,11 +332,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", @@ -350,9 +349,12 @@ func TestTrackOnion(t *testing.T) { } }, getCtx: t.Context, - expectedResponse: &TrackOnionResponse{ - ErrorMessage: ErrAmbiguousPaymentState.Error(), - ErrorCode: ErrorCode_INTERNAL, + checkResponse: func(t *testing.T, resp *TrackOnionResponse) { + details := resp.GetFailureDetails() + require.NotNil(t, details) + require.Equal(t, ErrorCode_INTERNAL, details.ErrorCode) + require.Contains(t, details.ErrorMessage, + ErrAmbiguousPaymentState.Error()) }, }, } @@ -391,7 +393,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) }) } } @@ -575,11 +577,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 @@ -613,12 +611,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, @@ -642,87 +634,161 @@ 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: "not found", + err: htlcswitch.ErrPaymentIDNotFound, + expectedDetails: &FailureDetails{ + ErrorCode: ErrorCode_PAYMENT_ID_NOT_FOUND, + ErrorMessage: htlcswitch.ErrPaymentIDNotFound.Error(), + }, }, { - name: "Invalid Format", - errStr: "invalid_format", - expectsError: true, + name: "unreadable", + err: htlcswitch.ErrUnreadableFailureMessage, + expectedDetails: &FailureDetails{ + ErrorCode: ErrorCode_UNREADABLE_FAILURE_MESSAGE, + ErrorMessage: htlcswitch.ErrUnreadableFailureMessage.Error(), + }, }, { - name: "Invalid Index", - errStr: "invalid@" + encodedMsg, - expectsError: true, + name: "clear text error", + err: mockLinkErr, + expectedDetails: &FailureDetails{ + ErrorMessage: mockLinkErr.Error(), + Failure: &FailureDetails_ClearTextFailure{ + ClearTextFailure: &ClearTextFailure{}, + }, + }, }, { - name: "Invalid Wire Message", - errStr: "1@invalid", - expectsError: true, + name: "forwarding error", + err: mockFwdErr, + expectedDetails: &FailureDetails{ + ErrorMessage: mockFwdErr.Error(), + Failure: &FailureDetails_ForwardingFailure{ + ForwardingFailure: &ForwardingFailure{ + FailureSourceIndex: 1, + }, + }, + }, }, } - 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.Equal(t, tc.expectedDetails.ErrorCode, + details.ErrorCode) + 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) + + 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) + exitErr := htlcswitch.ErrSwitchExiting - // Encode the forwarding error. - encodedError, _ := translateErrorForRPC(mockForwardingErr) + testCases := []struct { + name string + originalErr error + }{ + { + name: "forwarding failure", + originalErr: fwdErr, + }, + { + name: "clear text failure", + originalErr: linkErr, + }, + { + name: "switch exiting", + originalErr: exitErr, + }, + } - // Decode the forwarding error. - decodedError, err := ParseForwardingError(encodedError) - require.NoError(t, err, "decoding failed") + 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) - // Assert the decoded error matches the original. - require.Equal(t, mockForwardingErr.FailureSourceIdx, - decodedError.FailureSourceIdx) - require.Equal(t, mockForwardingErr.WireMessage(), - decodedError.WireMessage()) + // Confirm that the final error is of the same type as + // the original. + require.IsType(t, tc.originalErr, translatedErr) + }) + } } // TestBuildErrorDecryptor tests the buildErrorDecryptor function.