Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions sdk/swaps/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions sdk/swaps/grpc_conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -271,13 +272,33 @@ 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()),
OnionBlob: append(
[]byte(nil), event.GetOnionBlob()...,
),
VHTLCConfig: *cfg,
Parts: parts,
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions sdk/swaps/invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ func genInvoiceCfg(nodeSigner *netann.NodeSigner,
lnwire.NewRawFeatureVector(
lnwire.TLVOnionPayloadRequired,
lnwire.PaymentAddrRequired,
lnwire.MPPOptional,
),
lnwire.Features,
)
Expand Down
7 changes: 4 additions & 3 deletions sdk/swaps/invoice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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))
}
83 changes: 59 additions & 24 deletions sdk/swaps/out_swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -1156,39 +1156,74 @@ 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 {

if authKey == nil {
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent unnecessary cryptographic onion decoding operations (which are CPU-intensive) for subsequent parts when the total amount has already exceeded the expected invoice amount, we should perform an early-exit check right after updating sumMsat.

		sumMsat += payload.amountToForward
		if sumMsat > expectedMsat {
			return fmt.Errorf("part %d: onion amounts sum %d msat exceeds invoice amount %d msat",
				idx, sumMsat, expectedMsat)
		}

}
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
Expand Down
Loading
Loading