diff --git a/sdk/swaps/client.go b/sdk/swaps/client.go index 9dde516ec..87a0c16c8 100644 --- a/sdk/swaps/client.go +++ b/sdk/swaps/client.go @@ -14,6 +14,7 @@ import ( "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" ) // SwapDirection identifies which Lightning/Ark direction one persisted swap @@ -264,6 +265,23 @@ type OutSwapHtlcEvent struct { // VHTLCConfig contains the script parameters for the funded vHTLC. VHTLCConfig VHTLCConfig + + // Parts lists the individual HTLC shards of a multi-part payment set. + // When empty the event is a legacy single-part payment carried by + // OnionBlob. + Parts []OutSwapHtlcPart +} + +// OutSwapHtlcPart carries one HTLC shard of a multi-part out-swap payment +// set. Each shard has its own final-hop onion that the client validates +// before acknowledging the event. +type OutSwapHtlcPart struct { + // AmountMsat is the millisatoshi amount forwarded by this shard. + AmountMsat lnwire.MilliSatoshi + + // OnionBlob is the raw final-hop onion blob forwarded by the server + // for this shard. + OnionBlob []byte } // OutSwapHtlcNotification carries one mailbox-delivered out-swap HTLC event diff --git a/sdk/swaps/grpc_conn.go b/sdk/swaps/grpc_conn.go index 0adb74d98..987cd2830 100644 --- a/sdk/swaps/grpc_conn.go +++ b/sdk/swaps/grpc_conn.go @@ -10,6 +10,7 @@ import ( "github.com/lightninglabs/darepo-client/rpc/restclient" "github.com/lightninglabs/darepo-client/swaprpc" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" "google.golang.org/grpc" ) @@ -271,6 +272,25 @@ func outSwapHtlcEventFromProto(event *swaprpc.OutSwapHtlcEvent) ( return nil, err } + var parts []OutSwapHtlcPart + for _, part := range event.GetParts() { + if part == nil { + return nil, fmt.Errorf("out-swap event part must be " + + "provided") + } + if len(part.GetOnionBlob()) == 0 { + return nil, fmt.Errorf("out-swap event part missing " + + "onion blob") + } + + parts = append(parts, OutSwapHtlcPart{ + AmountMsat: lnwire.MilliSatoshi(part.GetAmountMsat()), + OnionBlob: append( + []byte(nil), part.GetOnionBlob()..., + ), + }) + } + return &OutSwapHtlcEvent{ PaymentHash: paymentHash, AmountSat: int64(event.GetAmountSat()), @@ -278,6 +298,7 @@ func outSwapHtlcEventFromProto(event *swaprpc.OutSwapHtlcEvent) ( []byte(nil), event.GetOnionBlob()..., ), VHTLCConfig: *cfg, + Parts: parts, }, nil } diff --git a/sdk/swaps/invoice.go b/sdk/swaps/invoice.go index 9a883a13c..1a5163a05 100644 --- a/sdk/swaps/invoice.go +++ b/sdk/swaps/invoice.go @@ -146,6 +146,7 @@ func genInvoiceCfg(nodeSigner *netann.NodeSigner, lnwire.NewRawFeatureVector( lnwire.TLVOnionPayloadRequired, lnwire.PaymentAddrRequired, + lnwire.MPPOptional, ), lnwire.Features, ) diff --git a/sdk/swaps/invoice_test.go b/sdk/swaps/invoice_test.go index f63679254..cbea7a5b0 100644 --- a/sdk/swaps/invoice_test.go +++ b/sdk/swaps/invoice_test.go @@ -103,7 +103,8 @@ func TestInvoiceGeneratorIncludesPaymentAddress(t *testing.T) { } // TestInvoiceGeneratorPreservesPayerFeeRouteHint verifies receive invoices -// encode the payer-paid route fee and keep multi-part payments disabled. +// encode the payer-paid route fee and advertise optional multi-part +// payments. func TestInvoiceGeneratorPreservesPayerFeeRouteHint(t *testing.T) { t.Parallel() @@ -141,6 +142,6 @@ func TestInvoiceGeneratorPreservesPayerFeeRouteHint(t *testing.T) { hop := decoded.RouteHints[0][0] require.Equal(t, uint32(0), hop.FeeBaseMSat) require.Equal(t, uint32(10_000), hop.FeeProportionalMillionths) - require.False(t, decoded.Features.HasFeature(lnwire.MPPOptional)) - require.False(t, decoded.Features.HasFeature(lnwire.MPPRequired)) + require.True(t, decoded.Features.IsSet(lnwire.MPPOptional)) + require.False(t, decoded.Features.IsSet(lnwire.MPPRequired)) } diff --git a/sdk/swaps/out_swap.go b/sdk/swaps/out_swap.go index 13d6b1452..3619962f1 100644 --- a/sdk/swaps/out_swap.go +++ b/sdk/swaps/out_swap.go @@ -1156,8 +1156,10 @@ func (s *ReceiveSession) acceptInArkHtlcEvent(ctx context.Context, }) } -// validateOnionPayload decodes the final-hop onion with the invoice auth key -// and checks that it matches the prepared invoice fields. +// validateOnionPayload decodes the final-hop onion of every payment part with +// the invoice auth key and checks that each matches the prepared invoice +// fields. Events without parts carry one legacy single-part onion that must +// forward the full invoice amount on its own. func (s *ReceiveSession) validateOnionPayload(event *OutSwapHtlcEvent, authKey ReceiveAuthKey) error { @@ -1165,30 +1167,63 @@ func (s *ReceiveSession) validateOnionPayload(event *OutSwapHtlcEvent, return fmt.Errorf("receive auth key must be provided") } - decoder := s.client.decodeOutSwapOnion - payload, err := decoder( - authKey, s.PaymentHash, event.OnionBlob, - ) - if err != nil { - return err - } - expectedMsat := lnwire.NewMSatFromSatoshis(s.amountSat) - if payload.amountToForward != expectedMsat { - return fmt.Errorf("onion amount %d msat does not match "+ - "invoice amount %d msat", payload.amountToForward, - expectedMsat) - } - if !payload.hasMPP { - return fmt.Errorf("onion missing MPP payment address") - } - if payload.paymentAddr != s.paymentAddr { - return fmt.Errorf("onion payment address mismatch") + + // Legacy single-part events carry the lone onion in the event body and + // the shard must forward the full invoice amount. + parts := event.Parts + if len(parts) == 0 { + parts = []OutSwapHtlcPart{{ + AmountMsat: expectedMsat, + OnionBlob: event.OnionBlob, + }} + } + + // Every shard must commit to the invoice payment address and total, + // while individual forwarded amounts only need to sum to the total. + var sumMsat lnwire.MilliSatoshi + for idx, part := range parts { + payload, err := s.client.decodeOutSwapOnion( + authKey, s.PaymentHash, part.OnionBlob, + ) + if err != nil { + return fmt.Errorf("part %d: %w", idx, err) + } + + if payload.amountToForward == 0 { + return fmt.Errorf("part %d: onion forwards zero amount", + idx) + } + if payload.amountToForward > expectedMsat { + return fmt.Errorf("part %d: onion amount %d msat "+ + "exceeds invoice amount %d msat", idx, + payload.amountToForward, expectedMsat) + } + if payload.amountToForward != part.AmountMsat { + return fmt.Errorf("part %d: onion amount %d msat does "+ + "not match part amount %d msat", idx, + payload.amountToForward, part.AmountMsat) + } + if !payload.hasMPP { + return fmt.Errorf("part %d: onion missing MPP "+ + "payment address", idx) + } + if payload.paymentAddr != s.paymentAddr { + return fmt.Errorf("part %d: onion payment address "+ + "mismatch", idx) + } + if payload.totalAmount != expectedMsat { + return fmt.Errorf("part %d: onion total amount %d "+ + "msat does not match invoice amount %d msat", + idx, payload.totalAmount, expectedMsat) + } + + sumMsat += payload.amountToForward } - if payload.totalAmount != expectedMsat { - return fmt.Errorf("onion total amount %d msat does not match "+ - "invoice amount %d msat", payload.totalAmount, - expectedMsat) + + if sumMsat != expectedMsat { + return fmt.Errorf("onion amounts sum to %d msat, invoice "+ + "amount is %d msat", sumMsat, expectedMsat) } return nil diff --git a/sdk/swaps/out_swap_test.go b/sdk/swaps/out_swap_test.go index 28e5aa26a..d29c4057c 100644 --- a/sdk/swaps/out_swap_test.go +++ b/sdk/swaps/out_swap_test.go @@ -505,6 +505,285 @@ func useTestOnionDecoder(client *SwapClient, amount btcutil.Amount) { } } +// useMappedOnionDecoder installs a deterministic final-hop onion decoder that +// returns one payload per test onion blob. +func useMappedOnionDecoder(client *SwapClient, + payloads map[string]*decodedOutSwapOnion) { + + client.decodeOutSwapOnion = func(_ ReceiveAuthKey, _ lntypes.Hash, + onion []byte) (*decodedOutSwapOnion, error) { + + payload, ok := payloads[string(onion)] + if !ok { + return nil, fmt.Errorf("unexpected onion %x", onion) + } + + return payload, nil + } +} + +// TestReceiveSessionValidateOnionPayloadAcceptsMPPParts verifies the client +// accepts a complete multi-part payment set where each shard forwards less +// than the invoice amount but all shards commit to the same MPP total. +func TestReceiveSessionValidateOnionPayloadAcceptsMPPParts(t *testing.T) { + t.Parallel() + + var paymentAddr [32]byte + copy(paymentAddr[:], bytes.Repeat([]byte{7}, 32)) + + client := &SwapClient{} + useMappedOnionDecoder(client, map[string]*decodedOutSwapOnion{ + "a": { + amountToForward: 21_000_000, + totalAmount: 42_000_000, + paymentAddr: paymentAddr, + hasMPP: true, + }, + "b": { + amountToForward: 21_000_000, + totalAmount: 42_000_000, + paymentAddr: paymentAddr, + hasMPP: true, + }, + }) + + session := &ReceiveSession{ + client: client, + amountSat: 42_000, + paymentAddr: paymentAddr, + } + event := &OutSwapHtlcEvent{ + Parts: []OutSwapHtlcPart{{ + AmountMsat: 21_000_000, + OnionBlob: []byte("a"), + }, { + AmountMsat: 21_000_000, + OnionBlob: []byte("b"), + }}, + } + + err := session.validateOnionPayload(event, &daemonReceiveAuthKey{}) + require.NoError(t, err) +} + +// TestReceiveSessionValidateOnionPayloadKeepsLegacySinglePart verifies an +// event without explicit parts still validates the legacy single-onion field. +func TestReceiveSessionValidateOnionPayloadKeepsLegacySinglePart(t *testing.T) { + t.Parallel() + + var paymentAddr [32]byte + copy(paymentAddr[:], bytes.Repeat([]byte{8}, 32)) + + client := &SwapClient{} + useMappedOnionDecoder(client, map[string]*decodedOutSwapOnion{ + "legacy": { + amountToForward: 42_000_000, + totalAmount: 42_000_000, + paymentAddr: paymentAddr, + hasMPP: true, + }, + }) + + session := &ReceiveSession{ + client: client, + amountSat: 42_000, + paymentAddr: paymentAddr, + } + event := &OutSwapHtlcEvent{ + OnionBlob: []byte("legacy"), + } + + err := session.validateOnionPayload(event, &daemonReceiveAuthKey{}) + require.NoError(t, err) +} + +// TestReceiveSessionValidateOnionPayloadRejectsLegacyUnderpay verifies a +// legacy single-onion event must still forward the full invoice amount on +// its own. +func TestReceiveSessionValidateOnionPayloadRejectsLegacyUnderpay(t *testing.T) { + t.Parallel() + + var paymentAddr [32]byte + copy(paymentAddr[:], bytes.Repeat([]byte{8}, 32)) + + client := &SwapClient{} + useMappedOnionDecoder(client, map[string]*decodedOutSwapOnion{ + "legacy": { + amountToForward: 41_000_000, + totalAmount: 42_000_000, + paymentAddr: paymentAddr, + hasMPP: true, + }, + }) + + session := &ReceiveSession{ + client: client, + amountSat: 42_000, + paymentAddr: paymentAddr, + } + event := &OutSwapHtlcEvent{ + OnionBlob: []byte("legacy"), + } + + err := session.validateOnionPayload(event, &daemonReceiveAuthKey{}) + require.ErrorContains(t, err, "does not match part amount") +} + +// TestReceiveSessionValidateOnionPayloadRejectsBadMPPParts verifies malformed +// MPP sets are rejected before the receiver acknowledges the server event. +func TestReceiveSessionValidateOnionPayloadRejectsBadMPPParts(t *testing.T) { + t.Parallel() + + var paymentAddr [32]byte + copy(paymentAddr[:], bytes.Repeat([]byte{9}, 32)) + + var otherAddr [32]byte + copy(otherAddr[:], bytes.Repeat([]byte{10}, 32)) + + tests := []struct { + name string + payloads map[string]*decodedOutSwapOnion + parts []OutSwapHtlcPart + wantErr string + }{{ + name: "payment address mismatch", + payloads: map[string]*decodedOutSwapOnion{ + "a": { + amountToForward: 42_000_000, + totalAmount: 42_000_000, + paymentAddr: otherAddr, + hasMPP: true, + }, + }, + parts: []OutSwapHtlcPart{{ + AmountMsat: 42_000_000, + OnionBlob: []byte("a"), + }}, + wantErr: "payment address mismatch", + }, { + name: "total mismatch", + payloads: map[string]*decodedOutSwapOnion{ + "a": { + amountToForward: 42_000_000, + totalAmount: 41_000_000, + paymentAddr: paymentAddr, + hasMPP: true, + }, + }, + parts: []OutSwapHtlcPart{{ + AmountMsat: 42_000_000, + OnionBlob: []byte("a"), + }}, + wantErr: "total amount", + }, { + name: "sum mismatch", + payloads: map[string]*decodedOutSwapOnion{ + "a": { + amountToForward: 20_000_000, + totalAmount: 42_000_000, + paymentAddr: paymentAddr, + hasMPP: true, + }, + "b": { + amountToForward: 21_000_000, + totalAmount: 42_000_000, + paymentAddr: paymentAddr, + hasMPP: true, + }, + }, + parts: []OutSwapHtlcPart{{ + AmountMsat: 20_000_000, + OnionBlob: []byte("a"), + }, { + AmountMsat: 21_000_000, + OnionBlob: []byte("b"), + }}, + wantErr: "amounts sum", + }, { + name: "part amount mismatch", + payloads: map[string]*decodedOutSwapOnion{ + "a": { + amountToForward: 21_000_000, + totalAmount: 42_000_000, + paymentAddr: paymentAddr, + hasMPP: true, + }, + }, + parts: []OutSwapHtlcPart{{ + AmountMsat: 20_000_000, + OnionBlob: []byte("a"), + }}, + wantErr: "does not match part amount", + }, { + name: "missing MPP record", + payloads: map[string]*decodedOutSwapOnion{ + "a": { + amountToForward: 42_000_000, + totalAmount: 42_000_000, + paymentAddr: paymentAddr, + }, + }, + parts: []OutSwapHtlcPart{{ + AmountMsat: 42_000_000, + OnionBlob: []byte("a"), + }}, + wantErr: "missing MPP payment address", + }, { + name: "zero amount part", + payloads: map[string]*decodedOutSwapOnion{ + "a": { + amountToForward: 0, + totalAmount: 42_000_000, + paymentAddr: paymentAddr, + hasMPP: true, + }, + }, + parts: []OutSwapHtlcPart{{ + AmountMsat: 0, + OnionBlob: []byte("a"), + }}, + wantErr: "forwards zero amount", + }, { + name: "part amount above invoice", + payloads: map[string]*decodedOutSwapOnion{ + "a": { + amountToForward: 43_000_000, + totalAmount: 42_000_000, + paymentAddr: paymentAddr, + hasMPP: true, + }, + }, + parts: []OutSwapHtlcPart{{ + AmountMsat: 43_000_000, + OnionBlob: []byte("a"), + }}, + wantErr: "exceeds invoice amount", + }} + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + client := &SwapClient{} + useMappedOnionDecoder(client, test.payloads) + session := &ReceiveSession{ + client: client, + amountSat: 42_000, + paymentAddr: paymentAddr, + } + event := &OutSwapHtlcEvent{ + Parts: test.parts, + } + + err := session.validateOnionPayload( + event, &daemonReceiveAuthKey{}, + ) + require.ErrorContains(t, err, test.wantErr) + }) + } +} + // acceptTestOutSwapHtlcEvent moves a receive session through the durable // mailbox-event boundary without waiting for the test server receiver. func acceptTestOutSwapHtlcEvent(t *testing.T, client *SwapClient, diff --git a/swaprpc/swap.pb.go b/swaprpc/swap.pb.go index 2ded4ada0..a1bba28aa 100644 --- a/swaprpc/swap.pb.go +++ b/swaprpc/swap.pb.go @@ -324,7 +324,13 @@ type OutSwapHtlcEvent struct { OnionBlob []byte `protobuf:"bytes,3,opt,name=onion_blob,json=onionBlob,proto3" json:"onion_blob,omitempty"` // vhtlc_config contains the virtual HTLC parameters the client should use // when constructing the expected output. - VhtlcConfig *VHTLCConfig `protobuf:"bytes,4,opt,name=vhtlc_config,json=vhtlcConfig,proto3" json:"vhtlc_config,omitempty"` + VhtlcConfig *VHTLCConfig `protobuf:"bytes,4,opt,name=vhtlc_config,json=vhtlcConfig,proto3" json:"vhtlc_config,omitempty"` + // parts lists the individual HTLC shards that make up a multi-part + // payment set. When empty the event describes a legacy single-part + // payment carried by onion_blob. When populated, amount_sat is still + // the whole-satoshi total funded in the vHTLC and onion_blob mirrors + // the first part for backwards compatibility. + Parts []*OutSwapHtlcPart `protobuf:"bytes,5,rep,name=parts,proto3" json:"parts,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -387,6 +393,72 @@ func (x *OutSwapHtlcEvent) GetVhtlcConfig() *VHTLCConfig { return nil } +func (x *OutSwapHtlcEvent) GetParts() []*OutSwapHtlcPart { + if x != nil { + return x.Parts + } + return nil +} + +// OutSwapHtlcPart describes one HTLC shard of a multi-part out-swap payment +// set. Each part carries its own onion blob that the client must decode and +// validate before acknowledging the event. +type OutSwapHtlcPart struct { + state protoimpl.MessageState `protogen:"open.v1"` + // amount_msat is the millisatoshi amount forwarded to the receiver by + // this shard. + AmountMsat uint64 `protobuf:"varint,1,opt,name=amount_msat,json=amountMsat,proto3" json:"amount_msat,omitempty"` + // onion_blob is the raw next-hop onion blob delivered by LND for this + // shard. + OnionBlob []byte `protobuf:"bytes,2,opt,name=onion_blob,json=onionBlob,proto3" json:"onion_blob,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OutSwapHtlcPart) Reset() { + *x = OutSwapHtlcPart{} + mi := &file_swap_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OutSwapHtlcPart) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OutSwapHtlcPart) ProtoMessage() {} + +func (x *OutSwapHtlcPart) ProtoReflect() protoreflect.Message { + mi := &file_swap_proto_msgTypes[5] + 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 OutSwapHtlcPart.ProtoReflect.Descriptor instead. +func (*OutSwapHtlcPart) Descriptor() ([]byte, []int) { + return file_swap_proto_rawDescGZIP(), []int{5} +} + +func (x *OutSwapHtlcPart) GetAmountMsat() uint64 { + if x != nil { + return x.AmountMsat + } + return 0 +} + +func (x *OutSwapHtlcPart) GetOnionBlob() []byte { + if x != nil { + return x.OnionBlob + } + return nil +} + // SwapMailboxEvent is the explicit event wrapper carried by swap mailbox // envelopes. type SwapMailboxEvent struct { @@ -402,7 +474,7 @@ type SwapMailboxEvent struct { func (x *SwapMailboxEvent) Reset() { *x = SwapMailboxEvent{} - mi := &file_swap_proto_msgTypes[5] + mi := &file_swap_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -414,7 +486,7 @@ func (x *SwapMailboxEvent) String() string { func (*SwapMailboxEvent) ProtoMessage() {} func (x *SwapMailboxEvent) ProtoReflect() protoreflect.Message { - mi := &file_swap_proto_msgTypes[5] + mi := &file_swap_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -427,7 +499,7 @@ func (x *SwapMailboxEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use SwapMailboxEvent.ProtoReflect.Descriptor instead. func (*SwapMailboxEvent) Descriptor() ([]byte, []int) { - return file_swap_proto_rawDescGZIP(), []int{5} + return file_swap_proto_rawDescGZIP(), []int{6} } func (x *SwapMailboxEvent) GetEvent() isSwapMailboxEvent_Event { @@ -501,7 +573,7 @@ type InArkHtlcEvent struct { func (x *InArkHtlcEvent) Reset() { *x = InArkHtlcEvent{} - mi := &file_swap_proto_msgTypes[6] + mi := &file_swap_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -513,7 +585,7 @@ func (x *InArkHtlcEvent) String() string { func (*InArkHtlcEvent) ProtoMessage() {} func (x *InArkHtlcEvent) ProtoReflect() protoreflect.Message { - mi := &file_swap_proto_msgTypes[6] + mi := &file_swap_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -526,7 +598,7 @@ func (x *InArkHtlcEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use InArkHtlcEvent.ProtoReflect.Descriptor instead. func (*InArkHtlcEvent) Descriptor() ([]byte, []int) { - return file_swap_proto_rawDescGZIP(), []int{6} + return file_swap_proto_rawDescGZIP(), []int{7} } func (x *InArkHtlcEvent) GetPaymentHash() []byte { @@ -590,7 +662,7 @@ type RouteHint struct { func (x *RouteHint) Reset() { *x = RouteHint{} - mi := &file_swap_proto_msgTypes[7] + mi := &file_swap_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -602,7 +674,7 @@ func (x *RouteHint) String() string { func (*RouteHint) ProtoMessage() {} func (x *RouteHint) ProtoReflect() protoreflect.Message { - mi := &file_swap_proto_msgTypes[7] + mi := &file_swap_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -615,7 +687,7 @@ func (x *RouteHint) ProtoReflect() protoreflect.Message { // Deprecated: Use RouteHint.ProtoReflect.Descriptor instead. func (*RouteHint) Descriptor() ([]byte, []int) { - return file_swap_proto_rawDescGZIP(), []int{7} + return file_swap_proto_rawDescGZIP(), []int{8} } func (x *RouteHint) GetNodeId() []byte { @@ -677,7 +749,7 @@ type VHTLCConfig struct { func (x *VHTLCConfig) Reset() { *x = VHTLCConfig{} - mi := &file_swap_proto_msgTypes[8] + mi := &file_swap_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -689,7 +761,7 @@ func (x *VHTLCConfig) String() string { func (*VHTLCConfig) ProtoMessage() {} func (x *VHTLCConfig) ProtoReflect() protoreflect.Message { - mi := &file_swap_proto_msgTypes[8] + mi := &file_swap_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -702,7 +774,7 @@ func (x *VHTLCConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use VHTLCConfig.ProtoReflect.Descriptor instead. func (*VHTLCConfig) Descriptor() ([]byte, []int) { - return file_swap_proto_rawDescGZIP(), []int{8} + return file_swap_proto_rawDescGZIP(), []int{9} } func (x *VHTLCConfig) GetRefundLocktime() uint32 { @@ -756,7 +828,7 @@ type CreateInSwapRequest struct { func (x *CreateInSwapRequest) Reset() { *x = CreateInSwapRequest{} - mi := &file_swap_proto_msgTypes[9] + mi := &file_swap_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -768,7 +840,7 @@ func (x *CreateInSwapRequest) String() string { func (*CreateInSwapRequest) ProtoMessage() {} func (x *CreateInSwapRequest) ProtoReflect() protoreflect.Message { - mi := &file_swap_proto_msgTypes[9] + mi := &file_swap_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -781,7 +853,7 @@ func (x *CreateInSwapRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateInSwapRequest.ProtoReflect.Descriptor instead. func (*CreateInSwapRequest) Descriptor() ([]byte, []int) { - return file_swap_proto_rawDescGZIP(), []int{9} + return file_swap_proto_rawDescGZIP(), []int{10} } func (x *CreateInSwapRequest) GetInvoice() string { @@ -829,7 +901,7 @@ type CreateInSwapResponse struct { func (x *CreateInSwapResponse) Reset() { *x = CreateInSwapResponse{} - mi := &file_swap_proto_msgTypes[10] + mi := &file_swap_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -841,7 +913,7 @@ func (x *CreateInSwapResponse) String() string { func (*CreateInSwapResponse) ProtoMessage() {} func (x *CreateInSwapResponse) ProtoReflect() protoreflect.Message { - mi := &file_swap_proto_msgTypes[10] + mi := &file_swap_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -854,7 +926,7 @@ func (x *CreateInSwapResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateInSwapResponse.ProtoReflect.Descriptor instead. func (*CreateInSwapResponse) Descriptor() ([]byte, []int) { - return file_swap_proto_rawDescGZIP(), []int{10} + return file_swap_proto_rawDescGZIP(), []int{11} } func (x *CreateInSwapResponse) GetPaymentHash() []byte { @@ -926,7 +998,7 @@ type AuthorizeInSwapRefundRequest struct { func (x *AuthorizeInSwapRefundRequest) Reset() { *x = AuthorizeInSwapRefundRequest{} - mi := &file_swap_proto_msgTypes[11] + mi := &file_swap_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -938,7 +1010,7 @@ func (x *AuthorizeInSwapRefundRequest) String() string { func (*AuthorizeInSwapRefundRequest) ProtoMessage() {} func (x *AuthorizeInSwapRefundRequest) ProtoReflect() protoreflect.Message { - mi := &file_swap_proto_msgTypes[11] + mi := &file_swap_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -951,7 +1023,7 @@ func (x *AuthorizeInSwapRefundRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthorizeInSwapRefundRequest.ProtoReflect.Descriptor instead. func (*AuthorizeInSwapRefundRequest) Descriptor() ([]byte, []int) { - return file_swap_proto_rawDescGZIP(), []int{11} + return file_swap_proto_rawDescGZIP(), []int{12} } func (x *AuthorizeInSwapRefundRequest) GetPaymentHash() []byte { @@ -1009,7 +1081,7 @@ type AuthorizeInSwapRefundResponse struct { func (x *AuthorizeInSwapRefundResponse) Reset() { *x = AuthorizeInSwapRefundResponse{} - mi := &file_swap_proto_msgTypes[12] + mi := &file_swap_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1021,7 +1093,7 @@ func (x *AuthorizeInSwapRefundResponse) String() string { func (*AuthorizeInSwapRefundResponse) ProtoMessage() {} func (x *AuthorizeInSwapRefundResponse) ProtoReflect() protoreflect.Message { - mi := &file_swap_proto_msgTypes[12] + mi := &file_swap_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1034,7 +1106,7 @@ func (x *AuthorizeInSwapRefundResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthorizeInSwapRefundResponse.ProtoReflect.Descriptor instead. func (*AuthorizeInSwapRefundResponse) Descriptor() ([]byte, []int) { - return file_swap_proto_rawDescGZIP(), []int{12} + return file_swap_proto_rawDescGZIP(), []int{13} } func (x *AuthorizeInSwapRefundResponse) GetSignature() *TaprootScriptSignature { @@ -1070,7 +1142,7 @@ type TaprootScriptSignature struct { func (x *TaprootScriptSignature) Reset() { *x = TaprootScriptSignature{} - mi := &file_swap_proto_msgTypes[13] + mi := &file_swap_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1082,7 +1154,7 @@ func (x *TaprootScriptSignature) String() string { func (*TaprootScriptSignature) ProtoMessage() {} func (x *TaprootScriptSignature) ProtoReflect() protoreflect.Message { - mi := &file_swap_proto_msgTypes[13] + mi := &file_swap_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1095,7 +1167,7 @@ func (x *TaprootScriptSignature) ProtoReflect() protoreflect.Message { // Deprecated: Use TaprootScriptSignature.ProtoReflect.Descriptor instead. func (*TaprootScriptSignature) Descriptor() ([]byte, []int) { - return file_swap_proto_rawDescGZIP(), []int{13} + return file_swap_proto_rawDescGZIP(), []int{14} } func (x *TaprootScriptSignature) GetPubkey() []byte { @@ -1145,14 +1217,20 @@ const file_swap_proto_rawDesc = "" + "\x1dAcknowledgeOutSwapHtlcRequest\x12!\n" + "\fpayment_hash\x18\x01 \x01(\fR\vpaymentHash\x12.\n" + "\x13client_vhtlc_pubkey\x18\x02 \x01(\fR\x11clientVhtlcPubkey\" \n" + - "\x1eAcknowledgeOutSwapHtlcResponse\"\xac\x01\n" + + "\x1eAcknowledgeOutSwapHtlcResponse\"\xdc\x01\n" + "\x10OutSwapHtlcEvent\x12!\n" + "\fpayment_hash\x18\x01 \x01(\fR\vpaymentHash\x12\x1d\n" + "\n" + "amount_sat\x18\x02 \x01(\x04R\tamountSat\x12\x1d\n" + "\n" + "onion_blob\x18\x03 \x01(\fR\tonionBlob\x127\n" + - "\fvhtlc_config\x18\x04 \x01(\v2\x14.swaprpc.VHTLCConfigR\vvhtlcConfig\"\x97\x01\n" + + "\fvhtlc_config\x18\x04 \x01(\v2\x14.swaprpc.VHTLCConfigR\vvhtlcConfig\x12.\n" + + "\x05parts\x18\x05 \x03(\v2\x18.swaprpc.OutSwapHtlcPartR\x05parts\"Q\n" + + "\x0fOutSwapHtlcPart\x12\x1f\n" + + "\vamount_msat\x18\x01 \x01(\x04R\n" + + "amountMsat\x12\x1d\n" + + "\n" + + "onion_blob\x18\x02 \x01(\fR\tonionBlob\"\x97\x01\n" + "\x10SwapMailboxEvent\x12?\n" + "\rout_swap_htlc\x18\x01 \x01(\v2\x19.swaprpc.OutSwapHtlcEventH\x00R\voutSwapHtlc\x129\n" + "\vin_ark_htlc\x18\x02 \x01(\v2\x17.swaprpc.InArkHtlcEventH\x00R\tinArkHtlcB\a\n" + @@ -1229,7 +1307,7 @@ func file_swap_proto_rawDescGZIP() []byte { } var file_swap_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_swap_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_swap_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_swap_proto_goTypes = []any{ (SettlementType)(0), // 0: swaprpc.SettlementType (*RequestChannelIdRequest)(nil), // 1: swaprpc.RequestChannelIdRequest @@ -1237,40 +1315,42 @@ var file_swap_proto_goTypes = []any{ (*AcknowledgeOutSwapHtlcRequest)(nil), // 3: swaprpc.AcknowledgeOutSwapHtlcRequest (*AcknowledgeOutSwapHtlcResponse)(nil), // 4: swaprpc.AcknowledgeOutSwapHtlcResponse (*OutSwapHtlcEvent)(nil), // 5: swaprpc.OutSwapHtlcEvent - (*SwapMailboxEvent)(nil), // 6: swaprpc.SwapMailboxEvent - (*InArkHtlcEvent)(nil), // 7: swaprpc.InArkHtlcEvent - (*RouteHint)(nil), // 8: swaprpc.RouteHint - (*VHTLCConfig)(nil), // 9: swaprpc.VHTLCConfig - (*CreateInSwapRequest)(nil), // 10: swaprpc.CreateInSwapRequest - (*CreateInSwapResponse)(nil), // 11: swaprpc.CreateInSwapResponse - (*AuthorizeInSwapRefundRequest)(nil), // 12: swaprpc.AuthorizeInSwapRefundRequest - (*AuthorizeInSwapRefundResponse)(nil), // 13: swaprpc.AuthorizeInSwapRefundResponse - (*TaprootScriptSignature)(nil), // 14: swaprpc.TaprootScriptSignature - (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp + (*OutSwapHtlcPart)(nil), // 6: swaprpc.OutSwapHtlcPart + (*SwapMailboxEvent)(nil), // 7: swaprpc.SwapMailboxEvent + (*InArkHtlcEvent)(nil), // 8: swaprpc.InArkHtlcEvent + (*RouteHint)(nil), // 9: swaprpc.RouteHint + (*VHTLCConfig)(nil), // 10: swaprpc.VHTLCConfig + (*CreateInSwapRequest)(nil), // 11: swaprpc.CreateInSwapRequest + (*CreateInSwapResponse)(nil), // 12: swaprpc.CreateInSwapResponse + (*AuthorizeInSwapRefundRequest)(nil), // 13: swaprpc.AuthorizeInSwapRefundRequest + (*AuthorizeInSwapRefundResponse)(nil), // 14: swaprpc.AuthorizeInSwapRefundResponse + (*TaprootScriptSignature)(nil), // 15: swaprpc.TaprootScriptSignature + (*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp } var file_swap_proto_depIdxs = []int32{ - 8, // 0: swaprpc.RequestChannelIdResponse.route_hint:type_name -> swaprpc.RouteHint - 9, // 1: swaprpc.OutSwapHtlcEvent.vhtlc_config:type_name -> swaprpc.VHTLCConfig - 5, // 2: swaprpc.SwapMailboxEvent.out_swap_htlc:type_name -> swaprpc.OutSwapHtlcEvent - 7, // 3: swaprpc.SwapMailboxEvent.in_ark_htlc:type_name -> swaprpc.InArkHtlcEvent - 9, // 4: swaprpc.InArkHtlcEvent.vhtlc_config:type_name -> swaprpc.VHTLCConfig - 9, // 5: swaprpc.CreateInSwapResponse.vhtlc_config:type_name -> swaprpc.VHTLCConfig - 15, // 6: swaprpc.CreateInSwapResponse.expiry:type_name -> google.protobuf.Timestamp - 0, // 7: swaprpc.CreateInSwapResponse.settlement_type:type_name -> swaprpc.SettlementType - 14, // 8: swaprpc.AuthorizeInSwapRefundResponse.signature:type_name -> swaprpc.TaprootScriptSignature - 1, // 9: swaprpc.SwapService.RequestChannelId:input_type -> swaprpc.RequestChannelIdRequest - 10, // 10: swaprpc.SwapService.CreateInSwap:input_type -> swaprpc.CreateInSwapRequest - 12, // 11: swaprpc.SwapService.AuthorizeInSwapRefund:input_type -> swaprpc.AuthorizeInSwapRefundRequest - 3, // 12: swaprpc.SwapService.AcknowledgeOutSwapHtlc:input_type -> swaprpc.AcknowledgeOutSwapHtlcRequest - 2, // 13: swaprpc.SwapService.RequestChannelId:output_type -> swaprpc.RequestChannelIdResponse - 11, // 14: swaprpc.SwapService.CreateInSwap:output_type -> swaprpc.CreateInSwapResponse - 13, // 15: swaprpc.SwapService.AuthorizeInSwapRefund:output_type -> swaprpc.AuthorizeInSwapRefundResponse - 4, // 16: swaprpc.SwapService.AcknowledgeOutSwapHtlc:output_type -> swaprpc.AcknowledgeOutSwapHtlcResponse - 13, // [13:17] is the sub-list for method output_type - 9, // [9:13] 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 + 9, // 0: swaprpc.RequestChannelIdResponse.route_hint:type_name -> swaprpc.RouteHint + 10, // 1: swaprpc.OutSwapHtlcEvent.vhtlc_config:type_name -> swaprpc.VHTLCConfig + 6, // 2: swaprpc.OutSwapHtlcEvent.parts:type_name -> swaprpc.OutSwapHtlcPart + 5, // 3: swaprpc.SwapMailboxEvent.out_swap_htlc:type_name -> swaprpc.OutSwapHtlcEvent + 8, // 4: swaprpc.SwapMailboxEvent.in_ark_htlc:type_name -> swaprpc.InArkHtlcEvent + 10, // 5: swaprpc.InArkHtlcEvent.vhtlc_config:type_name -> swaprpc.VHTLCConfig + 10, // 6: swaprpc.CreateInSwapResponse.vhtlc_config:type_name -> swaprpc.VHTLCConfig + 16, // 7: swaprpc.CreateInSwapResponse.expiry:type_name -> google.protobuf.Timestamp + 0, // 8: swaprpc.CreateInSwapResponse.settlement_type:type_name -> swaprpc.SettlementType + 15, // 9: swaprpc.AuthorizeInSwapRefundResponse.signature:type_name -> swaprpc.TaprootScriptSignature + 1, // 10: swaprpc.SwapService.RequestChannelId:input_type -> swaprpc.RequestChannelIdRequest + 11, // 11: swaprpc.SwapService.CreateInSwap:input_type -> swaprpc.CreateInSwapRequest + 13, // 12: swaprpc.SwapService.AuthorizeInSwapRefund:input_type -> swaprpc.AuthorizeInSwapRefundRequest + 3, // 13: swaprpc.SwapService.AcknowledgeOutSwapHtlc:input_type -> swaprpc.AcknowledgeOutSwapHtlcRequest + 2, // 14: swaprpc.SwapService.RequestChannelId:output_type -> swaprpc.RequestChannelIdResponse + 12, // 15: swaprpc.SwapService.CreateInSwap:output_type -> swaprpc.CreateInSwapResponse + 14, // 16: swaprpc.SwapService.AuthorizeInSwapRefund:output_type -> swaprpc.AuthorizeInSwapRefundResponse + 4, // 17: swaprpc.SwapService.AcknowledgeOutSwapHtlc:output_type -> swaprpc.AcknowledgeOutSwapHtlcResponse + 14, // [14:18] is the sub-list for method output_type + 10, // [10:14] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_swap_proto_init() } @@ -1278,7 +1358,7 @@ func file_swap_proto_init() { if File_swap_proto != nil { return } - file_swap_proto_msgTypes[5].OneofWrappers = []any{ + file_swap_proto_msgTypes[6].OneofWrappers = []any{ (*SwapMailboxEvent_OutSwapHtlc)(nil), (*SwapMailboxEvent_InArkHtlc)(nil), } @@ -1288,7 +1368,7 @@ func file_swap_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_swap_proto_rawDesc), len(file_swap_proto_rawDesc)), NumEnums: 1, - NumMessages: 14, + NumMessages: 15, NumExtensions: 0, NumServices: 1, }, diff --git a/swaprpc/swap.proto b/swaprpc/swap.proto index ccfcae019..bd6a2bdbe 100644 --- a/swaprpc/swap.proto +++ b/swaprpc/swap.proto @@ -111,6 +111,26 @@ message OutSwapHtlcEvent { // vhtlc_config contains the virtual HTLC parameters the client should use // when constructing the expected output. VHTLCConfig vhtlc_config = 4; + + // parts lists the individual HTLC shards that make up a multi-part + // payment set. When empty the event describes a legacy single-part + // payment carried by onion_blob. When populated, amount_sat is still + // the whole-satoshi total funded in the vHTLC and onion_blob mirrors + // the first part for backwards compatibility. + repeated OutSwapHtlcPart parts = 5; +} + +// OutSwapHtlcPart describes one HTLC shard of a multi-part out-swap payment +// set. Each part carries its own onion blob that the client must decode and +// validate before acknowledging the event. +message OutSwapHtlcPart { + // amount_msat is the millisatoshi amount forwarded to the receiver by + // this shard. + uint64 amount_msat = 1; + + // onion_blob is the raw next-hop onion blob delivered by LND for this + // shard. + bytes onion_blob = 2; } // SwapMailboxEvent is the explicit event wrapper carried by swap mailbox