From 04dede0f81cc4e737569020436cfc7ef9a509e88 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 30 Jun 2026 14:09:58 -0700 Subject: [PATCH 1/2] lnrpc/chainrpc: add reorg depth and Done to notifier proto The ChainNotifier confirmation/spend streams previously exposed re-org only as a payload-less event and had no terminal event for reaching re-org safety depth. Add a depth field to the Reorg message and a new Done event to both the ConfEvent and SpendEvent oneofs so the gRPC surface can carry the same information lnd already tracks internally (ConfirmationEvent.NegativeConf depth and the Done channel). This is an additive, backwards-compatible proto change; the server side is wired in a follow-up commit. --- lnrpc/chainrpc/chainnotifier.pb.go | 196 ++++++++++++++++------ lnrpc/chainrpc/chainnotifier.proto | 30 +++- lnrpc/chainrpc/chainnotifier.swagger.json | 20 ++- 3 files changed, 193 insertions(+), 53 deletions(-) diff --git a/lnrpc/chainrpc/chainnotifier.pb.go b/lnrpc/chainrpc/chainnotifier.pb.go index 41785b60581..721a6e47e32 100644 --- a/lnrpc/chainrpc/chainnotifier.pb.go +++ b/lnrpc/chainrpc/chainnotifier.pb.go @@ -195,7 +195,12 @@ func (x *ConfDetails) GetRawBlock() []byte { } type Reorg struct { - state protoimpl.MessageState `protogen:"open.v1"` + state protoimpl.MessageState `protogen:"open.v1"` + // The depth of the re-org, i.e. the number of blocks by which the + // transaction/spend was reorged out of the chain. Only populated for + // confirmation notifications; spend notifications do not currently track + // re-org depth and leave this field as 0. + Depth uint32 `protobuf:"varint,1,opt,name=depth,proto3" json:"depth,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -230,12 +235,56 @@ func (*Reorg) Descriptor() ([]byte, []int) { return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{2} } +func (x *Reorg) GetDepth() uint32 { + if x != nil { + return x.Depth + } + return 0 +} + +type Done struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Done) Reset() { + *x = Done{} + mi := &file_chainrpc_chainnotifier_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Done) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Done) ProtoMessage() {} + +func (x *Done) ProtoReflect() protoreflect.Message { + mi := &file_chainrpc_chainnotifier_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Done.ProtoReflect.Descriptor instead. +func (*Done) Descriptor() ([]byte, []int) { + return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{3} +} + type ConfEvent struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Event: // // *ConfEvent_Conf // *ConfEvent_Reorg + // *ConfEvent_Done Event isConfEvent_Event `protobuf_oneof:"event"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -243,7 +292,7 @@ type ConfEvent struct { func (x *ConfEvent) Reset() { *x = ConfEvent{} - mi := &file_chainrpc_chainnotifier_proto_msgTypes[3] + mi := &file_chainrpc_chainnotifier_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -255,7 +304,7 @@ func (x *ConfEvent) String() string { func (*ConfEvent) ProtoMessage() {} func (x *ConfEvent) ProtoReflect() protoreflect.Message { - mi := &file_chainrpc_chainnotifier_proto_msgTypes[3] + mi := &file_chainrpc_chainnotifier_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -268,7 +317,7 @@ func (x *ConfEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfEvent.ProtoReflect.Descriptor instead. func (*ConfEvent) Descriptor() ([]byte, []int) { - return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{3} + return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{4} } func (x *ConfEvent) GetEvent() isConfEvent_Event { @@ -296,6 +345,15 @@ func (x *ConfEvent) GetReorg() *Reorg { return nil } +func (x *ConfEvent) GetDone() *Done { + if x != nil { + if x, ok := x.Event.(*ConfEvent_Done); ok { + return x.Done + } + } + return nil +} + type isConfEvent_Event interface { isConfEvent_Event() } @@ -312,10 +370,19 @@ type ConfEvent_Reorg struct { Reorg *Reorg `protobuf:"bytes,2,opt,name=reorg,proto3,oneof"` } +type ConfEvent_Done struct { + // An event sent once the confirmation is no longer under the risk of + // being reorged out of the chain (the watch has reached re-org safety + // depth). + Done *Done `protobuf:"bytes,3,opt,name=done,proto3,oneof"` +} + func (*ConfEvent_Conf) isConfEvent_Event() {} func (*ConfEvent_Reorg) isConfEvent_Event() {} +func (*ConfEvent_Done) isConfEvent_Event() {} + type Outpoint struct { state protoimpl.MessageState `protogen:"open.v1"` // The hash of the transaction. @@ -328,7 +395,7 @@ type Outpoint struct { func (x *Outpoint) Reset() { *x = Outpoint{} - mi := &file_chainrpc_chainnotifier_proto_msgTypes[4] + mi := &file_chainrpc_chainnotifier_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -340,7 +407,7 @@ func (x *Outpoint) String() string { func (*Outpoint) ProtoMessage() {} func (x *Outpoint) ProtoReflect() protoreflect.Message { - mi := &file_chainrpc_chainnotifier_proto_msgTypes[4] + mi := &file_chainrpc_chainnotifier_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -353,7 +420,7 @@ func (x *Outpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use Outpoint.ProtoReflect.Descriptor instead. func (*Outpoint) Descriptor() ([]byte, []int) { - return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{4} + return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{5} } func (x *Outpoint) GetHash() []byte { @@ -394,7 +461,7 @@ type SpendRequest struct { func (x *SpendRequest) Reset() { *x = SpendRequest{} - mi := &file_chainrpc_chainnotifier_proto_msgTypes[5] + mi := &file_chainrpc_chainnotifier_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -406,7 +473,7 @@ func (x *SpendRequest) String() string { func (*SpendRequest) ProtoMessage() {} func (x *SpendRequest) ProtoReflect() protoreflect.Message { - mi := &file_chainrpc_chainnotifier_proto_msgTypes[5] + mi := &file_chainrpc_chainnotifier_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -419,7 +486,7 @@ func (x *SpendRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SpendRequest.ProtoReflect.Descriptor instead. func (*SpendRequest) Descriptor() ([]byte, []int) { - return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{5} + return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{6} } func (x *SpendRequest) GetOutpoint() *Outpoint { @@ -461,7 +528,7 @@ type SpendDetails struct { func (x *SpendDetails) Reset() { *x = SpendDetails{} - mi := &file_chainrpc_chainnotifier_proto_msgTypes[6] + mi := &file_chainrpc_chainnotifier_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -473,7 +540,7 @@ func (x *SpendDetails) String() string { func (*SpendDetails) ProtoMessage() {} func (x *SpendDetails) ProtoReflect() protoreflect.Message { - mi := &file_chainrpc_chainnotifier_proto_msgTypes[6] + mi := &file_chainrpc_chainnotifier_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -486,7 +553,7 @@ func (x *SpendDetails) ProtoReflect() protoreflect.Message { // Deprecated: Use SpendDetails.ProtoReflect.Descriptor instead. func (*SpendDetails) Descriptor() ([]byte, []int) { - return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{6} + return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{7} } func (x *SpendDetails) GetSpendingOutpoint() *Outpoint { @@ -530,6 +597,7 @@ type SpendEvent struct { // // *SpendEvent_Spend // *SpendEvent_Reorg + // *SpendEvent_Done Event isSpendEvent_Event `protobuf_oneof:"event"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -537,7 +605,7 @@ type SpendEvent struct { func (x *SpendEvent) Reset() { *x = SpendEvent{} - mi := &file_chainrpc_chainnotifier_proto_msgTypes[7] + mi := &file_chainrpc_chainnotifier_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -549,7 +617,7 @@ func (x *SpendEvent) String() string { func (*SpendEvent) ProtoMessage() {} func (x *SpendEvent) ProtoReflect() protoreflect.Message { - mi := &file_chainrpc_chainnotifier_proto_msgTypes[7] + mi := &file_chainrpc_chainnotifier_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -562,7 +630,7 @@ func (x *SpendEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use SpendEvent.ProtoReflect.Descriptor instead. func (*SpendEvent) Descriptor() ([]byte, []int) { - return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{7} + return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{8} } func (x *SpendEvent) GetEvent() isSpendEvent_Event { @@ -590,6 +658,15 @@ func (x *SpendEvent) GetReorg() *Reorg { return nil } +func (x *SpendEvent) GetDone() *Done { + if x != nil { + if x, ok := x.Event.(*SpendEvent_Done); ok { + return x.Done + } + } + return nil +} + type isSpendEvent_Event interface { isSpendEvent_Event() } @@ -606,10 +683,18 @@ type SpendEvent_Reorg struct { Reorg *Reorg `protobuf:"bytes,2,opt,name=reorg,proto3,oneof"` } +type SpendEvent_Done struct { + // An event sent once the spend is no longer under the risk of being + // reorged out of the chain (the watch has reached re-org safety depth). + Done *Done `protobuf:"bytes,3,opt,name=done,proto3,oneof"` +} + func (*SpendEvent_Spend) isSpendEvent_Event() {} func (*SpendEvent_Reorg) isSpendEvent_Event() {} +func (*SpendEvent_Done) isSpendEvent_Event() {} + type BlockEpoch struct { state protoimpl.MessageState `protogen:"open.v1"` // The hash of the block. @@ -622,7 +707,7 @@ type BlockEpoch struct { func (x *BlockEpoch) Reset() { *x = BlockEpoch{} - mi := &file_chainrpc_chainnotifier_proto_msgTypes[8] + mi := &file_chainrpc_chainnotifier_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -634,7 +719,7 @@ func (x *BlockEpoch) String() string { func (*BlockEpoch) ProtoMessage() {} func (x *BlockEpoch) ProtoReflect() protoreflect.Message { - mi := &file_chainrpc_chainnotifier_proto_msgTypes[8] + mi := &file_chainrpc_chainnotifier_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -647,7 +732,7 @@ func (x *BlockEpoch) ProtoReflect() protoreflect.Message { // Deprecated: Use BlockEpoch.ProtoReflect.Descriptor instead. func (*BlockEpoch) Descriptor() ([]byte, []int) { - return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{8} + return file_chainrpc_chainnotifier_proto_rawDescGZIP(), []int{9} } func (x *BlockEpoch) GetHash() []byte { @@ -682,11 +767,14 @@ const file_chainrpc_chainnotifier_proto_rawDesc = "" + "block_hash\x18\x02 \x01(\fR\tblockHash\x12!\n" + "\fblock_height\x18\x03 \x01(\rR\vblockHeight\x12\x19\n" + "\btx_index\x18\x04 \x01(\rR\atxIndex\x12\x1b\n" + - "\traw_block\x18\x05 \x01(\fR\brawBlock\"\a\n" + - "\x05Reorg\"j\n" + + "\traw_block\x18\x05 \x01(\fR\brawBlock\"\x1d\n" + + "\x05Reorg\x12\x14\n" + + "\x05depth\x18\x01 \x01(\rR\x05depth\"\x06\n" + + "\x04Done\"\x90\x01\n" + "\tConfEvent\x12+\n" + "\x04conf\x18\x01 \x01(\v2\x15.chainrpc.ConfDetailsH\x00R\x04conf\x12'\n" + - "\x05reorg\x18\x02 \x01(\v2\x0f.chainrpc.ReorgH\x00R\x05reorgB\a\n" + + "\x05reorg\x18\x02 \x01(\v2\x0f.chainrpc.ReorgH\x00R\x05reorg\x12$\n" + + "\x04done\x18\x03 \x01(\v2\x0e.chainrpc.DoneH\x00R\x04doneB\a\n" + "\x05event\"4\n" + "\bOutpoint\x12\x12\n" + "\x04hash\x18\x01 \x01(\fR\x04hash\x12\x14\n" + @@ -701,11 +789,12 @@ const file_chainrpc_chainnotifier_proto_rawDesc = "" + "\x0fraw_spending_tx\x18\x02 \x01(\fR\rrawSpendingTx\x12(\n" + "\x10spending_tx_hash\x18\x03 \x01(\fR\x0espendingTxHash\x120\n" + "\x14spending_input_index\x18\x04 \x01(\rR\x12spendingInputIndex\x12'\n" + - "\x0fspending_height\x18\x05 \x01(\rR\x0espendingHeight\"n\n" + + "\x0fspending_height\x18\x05 \x01(\rR\x0espendingHeight\"\x94\x01\n" + "\n" + "SpendEvent\x12.\n" + "\x05spend\x18\x01 \x01(\v2\x16.chainrpc.SpendDetailsH\x00R\x05spend\x12'\n" + - "\x05reorg\x18\x02 \x01(\v2\x0f.chainrpc.ReorgH\x00R\x05reorgB\a\n" + + "\x05reorg\x18\x02 \x01(\v2\x0f.chainrpc.ReorgH\x00R\x05reorg\x12$\n" + + "\x04done\x18\x03 \x01(\v2\x0e.chainrpc.DoneH\x00R\x04doneB\a\n" + "\x05event\"8\n" + "\n" + "BlockEpoch\x12\x12\n" + @@ -728,36 +817,39 @@ func file_chainrpc_chainnotifier_proto_rawDescGZIP() []byte { return file_chainrpc_chainnotifier_proto_rawDescData } -var file_chainrpc_chainnotifier_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_chainrpc_chainnotifier_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_chainrpc_chainnotifier_proto_goTypes = []any{ (*ConfRequest)(nil), // 0: chainrpc.ConfRequest (*ConfDetails)(nil), // 1: chainrpc.ConfDetails (*Reorg)(nil), // 2: chainrpc.Reorg - (*ConfEvent)(nil), // 3: chainrpc.ConfEvent - (*Outpoint)(nil), // 4: chainrpc.Outpoint - (*SpendRequest)(nil), // 5: chainrpc.SpendRequest - (*SpendDetails)(nil), // 6: chainrpc.SpendDetails - (*SpendEvent)(nil), // 7: chainrpc.SpendEvent - (*BlockEpoch)(nil), // 8: chainrpc.BlockEpoch + (*Done)(nil), // 3: chainrpc.Done + (*ConfEvent)(nil), // 4: chainrpc.ConfEvent + (*Outpoint)(nil), // 5: chainrpc.Outpoint + (*SpendRequest)(nil), // 6: chainrpc.SpendRequest + (*SpendDetails)(nil), // 7: chainrpc.SpendDetails + (*SpendEvent)(nil), // 8: chainrpc.SpendEvent + (*BlockEpoch)(nil), // 9: chainrpc.BlockEpoch } var file_chainrpc_chainnotifier_proto_depIdxs = []int32{ - 1, // 0: chainrpc.ConfEvent.conf:type_name -> chainrpc.ConfDetails - 2, // 1: chainrpc.ConfEvent.reorg:type_name -> chainrpc.Reorg - 4, // 2: chainrpc.SpendRequest.outpoint:type_name -> chainrpc.Outpoint - 4, // 3: chainrpc.SpendDetails.spending_outpoint:type_name -> chainrpc.Outpoint - 6, // 4: chainrpc.SpendEvent.spend:type_name -> chainrpc.SpendDetails - 2, // 5: chainrpc.SpendEvent.reorg:type_name -> chainrpc.Reorg - 0, // 6: chainrpc.ChainNotifier.RegisterConfirmationsNtfn:input_type -> chainrpc.ConfRequest - 5, // 7: chainrpc.ChainNotifier.RegisterSpendNtfn:input_type -> chainrpc.SpendRequest - 8, // 8: chainrpc.ChainNotifier.RegisterBlockEpochNtfn:input_type -> chainrpc.BlockEpoch - 3, // 9: chainrpc.ChainNotifier.RegisterConfirmationsNtfn:output_type -> chainrpc.ConfEvent - 7, // 10: chainrpc.ChainNotifier.RegisterSpendNtfn:output_type -> chainrpc.SpendEvent - 8, // 11: chainrpc.ChainNotifier.RegisterBlockEpochNtfn:output_type -> chainrpc.BlockEpoch - 9, // [9:12] is the sub-list for method output_type - 6, // [6:9] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 1, // 0: chainrpc.ConfEvent.conf:type_name -> chainrpc.ConfDetails + 2, // 1: chainrpc.ConfEvent.reorg:type_name -> chainrpc.Reorg + 3, // 2: chainrpc.ConfEvent.done:type_name -> chainrpc.Done + 5, // 3: chainrpc.SpendRequest.outpoint:type_name -> chainrpc.Outpoint + 5, // 4: chainrpc.SpendDetails.spending_outpoint:type_name -> chainrpc.Outpoint + 7, // 5: chainrpc.SpendEvent.spend:type_name -> chainrpc.SpendDetails + 2, // 6: chainrpc.SpendEvent.reorg:type_name -> chainrpc.Reorg + 3, // 7: chainrpc.SpendEvent.done:type_name -> chainrpc.Done + 0, // 8: chainrpc.ChainNotifier.RegisterConfirmationsNtfn:input_type -> chainrpc.ConfRequest + 6, // 9: chainrpc.ChainNotifier.RegisterSpendNtfn:input_type -> chainrpc.SpendRequest + 9, // 10: chainrpc.ChainNotifier.RegisterBlockEpochNtfn:input_type -> chainrpc.BlockEpoch + 4, // 11: chainrpc.ChainNotifier.RegisterConfirmationsNtfn:output_type -> chainrpc.ConfEvent + 8, // 12: chainrpc.ChainNotifier.RegisterSpendNtfn:output_type -> chainrpc.SpendEvent + 9, // 13: chainrpc.ChainNotifier.RegisterBlockEpochNtfn:output_type -> chainrpc.BlockEpoch + 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_chainrpc_chainnotifier_proto_init() } @@ -765,13 +857,15 @@ func file_chainrpc_chainnotifier_proto_init() { if File_chainrpc_chainnotifier_proto != nil { return } - file_chainrpc_chainnotifier_proto_msgTypes[3].OneofWrappers = []any{ + file_chainrpc_chainnotifier_proto_msgTypes[4].OneofWrappers = []any{ (*ConfEvent_Conf)(nil), (*ConfEvent_Reorg)(nil), + (*ConfEvent_Done)(nil), } - file_chainrpc_chainnotifier_proto_msgTypes[7].OneofWrappers = []any{ + file_chainrpc_chainnotifier_proto_msgTypes[8].OneofWrappers = []any{ (*SpendEvent_Spend)(nil), (*SpendEvent_Reorg)(nil), + (*SpendEvent_Done)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -779,7 +873,7 @@ func file_chainrpc_chainnotifier_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_chainrpc_chainnotifier_proto_rawDesc), len(file_chainrpc_chainnotifier_proto_rawDesc)), NumEnums: 0, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/lnrpc/chainrpc/chainnotifier.proto b/lnrpc/chainrpc/chainnotifier.proto index 36e66a279ef..1c1e5c99000 100644 --- a/lnrpc/chainrpc/chainnotifier.proto +++ b/lnrpc/chainrpc/chainnotifier.proto @@ -102,7 +102,22 @@ message ConfDetails { } message Reorg { - // TODO(wilmer): need to know how the client will use this first. + /* + The depth of the re-org, i.e. the number of blocks by which the + transaction/spend was reorged out of the chain. Only populated for + confirmation notifications; spend notifications do not currently track + re-org depth and leave this field as 0. + */ + uint32 depth = 1; +} + +message Done { + /* + Done is sent once the confirmation/spend notification is no longer under + the risk of being reorged out of the chain, i.e. it has reached the + backend's re-org safety depth. It carries no payload: receiving it means + the notification stream is complete and no further events will be sent. + */ } message ConfEvent { @@ -118,6 +133,13 @@ message ConfEvent { chain. */ Reorg reorg = 2; + + /* + An event sent once the confirmation is no longer under the risk of + being reorged out of the chain (the watch has reached re-org safety + depth). + */ + Done done = 3; } } @@ -188,6 +210,12 @@ message SpendEvent { reorged out of the chain. */ Reorg reorg = 2; + + /* + An event sent once the spend is no longer under the risk of being + reorged out of the chain (the watch has reached re-org safety depth). + */ + Done done = 3; } } diff --git a/lnrpc/chainrpc/chainnotifier.swagger.json b/lnrpc/chainrpc/chainnotifier.swagger.json index b8e2924cdb7..9b151dbc654 100644 --- a/lnrpc/chainrpc/chainnotifier.swagger.json +++ b/lnrpc/chainrpc/chainnotifier.swagger.json @@ -202,6 +202,10 @@ "reorg": { "$ref": "#/definitions/chainrpcReorg", "description": "An event send when the transaction of the request is reorged out of the\nchain." + }, + "done": { + "$ref": "#/definitions/chainrpcDone", + "description": "An event sent once the confirmation is no longer under the risk of\nbeing reorged out of the chain (the watch has reached re-org safety\ndepth)." } } }, @@ -234,6 +238,10 @@ } } }, + "chainrpcDone": { + "type": "object", + "description": "Done is sent once the confirmation/spend notification is no longer under\nthe risk of being reorged out of the chain, i.e. it has reached the\nbackend's re-org safety depth. It carries no payload: receiving it means\nthe notification stream is complete and no further events will be sent." + }, "chainrpcOutpoint": { "type": "object", "properties": { @@ -251,7 +259,13 @@ }, "chainrpcReorg": { "type": "object", - "description": "TODO(wilmer): need to know how the client will use this first." + "properties": { + "depth": { + "type": "integer", + "format": "int64", + "description": "The depth of the re-org, i.e. the number of blocks by which the\ntransaction/spend was reorged out of the chain. Only populated for\nconfirmation notifications; spend notifications do not currently track\nre-org depth and leave this field as 0." + } + } }, "chainrpcSpendDetails": { "type": "object", @@ -292,6 +306,10 @@ "reorg": { "$ref": "#/definitions/chainrpcReorg", "description": "An event sent when the spending transaction of the request was\nreorged out of the chain." + }, + "done": { + "$ref": "#/definitions/chainrpcDone", + "description": "An event sent once the spend is no longer under the risk of being\nreorged out of the chain (the watch has reached re-org safety depth)." } } }, From 8b4da077a4313bd00315c1743f9f05bab8e06f59 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 30 Jun 2026 14:09:59 -0700 Subject: [PATCH 2/2] chainrpc: forward reorg depth and emit Done from notifier server The RegisterConfirmationsNtfn/RegisterSpendNtfn handlers read the internal NegativeConf depth and Done channels but dropped both at the gRPC boundary: the re-org depth was discarded and Done was conveyed only by returning (closing the stream), which a caller cannot distinguish from an unrelated teardown. Forward the re-org depth on the confirmation path's Reorg message and send an explicit Done event before exiting on both the confirmation and spend paths. Spend-side re-org depth is left as 0 because the notifier does not currently track it internally. --- docs/release-notes/release-notes-0.22.0.md | 8 +++++ lnrpc/chainrpc/chain_server.go | 34 ++++++++++++++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/docs/release-notes/release-notes-0.22.0.md b/docs/release-notes/release-notes-0.22.0.md index 260c099f1f2..dc784cfb65e 100644 --- a/docs/release-notes/release-notes-0.22.0.md +++ b/docs/release-notes/release-notes-0.22.0.md @@ -77,6 +77,14 @@ ## RPC Updates +* The `ChainNotifier` confirmation and spend notification streams are now + [richer at the re-org boundary](https://github.com/lightningnetwork/lnd/pull/10943): + the `Reorg` message carries the re-org `depth` (populated for confirmation + notifications), and a new `Done` event is sent once a watch reaches the + backend's re-org safety depth. Previously the re-org depth was dropped and the + done signal was conveyed only by closing the stream, which callers could not + distinguish from an unrelated teardown. + ## lncli Updates ## Breaking Changes diff --git a/lnrpc/chainrpc/chain_server.go b/lnrpc/chainrpc/chain_server.go index 690cd817727..1b1ba135b6c 100644 --- a/lnrpc/chainrpc/chain_server.go +++ b/lnrpc/chainrpc/chain_server.go @@ -439,27 +439,40 @@ func (s *Server) RegisterConfirmationsNtfn(in *ConfRequest, } // The transaction satisfying the request has been reorged out - // of the chain, so we'll send an event describing it. - case _, ok := <-confEvent.NegativeConf: + // of the chain, so we'll send an event describing it. The depth + // of the re-org is forwarded so reorg-safety-aware callers can + // reason about how far the chain rewound. + case depth, ok := <-confEvent.NegativeConf: if !ok { return chainntnfs.ErrChainNotifierShuttingDown } reorg := &ConfEvent{ - Event: &ConfEvent_Reorg{Reorg: &Reorg{}}, + Event: &ConfEvent_Reorg{ + Reorg: &Reorg{Depth: uint32(depth)}, + }, } if err := confStream.Send(reorg); err != nil { return err } // The transaction satisfying the request has confirmed and is - // no longer under the risk of being reorged out of the chain, - // so we can safely exit. + // no longer under the risk of being reorged out of the chain. + // Send an explicit Done event before exiting so callers can + // distinguish reaching re-org safety depth from the stream + // being torn down for any other reason. case _, ok := <-confEvent.Done: if !ok { return chainntnfs.ErrChainNotifierShuttingDown } + done := &ConfEvent{ + Event: &ConfEvent_Done{Done: &Done{}}, + } + if err := confStream.Send(done); err != nil { + return err + } + return nil // The response stream's context for whatever reason has been @@ -565,12 +578,21 @@ func (s *Server) RegisterSpendNtfn(in *SpendRequest, // The spending transaction of the requests has confirmed // on-chain and is no longer under the risk of being reorged out - // of the chain, so we can safely exit. + // of the chain. Send an explicit Done event before exiting so + // callers can distinguish reaching re-org safety depth from the + // stream being torn down for any other reason. case _, ok := <-spendEvent.Done: if !ok { return chainntnfs.ErrChainNotifierShuttingDown } + done := &SpendEvent{ + Event: &SpendEvent_Done{Done: &Done{}}, + } + if err := spendStream.Send(done); err != nil { + return err + } + return nil // The response stream's context for whatever reason has been