mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-23 22:46:40 +01:00
Merge branch '0-18-4-branch-rc1-9095' into 0-18-4-branch-rc1
This commit is contained in:
commit
7857f38f45
54 changed files with 7326 additions and 3840 deletions
|
@ -42,6 +42,7 @@ import (
|
|||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/rpcwallet"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/msgmux"
|
||||
|
@ -182,6 +183,10 @@ type AuxComponents struct {
|
|||
// AuxDataParser is an optional data parser that can be used to parse
|
||||
// auxiliary data for certain custom channel types.
|
||||
AuxDataParser fn.Option[AuxDataParser]
|
||||
|
||||
// AuxChanCloser is an optional channel closer that can be used to
|
||||
// modify the way a coop-close transaction is constructed.
|
||||
AuxChanCloser fn.Option[chancloser.AuxChanCloser]
|
||||
}
|
||||
|
||||
// DefaultWalletImpl is the default implementation of our normal, btcwallet
|
||||
|
|
|
@ -308,7 +308,7 @@ func (h *htlcIncomingContestResolver) Resolve(
|
|||
|
||||
resolution, err := h.Registry.NotifyExitHopHtlc(
|
||||
h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
|
||||
circuitKey, hodlQueue.ChanIn(), payload,
|
||||
circuitKey, hodlQueue.ChanIn(), nil, payload,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -30,6 +30,7 @@ type Registry interface {
|
|||
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
|
||||
expiry uint32, currentHeight int32,
|
||||
circuitKey models.CircuitKey, hodlChan chan<- interface{},
|
||||
wireCustomRecords lnwire.CustomRecords,
|
||||
payload invoices.Payload) (invoices.HtlcResolution, error)
|
||||
|
||||
// HodlUnsubscribeAll unsubscribes from all htlc resolutions.
|
||||
|
|
|
@ -26,6 +26,7 @@ type mockRegistry struct {
|
|||
func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
|
||||
paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
||||
circuitKey models.CircuitKey, hodlChan chan<- interface{},
|
||||
wireCustomRecords lnwire.CustomRecords,
|
||||
payload invoices.Payload) (invoices.HtlcResolution, error) {
|
||||
|
||||
r.notifyChan <- notifyExitHopData{
|
||||
|
|
|
@ -33,6 +33,7 @@ type InvoiceDatabase interface {
|
|||
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
|
||||
expiry uint32, currentHeight int32,
|
||||
circuitKey models.CircuitKey, hodlChan chan<- interface{},
|
||||
wireCustomRecords lnwire.CustomRecords,
|
||||
payload invoices.Payload) (invoices.HtlcResolution, error)
|
||||
|
||||
// CancelInvoice attempts to cancel the invoice corresponding to the
|
||||
|
|
|
@ -3845,7 +3845,7 @@ func (l *channelLink) processExitHop(add lnwire.UpdateAddHTLC,
|
|||
|
||||
event, err := l.cfg.Registry.NotifyExitHopHtlc(
|
||||
invoiceHash, add.Amount, add.Expiry, int32(heightNow),
|
||||
circuitKey, l.hodlQueue.ChanIn(), payload,
|
||||
circuitKey, l.hodlQueue.ChanIn(), add.CustomRecords, payload,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -1014,6 +1014,7 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
modifierMock := &invoices.MockHtlcModifier{}
|
||||
registry := invoices.NewRegistry(
|
||||
cdb,
|
||||
invoices.NewInvoiceExpiryWatcher(
|
||||
|
@ -1022,6 +1023,7 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry {
|
|||
),
|
||||
&invoices.RegistryConfig{
|
||||
FinalCltvRejectDelta: 5,
|
||||
HtlcInterceptor: modifierMock,
|
||||
},
|
||||
)
|
||||
registry.Start()
|
||||
|
@ -1047,11 +1049,12 @@ func (i *mockInvoiceRegistry) SettleHodlInvoice(
|
|||
func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash,
|
||||
amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
||||
circuitKey models.CircuitKey, hodlChan chan<- interface{},
|
||||
wireCustomRecords lnwire.CustomRecords,
|
||||
payload invoices.Payload) (invoices.HtlcResolution, error) {
|
||||
|
||||
event, err := i.registry.NotifyExitHopHtlc(
|
||||
rhash, amt, expiry, currentHeight, circuitKey, hodlChan,
|
||||
payload,
|
||||
rhash, amt, expiry, currentHeight, circuitKey,
|
||||
hodlChan, wireCustomRecords, payload,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -207,3 +207,71 @@ type InvoiceUpdater interface {
|
|||
// Finalize finalizes the update before it is written to the database.
|
||||
Finalize(updateType UpdateType) error
|
||||
}
|
||||
|
||||
// HtlcModifyRequest is the request that is passed to the client via callback
|
||||
// during a HTLC interceptor session. The request contains the invoice that the
|
||||
// given HTLC is attempting to settle.
|
||||
type HtlcModifyRequest struct {
|
||||
// WireCustomRecords are the custom records that were parsed from the
|
||||
// HTLC wire message. These are the records of the current HTLC to be
|
||||
// accepted/settled. All previously accepted/settled HTLCs for the same
|
||||
// invoice are present in the Invoice field below.
|
||||
WireCustomRecords lnwire.CustomRecords
|
||||
|
||||
// ExitHtlcCircuitKey is the circuit key that identifies the HTLC which
|
||||
// is involved in the invoice settlement.
|
||||
ExitHtlcCircuitKey CircuitKey
|
||||
|
||||
// ExitHtlcAmt is the amount of the HTLC which is involved in the
|
||||
// invoice settlement.
|
||||
ExitHtlcAmt lnwire.MilliSatoshi
|
||||
|
||||
// ExitHtlcExpiry is the absolute expiry height of the HTLC which is
|
||||
// involved in the invoice settlement.
|
||||
ExitHtlcExpiry uint32
|
||||
|
||||
// CurrentHeight is the current block height.
|
||||
CurrentHeight uint32
|
||||
|
||||
// Invoice is the invoice that is being intercepted. The HTLCs within
|
||||
// the invoice are only those previously accepted/settled for the same
|
||||
// invoice.
|
||||
Invoice Invoice
|
||||
}
|
||||
|
||||
// HtlcModifyResponse is the response that the client should send back to the
|
||||
// interceptor after processing the HTLC modify request.
|
||||
type HtlcModifyResponse struct {
|
||||
// AmountPaid is the amount that the client has decided the HTLC is
|
||||
// actually worth. This might be different from the amount that the
|
||||
// HTLC was originally sent with, in case additional value is carried
|
||||
// along with it (which might be the case in custom channels).
|
||||
AmountPaid lnwire.MilliSatoshi
|
||||
}
|
||||
|
||||
// HtlcModifyCallback is a function that is called when an invoice is
|
||||
// intercepted by the invoice interceptor.
|
||||
type HtlcModifyCallback func(HtlcModifyRequest) (*HtlcModifyResponse, error)
|
||||
|
||||
// HtlcModifier is an interface that allows an intercept client to register
|
||||
// itself as a modifier of HTLCs that are settling an invoice. The client can
|
||||
// then modify the HTLCs based on the invoice and the HTLC that is settling it.
|
||||
type HtlcModifier interface {
|
||||
// RegisterInterceptor sets the client callback function that will be
|
||||
// called when an invoice is intercepted. If a callback is already set,
|
||||
// an error is returned. The returned function must be used to reset the
|
||||
// callback to nil once the client is done or disconnects. The read-only
|
||||
// channel closes when the server stops.
|
||||
RegisterInterceptor(HtlcModifyCallback) (func(), <-chan struct{}, error)
|
||||
}
|
||||
|
||||
// HtlcInterceptor is an interface that allows the invoice registry to let
|
||||
// clients intercept invoices before they are settled.
|
||||
type HtlcInterceptor interface {
|
||||
// Intercept generates a new intercept session for the given invoice.
|
||||
// The call blocks until the client has responded to the request or an
|
||||
// error occurs. The response callback is only called if a session was
|
||||
// created in the first place, which is only the case if a client is
|
||||
// registered.
|
||||
Intercept(HtlcModifyRequest, func(HtlcModifyResponse)) error
|
||||
}
|
||||
|
|
|
@ -74,6 +74,10 @@ type RegistryConfig struct {
|
|||
// KeysendHoldTime indicates for how long we want to accept and hold
|
||||
// spontaneous keysend payments.
|
||||
KeysendHoldTime time.Duration
|
||||
|
||||
// HtlcInterceptor is an interface that allows the invoice registry to
|
||||
// let clients intercept invoices before they are settled.
|
||||
HtlcInterceptor HtlcInterceptor
|
||||
}
|
||||
|
||||
// htlcReleaseEvent describes an htlc auto-release event. It is used to release
|
||||
|
@ -914,6 +918,7 @@ func (i *InvoiceRegistry) processAMP(ctx invoiceUpdateCtx) error {
|
|||
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
||||
amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
||||
circuitKey CircuitKey, hodlChan chan<- interface{},
|
||||
wireCustomRecords lnwire.CustomRecords,
|
||||
payload Payload) (HtlcResolution, error) {
|
||||
|
||||
// Create the update context containing the relevant details of the
|
||||
|
@ -925,6 +930,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||
expiry: expiry,
|
||||
currentHeight: currentHeight,
|
||||
finalCltvRejectDelta: i.cfg.FinalCltvRejectDelta,
|
||||
wireCustomRecords: wireCustomRecords,
|
||||
customRecords: payload.CustomRecords(),
|
||||
mpp: payload.MultiPath(),
|
||||
amp: payload.AMPRecord(),
|
||||
|
@ -1019,13 +1025,62 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
|
|||
ctx *invoiceUpdateCtx, hodlChan chan<- interface{}) (
|
||||
HtlcResolution, invoiceExpiry, error) {
|
||||
|
||||
invoiceRef := ctx.invoiceRef()
|
||||
setID := (*SetID)(ctx.setID())
|
||||
|
||||
// We need to look up the current state of the invoice in order to send
|
||||
// the previously accepted/settled HTLCs to the interceptor.
|
||||
existingInvoice, err := i.idb.LookupInvoice(
|
||||
context.Background(), invoiceRef,
|
||||
)
|
||||
switch {
|
||||
case errors.Is(err, ErrInvoiceNotFound) ||
|
||||
errors.Is(err, ErrNoInvoicesCreated):
|
||||
|
||||
// If the invoice was not found, return a failure resolution
|
||||
// with an invoice not found result.
|
||||
return NewFailResolution(
|
||||
ctx.circuitKey, ctx.currentHeight,
|
||||
ResultInvoiceNotFound,
|
||||
), nil, nil
|
||||
|
||||
case err != nil:
|
||||
ctx.log(err.Error())
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Provide the invoice to the settlement interceptor to allow
|
||||
// the interceptor's client an opportunity to manipulate the
|
||||
// settlement process.
|
||||
err = i.cfg.HtlcInterceptor.Intercept(HtlcModifyRequest{
|
||||
WireCustomRecords: ctx.wireCustomRecords,
|
||||
ExitHtlcCircuitKey: ctx.circuitKey,
|
||||
ExitHtlcAmt: ctx.amtPaid,
|
||||
ExitHtlcExpiry: ctx.expiry,
|
||||
CurrentHeight: uint32(ctx.currentHeight),
|
||||
Invoice: existingInvoice,
|
||||
}, func(resp HtlcModifyResponse) {
|
||||
log.Debugf("Received invoice HTLC interceptor response: %v",
|
||||
resp)
|
||||
|
||||
if resp.AmountPaid != 0 {
|
||||
ctx.amtPaid = resp.AmountPaid
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error during invoice HTLC interception: %w",
|
||||
err)
|
||||
ctx.log(err.Error())
|
||||
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// We'll attempt to settle an invoice matching this rHash on disk (if
|
||||
// one exists). The callback will update the invoice state and/or htlcs.
|
||||
var (
|
||||
resolution HtlcResolution
|
||||
updateSubscribers bool
|
||||
)
|
||||
|
||||
callback := func(inv *Invoice) (*InvoiceUpdateDesc, error) {
|
||||
updateDesc, res, err := updateInvoice(ctx, inv)
|
||||
if err != nil {
|
||||
|
@ -1042,8 +1097,6 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
|
|||
return updateDesc, nil
|
||||
}
|
||||
|
||||
invoiceRef := ctx.invoiceRef()
|
||||
setID := (*SetID)(ctx.setID())
|
||||
invoice, err := i.idb.UpdateInvoice(
|
||||
context.Background(), invoiceRef, setID, callback,
|
||||
)
|
||||
|
@ -1080,6 +1133,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
|
|||
|
||||
var invoiceToExpire invoiceExpiry
|
||||
|
||||
log.Tracef("Settlement resolution: %T %v", resolution, resolution)
|
||||
|
||||
switch res := resolution.(type) {
|
||||
case *HtlcFailResolution:
|
||||
// Inspect latest htlc state on the invoice. If it is found,
|
||||
|
@ -1212,7 +1267,7 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
|
|||
}
|
||||
|
||||
// Now that the links have been notified of any state changes to their
|
||||
// HTLCs, we'll go ahead and notify any clients wiaiting on the invoice
|
||||
// HTLCs, we'll go ahead and notify any clients waiting on the invoice
|
||||
// state changes.
|
||||
if updateSubscribers {
|
||||
// We'll add a setID onto the notification, but only if this is
|
||||
|
|
|
@ -23,6 +23,12 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
// htlcModifierMock is a mock implementation of the invoice HtlcModifier
|
||||
// interface.
|
||||
htlcModifierMock = &invpkg.MockHtlcModifier{}
|
||||
)
|
||||
|
||||
// TestInvoiceRegistry is a master test which encompasses all tests using an
|
||||
// InvoiceDB instance. The purpose of this test is to be able to run all tests
|
||||
// with a custom DB instance, so that we can test the same logic with different
|
||||
|
@ -239,7 +245,8 @@ func testSettleInvoice(t *testing.T,
|
|||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, testInvoice.Terms.Value,
|
||||
uint32(testCurrentHeight)+testInvoiceCltvDelta-1,
|
||||
testCurrentHeight, getCircuitKey(10), hodlChan, testPayload,
|
||||
testCurrentHeight, getCircuitKey(10), hodlChan,
|
||||
nil, testPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -255,7 +262,7 @@ func testSettleInvoice(t *testing.T,
|
|||
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(0), hodlChan,
|
||||
testPayload,
|
||||
nil, testPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -296,7 +303,8 @@ func testSettleInvoice(t *testing.T,
|
|||
// behaviour after a restart.
|
||||
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(0), hodlChan, testPayload,
|
||||
testCurrentHeight, getCircuitKey(0), hodlChan,
|
||||
nil, testPayload,
|
||||
)
|
||||
require.NoError(t, err, "unexpected NotifyExitHopHtlc error")
|
||||
require.NotNil(t, resolution)
|
||||
|
@ -310,7 +318,8 @@ func testSettleInvoice(t *testing.T,
|
|||
// paid invoice that may open up a probe vector.
|
||||
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, amtPaid+600, testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(1), hodlChan, testPayload,
|
||||
testCurrentHeight, getCircuitKey(1), hodlChan,
|
||||
nil, testPayload,
|
||||
)
|
||||
require.NoError(t, err, "unexpected NotifyExitHopHtlc error")
|
||||
require.NotNil(t, resolution)
|
||||
|
@ -325,7 +334,8 @@ func testSettleInvoice(t *testing.T,
|
|||
// would have failed if it were the first payment.
|
||||
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, amtPaid-600, testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(2), hodlChan, testPayload,
|
||||
testCurrentHeight, getCircuitKey(2), hodlChan,
|
||||
nil, testPayload,
|
||||
)
|
||||
require.NoError(t, err, "unexpected NotifyExitHopHtlc error")
|
||||
require.NotNil(t, resolution)
|
||||
|
@ -462,7 +472,8 @@ func testCancelInvoiceImpl(t *testing.T, gc bool,
|
|||
hodlChan := make(chan interface{})
|
||||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, testInvoiceAmount, testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(0), hodlChan, testPayload,
|
||||
testCurrentHeight, getCircuitKey(0), hodlChan,
|
||||
nil, testPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal("expected settlement of a canceled invoice to succeed")
|
||||
|
@ -517,6 +528,7 @@ func testSettleHoldInvoice(t *testing.T,
|
|||
cfg := invpkg.RegistryConfig{
|
||||
FinalCltvRejectDelta: testFinalCltvRejectDelta,
|
||||
Clock: clock,
|
||||
HtlcInterceptor: htlcModifierMock,
|
||||
}
|
||||
|
||||
expiryWatcher := invpkg.NewInvoiceExpiryWatcher(
|
||||
|
@ -570,7 +582,8 @@ func testSettleHoldInvoice(t *testing.T,
|
|||
// should be possible.
|
||||
resolution, err := registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(0), hodlChan, testPayload,
|
||||
testCurrentHeight, getCircuitKey(0), hodlChan,
|
||||
nil, testPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected settle to succeed but got %v", err)
|
||||
|
@ -582,7 +595,8 @@ func testSettleHoldInvoice(t *testing.T,
|
|||
// Test idempotency.
|
||||
resolution, err = registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(0), hodlChan, testPayload,
|
||||
testCurrentHeight, getCircuitKey(0), hodlChan,
|
||||
nil, testPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected settle to succeed but got %v", err)
|
||||
|
@ -595,7 +609,8 @@ func testSettleHoldInvoice(t *testing.T,
|
|||
// is a replay.
|
||||
resolution, err = registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry,
|
||||
testCurrentHeight+10, getCircuitKey(0), hodlChan, testPayload,
|
||||
testCurrentHeight+10, getCircuitKey(0), hodlChan,
|
||||
nil, testPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected settle to succeed but got %v", err)
|
||||
|
@ -608,7 +623,7 @@ func testSettleHoldInvoice(t *testing.T,
|
|||
// requirement. It should be rejected.
|
||||
resolution, err = registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, amtPaid, 1, testCurrentHeight,
|
||||
getCircuitKey(1), hodlChan, testPayload,
|
||||
getCircuitKey(1), hodlChan, nil, testPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected settle to succeed but got %v", err)
|
||||
|
@ -683,6 +698,7 @@ func testCancelHoldInvoice(t *testing.T,
|
|||
cfg := invpkg.RegistryConfig{
|
||||
FinalCltvRejectDelta: testFinalCltvRejectDelta,
|
||||
Clock: testClock,
|
||||
HtlcInterceptor: htlcModifierMock,
|
||||
}
|
||||
expiryWatcher := invpkg.NewInvoiceExpiryWatcher(
|
||||
cfg.Clock, 0, uint32(testCurrentHeight), nil, newMockNotifier(),
|
||||
|
@ -711,7 +727,8 @@ func testCancelHoldInvoice(t *testing.T,
|
|||
// should be possible.
|
||||
resolution, err := registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(0), hodlChan, testPayload,
|
||||
testCurrentHeight, getCircuitKey(0), hodlChan,
|
||||
nil, testPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected settle to succeed but got %v", err)
|
||||
|
@ -733,7 +750,8 @@ func testCancelHoldInvoice(t *testing.T,
|
|||
// accept height.
|
||||
resolution, err = registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry,
|
||||
testCurrentHeight+1, getCircuitKey(0), hodlChan, testPayload,
|
||||
testCurrentHeight+1, getCircuitKey(0), hodlChan,
|
||||
nil, testPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected settle to succeed but got %v", err)
|
||||
|
@ -762,7 +780,7 @@ func testUnknownInvoice(t *testing.T,
|
|||
amt := lnwire.MilliSatoshi(100000)
|
||||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, amt, testHtlcExpiry, testCurrentHeight,
|
||||
getCircuitKey(0), hodlChan, testPayload,
|
||||
getCircuitKey(0), hodlChan, nil, testPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error")
|
||||
|
@ -823,7 +841,7 @@ func testKeySendImpl(t *testing.T, keySendEnabled bool,
|
|||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
hash, amt, expiry,
|
||||
testCurrentHeight, getCircuitKey(10), hodlChan,
|
||||
invalidKeySendPayload,
|
||||
nil, invalidKeySendPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -844,8 +862,8 @@ func testKeySendImpl(t *testing.T, keySendEnabled bool,
|
|||
}
|
||||
|
||||
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||
hash, amt, expiry,
|
||||
testCurrentHeight, getCircuitKey(10), hodlChan, keySendPayload,
|
||||
hash, amt, expiry, testCurrentHeight, getCircuitKey(10),
|
||||
hodlChan, nil, keySendPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -873,8 +891,8 @@ func testKeySendImpl(t *testing.T, keySendEnabled bool,
|
|||
// Replay the same keysend payment. We expect an identical resolution,
|
||||
// but no event should be generated.
|
||||
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||
hash, amt, expiry,
|
||||
testCurrentHeight, getCircuitKey(10), hodlChan, keySendPayload,
|
||||
hash, amt, expiry, testCurrentHeight, getCircuitKey(10),
|
||||
hodlChan, nil, keySendPayload,
|
||||
)
|
||||
require.Nil(t, err)
|
||||
checkSettleResolution(t, resolution, preimage)
|
||||
|
@ -897,8 +915,8 @@ func testKeySendImpl(t *testing.T, keySendEnabled bool,
|
|||
}
|
||||
|
||||
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||
hash2, amt, expiry,
|
||||
testCurrentHeight, getCircuitKey(20), hodlChan, keySendPayload2,
|
||||
hash2, amt, expiry, testCurrentHeight, getCircuitKey(20),
|
||||
hodlChan, nil, keySendPayload2,
|
||||
)
|
||||
require.Nil(t, err)
|
||||
|
||||
|
@ -956,8 +974,8 @@ func testHoldKeysendImpl(t *testing.T, timeoutKeysend bool,
|
|||
}
|
||||
|
||||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
hash, amt, expiry,
|
||||
testCurrentHeight, getCircuitKey(10), hodlChan, keysendPayload,
|
||||
hash, amt, expiry, testCurrentHeight, getCircuitKey(10),
|
||||
hodlChan, nil, keysendPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -1039,8 +1057,8 @@ func testMppPayment(t *testing.T,
|
|||
hodlChan1 := make(chan interface{}, 1)
|
||||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, testInvoice.Terms.Value/2,
|
||||
testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(10), hodlChan1, mppPayload,
|
||||
testHtlcExpiry, testCurrentHeight, getCircuitKey(10),
|
||||
hodlChan1, nil, mppPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -1067,8 +1085,8 @@ func testMppPayment(t *testing.T,
|
|||
hodlChan2 := make(chan interface{}, 1)
|
||||
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, testInvoice.Terms.Value/2,
|
||||
testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(11), hodlChan2, mppPayload,
|
||||
testHtlcExpiry, testCurrentHeight, getCircuitKey(11),
|
||||
hodlChan2, nil, mppPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -1081,8 +1099,8 @@ func testMppPayment(t *testing.T,
|
|||
hodlChan3 := make(chan interface{}, 1)
|
||||
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, testInvoice.Terms.Value/2,
|
||||
testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(12), hodlChan3, mppPayload,
|
||||
testHtlcExpiry, testCurrentHeight, getCircuitKey(12),
|
||||
hodlChan3, nil, mppPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -1141,7 +1159,7 @@ func testMppPaymentWithOverpayment(t *testing.T,
|
|||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, testInvoice.Terms.Value/2,
|
||||
testHtlcExpiry, testCurrentHeight, getCircuitKey(11),
|
||||
hodlChan1, mppPayload,
|
||||
hodlChan1, nil, mppPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -1156,7 +1174,7 @@ func testMppPaymentWithOverpayment(t *testing.T,
|
|||
testInvoicePaymentHash,
|
||||
testInvoice.Terms.Value/2+overpayment, testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(12), hodlChan2,
|
||||
mppPayload,
|
||||
nil, mppPayload,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -1200,6 +1218,7 @@ func testInvoiceExpiryWithRegistry(t *testing.T,
|
|||
cfg := invpkg.RegistryConfig{
|
||||
FinalCltvRejectDelta: testFinalCltvRejectDelta,
|
||||
Clock: testClock,
|
||||
HtlcInterceptor: htlcModifierMock,
|
||||
}
|
||||
|
||||
expiryWatcher := invpkg.NewInvoiceExpiryWatcher(
|
||||
|
@ -1310,6 +1329,7 @@ func testOldInvoiceRemovalOnStart(t *testing.T,
|
|||
FinalCltvRejectDelta: testFinalCltvRejectDelta,
|
||||
Clock: testClock,
|
||||
GcCanceledInvoicesOnStartup: true,
|
||||
HtlcInterceptor: htlcModifierMock,
|
||||
}
|
||||
|
||||
expiryWatcher := invpkg.NewInvoiceExpiryWatcher(
|
||||
|
@ -1439,7 +1459,7 @@ func testHeightExpiryWithRegistryImpl(t *testing.T, numParts int, settle bool,
|
|||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, htlcAmt, expiry,
|
||||
testCurrentHeight, getCircuitKey(uint64(i)), hodlChan,
|
||||
payLoad,
|
||||
nil, payLoad,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resolution, "did not expect direct resolution")
|
||||
|
@ -1541,8 +1561,8 @@ func testMultipleSetHeightExpiry(t *testing.T,
|
|||
hodlChan1 := make(chan interface{}, 1)
|
||||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, testInvoice.Terms.Value/2,
|
||||
testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(10), hodlChan1, mppPayload,
|
||||
testHtlcExpiry, testCurrentHeight, getCircuitKey(10),
|
||||
hodlChan1, nil, mppPayload,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resolution, "did not expect direct resolution")
|
||||
|
@ -1571,7 +1591,8 @@ func testMultipleSetHeightExpiry(t *testing.T,
|
|||
hodlChan2 := make(chan interface{}, 1)
|
||||
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, testInvoice.Terms.Value/2, expiry,
|
||||
testCurrentHeight, getCircuitKey(11), hodlChan2, mppPayload,
|
||||
testCurrentHeight, getCircuitKey(11), hodlChan2,
|
||||
nil, mppPayload,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resolution, "did not expect direct resolution")
|
||||
|
@ -1580,7 +1601,8 @@ func testMultipleSetHeightExpiry(t *testing.T,
|
|||
hodlChan3 := make(chan interface{}, 1)
|
||||
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, testInvoice.Terms.Value/2, expiry,
|
||||
testCurrentHeight, getCircuitKey(12), hodlChan3, mppPayload,
|
||||
testCurrentHeight, getCircuitKey(12), hodlChan3,
|
||||
nil, mppPayload,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resolution, "did not expect direct resolution")
|
||||
|
@ -1685,7 +1707,8 @@ func testSettleInvoicePaymentAddrRequired(t *testing.T,
|
|||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, invoice.Terms.Value,
|
||||
uint32(testCurrentHeight)+testInvoiceCltvDelta-1,
|
||||
testCurrentHeight, getCircuitKey(10), hodlChan, testPayload,
|
||||
testCurrentHeight, getCircuitKey(10), hodlChan,
|
||||
nil, testPayload,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -1774,9 +1797,9 @@ func testSettleInvoicePaymentAddrRequiredOptionalGrace(t *testing.T,
|
|||
// no problem as we should allow these existing invoices to be settled.
|
||||
hodlChan := make(chan interface{}, 1)
|
||||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
testInvoicePaymentHash, testInvoiceAmount,
|
||||
testHtlcExpiry, testCurrentHeight,
|
||||
getCircuitKey(10), hodlChan, testPayload,
|
||||
testInvoicePaymentHash, testInvoiceAmount, testHtlcExpiry,
|
||||
testCurrentHeight, getCircuitKey(10), hodlChan,
|
||||
nil, testPayload,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -1838,8 +1861,8 @@ func testAMPWithoutMPPPayload(t *testing.T,
|
|||
|
||||
hodlChan := make(chan interface{}, 1)
|
||||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
lntypes.Hash{}, shardAmt, expiry,
|
||||
testCurrentHeight, getCircuitKey(uint64(10)), hodlChan,
|
||||
lntypes.Hash{}, shardAmt, expiry, testCurrentHeight,
|
||||
getCircuitKey(uint64(10)), hodlChan, nil,
|
||||
payload,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
@ -2007,8 +2030,8 @@ func testSpontaneousAmpPaymentImpl(
|
|||
}
|
||||
|
||||
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||
child.Hash, shardAmt, expiry,
|
||||
testCurrentHeight, getCircuitKey(uint64(i)), hodlChan,
|
||||
child.Hash, shardAmt, expiry, testCurrentHeight,
|
||||
getCircuitKey(uint64(i)), hodlChan, nil,
|
||||
payload,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -547,6 +547,11 @@ type InvoiceHTLC struct {
|
|||
// the htlc.
|
||||
CustomRecords record.CustomSet
|
||||
|
||||
// WireCustomRecords contains the custom key/value pairs that were only
|
||||
// included in p2p wire message of the HTLC and not in the onion
|
||||
// payload.
|
||||
WireCustomRecords lnwire.CustomRecords
|
||||
|
||||
// AMP encapsulates additional data relevant to AMP HTLCs. This includes
|
||||
// the AMP onion record, in addition to the HTLC's payment hash and
|
||||
// preimage since these are unique to each AMP HTLC, and not the invoice
|
||||
|
@ -566,6 +571,7 @@ func (h *InvoiceHTLC) Copy() *InvoiceHTLC {
|
|||
result.CustomRecords[k] = v
|
||||
}
|
||||
|
||||
result.WireCustomRecords = h.WireCustomRecords.Copy()
|
||||
result.AMP = h.AMP.Copy()
|
||||
|
||||
return &result
|
||||
|
|
|
@ -83,3 +83,34 @@ func (m *MockInvoiceDB) DeleteCanceledInvoices(ctx context.Context) error {
|
|||
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// MockHtlcModifier is a mock implementation of the HtlcModifier interface.
|
||||
type MockHtlcModifier struct {
|
||||
}
|
||||
|
||||
// Intercept generates a new intercept session for the given invoice.
|
||||
// The call blocks until the client has responded to the request or an
|
||||
// error occurs. The response callback is only called if a session was
|
||||
// created in the first place, which is only the case if a client is
|
||||
// registered.
|
||||
func (m *MockHtlcModifier) Intercept(
|
||||
_ HtlcModifyRequest, _ func(HtlcModifyResponse)) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterInterceptor sets the client callback function that will be
|
||||
// called when an invoice is intercepted. If a callback is already set,
|
||||
// an error is returned. The returned function must be used to reset the
|
||||
// callback to nil once the client is done or disconnects. The read-only channel
|
||||
// closes when the server stops.
|
||||
func (m *MockHtlcModifier) RegisterInterceptor(HtlcModifyCallback) (func(),
|
||||
<-chan struct{}, error) {
|
||||
|
||||
return func() {}, make(chan struct{}), nil
|
||||
}
|
||||
|
||||
// Ensure that MockHtlcModifier implements the HtlcInterceptor and HtlcModifier
|
||||
// interfaces.
|
||||
var _ HtlcInterceptor = (*MockHtlcModifier)(nil)
|
||||
var _ HtlcModifier = (*MockHtlcModifier)(nil)
|
||||
|
|
179
invoices/modification_interceptor.go
Normal file
179
invoices/modification_interceptor.go
Normal file
|
@ -0,0 +1,179 @@
|
|||
package invoices
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInterceptorClientAlreadyConnected is an error that is returned
|
||||
// when a client tries to connect to the interceptor service while
|
||||
// another client is already connected.
|
||||
ErrInterceptorClientAlreadyConnected = errors.New(
|
||||
"interceptor client already connected",
|
||||
)
|
||||
|
||||
// ErrInterceptorClientDisconnected is an error that is returned when
|
||||
// the client disconnects during an interceptor session.
|
||||
ErrInterceptorClientDisconnected = errors.New(
|
||||
"interceptor client disconnected",
|
||||
)
|
||||
)
|
||||
|
||||
// safeCallback is a wrapper around a callback function that is safe for
|
||||
// concurrent access.
|
||||
type safeCallback struct {
|
||||
// callback is the actual callback function that is called when an
|
||||
// invoice is intercepted. This might be nil if no client is currently
|
||||
// connected.
|
||||
callback atomic.Pointer[HtlcModifyCallback]
|
||||
}
|
||||
|
||||
// Set atomically sets the callback function. If a callback is already set, an
|
||||
// error is returned. The returned function can be used to reset the callback to
|
||||
// nil once the client is done.
|
||||
func (s *safeCallback) Set(callback HtlcModifyCallback) (func(), error) {
|
||||
if !s.callback.CompareAndSwap(nil, &callback) {
|
||||
return nil, ErrInterceptorClientAlreadyConnected
|
||||
}
|
||||
|
||||
return func() {
|
||||
s.callback.Store(nil)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsConnected returns true if a client is currently connected.
|
||||
func (s *safeCallback) IsConnected() bool {
|
||||
return s.callback.Load() != nil
|
||||
}
|
||||
|
||||
// Exec executes the callback function if it is set. If the callback is not set,
|
||||
// an error is returned.
|
||||
func (s *safeCallback) Exec(req HtlcModifyRequest) (*HtlcModifyResponse,
|
||||
error) {
|
||||
|
||||
callback := s.callback.Load()
|
||||
if callback == nil {
|
||||
return nil, ErrInterceptorClientDisconnected
|
||||
}
|
||||
|
||||
return (*callback)(req)
|
||||
}
|
||||
|
||||
// HtlcModificationInterceptor is a service that intercepts HTLCs that aim to
|
||||
// settle an invoice, enabling a subscribed client to modify certain aspects of
|
||||
// those HTLCs.
|
||||
type HtlcModificationInterceptor struct {
|
||||
// callback is the wrapped client callback function that is called when
|
||||
// an invoice is intercepted. This function gives the client the ability
|
||||
// to determine how the invoice should be settled.
|
||||
callback *safeCallback
|
||||
|
||||
// quit is a channel that is closed when the interceptor is stopped.
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
// NewHtlcModificationInterceptor creates a new HtlcModificationInterceptor.
|
||||
func NewHtlcModificationInterceptor() *HtlcModificationInterceptor {
|
||||
return &HtlcModificationInterceptor{
|
||||
callback: &safeCallback{},
|
||||
}
|
||||
}
|
||||
|
||||
// Intercept generates a new intercept session for the given invoice. The call
|
||||
// blocks until the client has responded to the request or an error occurs. The
|
||||
// response callback is only called if a session was created in the first place,
|
||||
// which is only the case if a client is registered.
|
||||
func (s *HtlcModificationInterceptor) Intercept(clientRequest HtlcModifyRequest,
|
||||
responseCallback func(HtlcModifyResponse)) error {
|
||||
|
||||
// If there is no client callback set we will not handle the invoice
|
||||
// further.
|
||||
if !s.callback.IsConnected() {
|
||||
log.Debugf("Not intercepting invoice with circuit key %v, no "+
|
||||
"intercept client connected",
|
||||
clientRequest.ExitHtlcCircuitKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// We'll block until the client has responded to the request or an error
|
||||
// occurs.
|
||||
var (
|
||||
responseChan = make(chan *HtlcModifyResponse, 1)
|
||||
errChan = make(chan error, 1)
|
||||
)
|
||||
|
||||
// The callback function will block at the client's discretion. We will
|
||||
// therefore execute it in a separate goroutine. We don't need a wait
|
||||
// group because we wait for the response directly below. The caller
|
||||
// needs to make sure they don't block indefinitely, by selecting on the
|
||||
// quit channel they receive when registering the callback.
|
||||
go func() {
|
||||
log.Debugf("Waiting for client response from invoice HTLC "+
|
||||
"interceptor session with circuit key %v",
|
||||
clientRequest.ExitHtlcCircuitKey)
|
||||
|
||||
// By this point, we've already checked that the client callback
|
||||
// is set. However, if the client disconnected since that check
|
||||
// then Exec will return an error.
|
||||
result, err := s.callback.Exec(clientRequest)
|
||||
if err != nil {
|
||||
_ = fn.SendOrQuit(errChan, err, s.quit)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_ = fn.SendOrQuit(responseChan, result, s.quit)
|
||||
}()
|
||||
|
||||
// Wait for the client to respond or an error to occur.
|
||||
select {
|
||||
case response := <-responseChan:
|
||||
log.Debugf("Received invoice HTLC interceptor response: %v",
|
||||
response)
|
||||
|
||||
responseCallback(*response)
|
||||
|
||||
return nil
|
||||
|
||||
case err := <-errChan:
|
||||
log.Errorf("Error from invoice HTLC interceptor session: %v",
|
||||
err)
|
||||
|
||||
return err
|
||||
|
||||
case <-s.quit:
|
||||
return ErrInterceptorClientDisconnected
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterInterceptor sets the client callback function that will be called
|
||||
// when an invoice is intercepted. If a callback is already set, an error is
|
||||
// returned. The returned function must be used to reset the callback to nil
|
||||
// once the client is done or disconnects.
|
||||
func (s *HtlcModificationInterceptor) RegisterInterceptor(
|
||||
callback HtlcModifyCallback) (func(), <-chan struct{}, error) {
|
||||
|
||||
done, err := s.callback.Set(callback)
|
||||
return done, s.quit, err
|
||||
}
|
||||
|
||||
// Start starts the service.
|
||||
func (s *HtlcModificationInterceptor) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the service.
|
||||
func (s *HtlcModificationInterceptor) Stop() error {
|
||||
close(s.quit)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure that HtlcModificationInterceptor implements the HtlcInterceptor and
|
||||
// HtlcModifier interfaces.
|
||||
var _ HtlcInterceptor = (*HtlcModificationInterceptor)(nil)
|
||||
var _ HtlcModifier = (*HtlcModificationInterceptor)(nil)
|
107
invoices/modification_interceptor_test.go
Normal file
107
invoices/modification_interceptor_test.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package invoices
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultTimeout = 50 * time.Millisecond
|
||||
)
|
||||
|
||||
// TestHtlcModificationInterceptor tests the basic functionality of the HTLC
|
||||
// modification interceptor.
|
||||
func TestHtlcModificationInterceptor(t *testing.T) {
|
||||
interceptor := NewHtlcModificationInterceptor()
|
||||
request := HtlcModifyRequest{
|
||||
WireCustomRecords: lnwire.CustomRecords{
|
||||
lnwire.MinCustomRecordsTlvType: []byte{1, 2, 3},
|
||||
},
|
||||
ExitHtlcCircuitKey: CircuitKey{
|
||||
ChanID: lnwire.NewShortChanIDFromInt(1),
|
||||
HtlcID: 1,
|
||||
},
|
||||
ExitHtlcAmt: 1234,
|
||||
}
|
||||
expectedResponse := HtlcModifyResponse{
|
||||
AmountPaid: 345,
|
||||
}
|
||||
interceptCallbackCalled := make(chan HtlcModifyRequest, 1)
|
||||
successInterceptCallback := func(
|
||||
req HtlcModifyRequest) (*HtlcModifyResponse, error) {
|
||||
|
||||
interceptCallbackCalled <- req
|
||||
|
||||
return &expectedResponse, nil
|
||||
}
|
||||
errorInterceptCallback := func(
|
||||
req HtlcModifyRequest) (*HtlcModifyResponse, error) {
|
||||
|
||||
interceptCallbackCalled <- req
|
||||
|
||||
return nil, fmt.Errorf("something went wrong")
|
||||
}
|
||||
responseCallbackCalled := make(chan HtlcModifyResponse, 1)
|
||||
responseCallback := func(resp HtlcModifyResponse) {
|
||||
responseCallbackCalled <- resp
|
||||
}
|
||||
|
||||
// Create a session without setting a callback first.
|
||||
err := interceptor.Intercept(request, responseCallback)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set the callback and create a new session.
|
||||
done, _, err := interceptor.RegisterInterceptor(
|
||||
successInterceptCallback,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = interceptor.Intercept(request, responseCallback)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The intercept callback should be called now.
|
||||
select {
|
||||
case req := <-interceptCallbackCalled:
|
||||
require.Equal(t, request, req)
|
||||
|
||||
case <-time.After(defaultTimeout):
|
||||
t.Fatal("intercept callback not called")
|
||||
}
|
||||
|
||||
// And the result should make it back to the response callback.
|
||||
select {
|
||||
case resp := <-responseCallbackCalled:
|
||||
require.Equal(t, expectedResponse, resp)
|
||||
|
||||
case <-time.After(defaultTimeout):
|
||||
t.Fatal("response callback not called")
|
||||
}
|
||||
|
||||
// If we try to set a new callback without first returning the previous
|
||||
// one, we should get an error.
|
||||
_, _, err = interceptor.RegisterInterceptor(successInterceptCallback)
|
||||
require.ErrorIs(t, err, ErrInterceptorClientAlreadyConnected)
|
||||
|
||||
// Reset the callback, then try to set a new one.
|
||||
done()
|
||||
done2, _, err := interceptor.RegisterInterceptor(errorInterceptCallback)
|
||||
require.NoError(t, err)
|
||||
defer done2()
|
||||
|
||||
// We should now get an error when intercepting.
|
||||
err = interceptor.Intercept(request, responseCallback)
|
||||
require.ErrorContains(t, err, "something went wrong")
|
||||
|
||||
// The success callback should not be called.
|
||||
select {
|
||||
case resp := <-responseCallbackCalled:
|
||||
t.Fatalf("unexpected response: %v", resp)
|
||||
|
||||
case <-time.After(defaultTimeout):
|
||||
// Expected.
|
||||
}
|
||||
}
|
|
@ -153,6 +153,7 @@ func defaultRegistryConfig() invpkg.RegistryConfig {
|
|||
return invpkg.RegistryConfig{
|
||||
FinalCltvRejectDelta: testFinalCltvRejectDelta,
|
||||
HtlcHoldDuration: 30 * time.Second,
|
||||
HtlcInterceptor: &invpkg.MockHtlcModifier{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,12 +21,20 @@ type invoiceUpdateCtx struct {
|
|||
expiry uint32
|
||||
currentHeight int32
|
||||
finalCltvRejectDelta int32
|
||||
customRecords record.CustomSet
|
||||
mpp *record.MPP
|
||||
amp *record.AMP
|
||||
metadata []byte
|
||||
pathID *chainhash.Hash
|
||||
totalAmtMsat lnwire.MilliSatoshi
|
||||
|
||||
// wireCustomRecords are the custom records that were included with the
|
||||
// HTLC wire message.
|
||||
wireCustomRecords lnwire.CustomRecords
|
||||
|
||||
// customRecords is a map of custom records that were included with the
|
||||
// HTLC onion payload.
|
||||
customRecords record.CustomSet
|
||||
|
||||
mpp *record.MPP
|
||||
amp *record.AMP
|
||||
metadata []byte
|
||||
pathID *chainhash.Hash
|
||||
totalAmtMsat lnwire.MilliSatoshi
|
||||
}
|
||||
|
||||
// invoiceRef returns an identifier that can be used to lookup or update the
|
||||
|
@ -95,12 +103,14 @@ func (i invoiceUpdateCtx) settleRes(preimage lntypes.Preimage,
|
|||
// acceptRes is a helper function which creates an accept resolution with
|
||||
// the information contained in the invoiceUpdateCtx and the accept resolution
|
||||
// result provided.
|
||||
func (i invoiceUpdateCtx) acceptRes(outcome acceptResolutionResult) *htlcAcceptResolution {
|
||||
func (i invoiceUpdateCtx) acceptRes(
|
||||
outcome acceptResolutionResult) *htlcAcceptResolution {
|
||||
|
||||
return newAcceptResolution(i.circuitKey, outcome)
|
||||
}
|
||||
|
||||
// updateInvoice is a callback for DB.UpdateInvoice that contains the invoice
|
||||
// settlement logic. It returns a hltc resolution that indicates what the
|
||||
// settlement logic. It returns a HTLC resolution that indicates what the
|
||||
// outcome of the update was.
|
||||
func updateInvoice(ctx *invoiceUpdateCtx, inv *Invoice) (
|
||||
*InvoiceUpdateDesc, HtlcResolution, error) {
|
||||
|
@ -119,7 +129,7 @@ func updateInvoice(ctx *invoiceUpdateCtx, inv *Invoice) (
|
|||
pre := inv.Terms.PaymentPreimage
|
||||
|
||||
// Terms.PaymentPreimage will be nil for AMP invoices.
|
||||
// Set it to the HTLC's AMP Preimage instead.
|
||||
// Set it to the HTLCs AMP Preimage instead.
|
||||
if pre == nil {
|
||||
pre = htlc.AMP.Preimage
|
||||
}
|
||||
|
@ -180,13 +190,19 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc,
|
|||
paymentAddr = ctx.pathID[:]
|
||||
}
|
||||
|
||||
// For storage, we don't really care where the custom records came from.
|
||||
// So we merge them together and store them in the same field.
|
||||
customRecords := lnwire.CustomRecords(
|
||||
ctx.customRecords,
|
||||
).MergedCopy(ctx.wireCustomRecords)
|
||||
|
||||
// Start building the accept descriptor.
|
||||
acceptDesc := &HtlcAcceptDesc{
|
||||
Amt: ctx.amtPaid,
|
||||
Expiry: ctx.expiry,
|
||||
AcceptHeight: ctx.currentHeight,
|
||||
MppTotalAmt: totalAmt,
|
||||
CustomRecords: ctx.customRecords,
|
||||
CustomRecords: record.CustomSet(customRecords),
|
||||
}
|
||||
|
||||
if ctx.amp != nil {
|
||||
|
@ -223,7 +239,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc,
|
|||
|
||||
htlcSet := inv.HTLCSet(setID, HtlcStateAccepted)
|
||||
|
||||
// Check whether total amt matches other htlcs in the set.
|
||||
// Check whether total amt matches other HTLCs in the set.
|
||||
var newSetTotal lnwire.MilliSatoshi
|
||||
for _, htlc := range htlcSet {
|
||||
if totalAmt != htlc.MppTotalAmt {
|
||||
|
@ -265,7 +281,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc,
|
|||
return &update, ctx.acceptRes(resultPartialAccepted), nil
|
||||
}
|
||||
|
||||
// Check to see if we can settle or this is an hold invoice and
|
||||
// Check to see if we can settle or this is a hold invoice, and
|
||||
// we need to wait for the preimage.
|
||||
if inv.HodlInvoice {
|
||||
update.State = &InvoiceStateUpdateDesc{
|
||||
|
@ -434,13 +450,19 @@ func updateLegacy(ctx *invoiceUpdateCtx,
|
|||
return nil, ctx.failRes(ResultExpiryTooSoon), nil
|
||||
}
|
||||
|
||||
// For storage, we don't really care where the custom records came from.
|
||||
// So we merge them together and store them in the same field.
|
||||
customRecords := lnwire.CustomRecords(
|
||||
ctx.customRecords,
|
||||
).MergedCopy(ctx.wireCustomRecords)
|
||||
|
||||
// Record HTLC in the invoice database.
|
||||
newHtlcs := map[CircuitKey]*HtlcAcceptDesc{
|
||||
ctx.circuitKey: {
|
||||
Amt: ctx.amtPaid,
|
||||
Expiry: ctx.expiry,
|
||||
AcceptHeight: ctx.currentHeight,
|
||||
CustomRecords: ctx.customRecords,
|
||||
CustomRecords: record.CustomSet(customRecords),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/record"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -37,98 +38,147 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "MPP accept",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: nil,
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: nil,
|
||||
},
|
||||
invState: ContractAccepted,
|
||||
setID: nil,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: nil,
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: nil,
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "MPP accept, copy custom records",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: record.CustomSet{
|
||||
0x01: []byte{0x02},
|
||||
0xffff: []byte{0x04, 0x05, 0x06},
|
||||
},
|
||||
WireCustomRecords: lnwire.CustomRecords{
|
||||
0x010101: []byte{0x02, 0x03},
|
||||
0xffffff: []byte{0x44, 0x55, 0x66},
|
||||
},
|
||||
AMP: nil,
|
||||
},
|
||||
invState: ContractAccepted,
|
||||
setID: nil,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: record.CustomSet{
|
||||
0x01: []byte{0x02},
|
||||
0xffff: []byte{0x04, 0x05, 0x06},
|
||||
},
|
||||
WireCustomRecords: lnwire.CustomRecords{
|
||||
0x010101: []byte{0x02, 0x03},
|
||||
0xffffff: []byte{0x44, 0x55, 0x66},
|
||||
},
|
||||
AMP: nil,
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "MPP settle",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: nil,
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: nil,
|
||||
},
|
||||
invState: ContractSettled,
|
||||
setID: nil,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: nil,
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: nil,
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "MPP cancel",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: nil,
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: nil,
|
||||
},
|
||||
invState: ContractCanceled,
|
||||
setID: nil,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
AMP: nil,
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: nil,
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "AMP accept missing preimage",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -138,14 +188,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractAccepted,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -157,14 +208,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "AMP accept invalid preimage",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -174,14 +226,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractAccepted,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -193,14 +246,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "AMP accept valid preimage",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -210,14 +264,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractAccepted,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -229,14 +284,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "AMP accept valid preimage different htlc set",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -246,14 +302,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractAccepted,
|
||||
setID: &diffSetID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -265,14 +322,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "AMP settle missing preimage",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -282,14 +340,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractSettled,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -301,14 +360,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "AMP settle invalid preimage",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -318,14 +378,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractSettled,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -337,14 +398,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "AMP settle valid preimage",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -354,14 +416,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractSettled,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -377,14 +440,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
// remain in the accepted state.
|
||||
name: "AMP settle valid preimage different htlc set",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -394,14 +458,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractSettled,
|
||||
setID: &diffSetID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -413,14 +478,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "accept invoice htlc already settled",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -430,14 +496,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractAccepted,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -449,14 +516,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "cancel invoice htlc already settled",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -466,14 +534,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractCanceled,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -485,14 +554,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "settle invoice htlc already settled",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -502,14 +572,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractSettled,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateSettled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -521,14 +592,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "cancel invoice",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: time.Time{},
|
||||
Expiry: 40,
|
||||
State: HtlcStateAccepted,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -538,14 +610,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractCanceled,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -557,14 +630,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "accept invoice htlc already canceled",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -574,14 +648,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractAccepted,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -593,14 +668,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "cancel invoice htlc already canceled",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -610,14 +686,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractCanceled,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -629,14 +706,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
{
|
||||
name: "settle invoice htlc already canceled",
|
||||
input: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
@ -646,14 +724,15 @@ func TestUpdateHTLC(t *testing.T) {
|
|||
invState: ContractSettled,
|
||||
setID: &setID,
|
||||
output: InvoiceHTLC{
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
Amt: 5000,
|
||||
MppTotalAmt: 5000,
|
||||
AcceptHeight: 100,
|
||||
AcceptTime: testNow,
|
||||
ResolveTime: testAlreadyNow,
|
||||
Expiry: 40,
|
||||
State: HtlcStateCanceled,
|
||||
CustomRecords: make(record.CustomSet),
|
||||
WireCustomRecords: make(lnwire.CustomRecords),
|
||||
AMP: &InvoiceHtlcAMPData{
|
||||
Record: *ampRecord,
|
||||
Hash: hash,
|
||||
|
|
|
@ -466,6 +466,10 @@ var allTestCases = []*lntest.TestCase{
|
|||
Name: "forward interceptor restart",
|
||||
TestFunc: testForwardInterceptorRestart,
|
||||
},
|
||||
{
|
||||
Name: "invoice HTLC modifier basic",
|
||||
TestFunc: testInvoiceHtlcModifierBasic,
|
||||
},
|
||||
{
|
||||
Name: "zero conf channel open",
|
||||
TestFunc: testZeroConfChannelOpen,
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/lntest/node"
|
||||
|
@ -374,8 +375,13 @@ func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) {
|
|||
// Connect an interceptor to Bob's node.
|
||||
bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
|
||||
|
||||
// We're going to modify the payment amount and want Carol to accept the
|
||||
// payment, so we set up an invoice acceptor on Dave.
|
||||
carolAcceptor, carolCancel := carol.RPC.InvoiceHtlcModifier()
|
||||
defer carolCancel()
|
||||
|
||||
// Prepare the test cases.
|
||||
invoiceValueAmtMsat := int64(1000)
|
||||
invoiceValueAmtMsat := int64(20_000_000)
|
||||
req := &lnrpc.Invoice{ValueMsat: invoiceValueAmtMsat}
|
||||
addResponse := carol.RPC.AddInvoice(req)
|
||||
invoice := carol.RPC.LookupInvoice(addResponse.RHash)
|
||||
|
@ -408,10 +414,10 @@ func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) {
|
|||
crValue := []byte("custom-records-test-value")
|
||||
customRecords[crKey] = crValue
|
||||
|
||||
// TODO(guggero): Actually modify the amount once we have the invoice
|
||||
// interceptor and can accept a lower amount.
|
||||
newOutAmountMsat := packet.OutgoingAmountMsat
|
||||
|
||||
// Modify the amount of the HTLC, so we send out less than the original
|
||||
// amount.
|
||||
const modifyAmount = 5_000_000
|
||||
newOutAmountMsat := packet.OutgoingAmountMsat - modifyAmount
|
||||
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||
IncomingCircuitKey: packet.IncomingCircuitKey,
|
||||
OutAmountMsat: newOutAmountMsat,
|
||||
|
@ -420,6 +426,17 @@ func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) {
|
|||
})
|
||||
require.NoError(ht, err, "failed to send request")
|
||||
|
||||
invoicePacket := ht.ReceiveInvoiceHtlcModification(carolAcceptor)
|
||||
require.EqualValues(
|
||||
ht, newOutAmountMsat, invoicePacket.ExitHtlcAmt,
|
||||
)
|
||||
amtPaid := newOutAmountMsat + modifyAmount
|
||||
err = carolAcceptor.Send(&invoicesrpc.HtlcModifyResponse{
|
||||
CircuitKey: invoicePacket.ExitHtlcCircuitKey,
|
||||
AmtPaid: &amtPaid,
|
||||
})
|
||||
require.NoError(ht, err, "carol acceptor response")
|
||||
|
||||
// Cancel the context, which will disconnect Bob's interceptor.
|
||||
cancelBobInterceptor()
|
||||
|
||||
|
@ -473,17 +490,23 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) {
|
|||
carolInterceptor, cancelCarolInterceptor := carol.RPC.HtlcInterceptor()
|
||||
defer cancelCarolInterceptor()
|
||||
|
||||
req := &lnrpc.Invoice{ValueMsat: 1000}
|
||||
// We're going to modify the payment amount and want Dave to accept the
|
||||
// payment, so we set up an invoice acceptor on Dave.
|
||||
daveAcceptor, daveCancel := dave.RPC.InvoiceHtlcModifier()
|
||||
defer daveCancel()
|
||||
|
||||
req := &lnrpc.Invoice{ValueMsat: 20_000_000}
|
||||
addResponse := dave.RPC.AddInvoice(req)
|
||||
invoice := dave.RPC.LookupInvoice(addResponse.RHash)
|
||||
|
||||
customRecords := map[uint64][]byte{
|
||||
65537: []byte("test"),
|
||||
}
|
||||
sendReq := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: invoice.PaymentRequest,
|
||||
TimeoutSeconds: int32(wait.PaymentTimeout.Seconds()),
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
FirstHopCustomRecords: map[uint64][]byte{
|
||||
65537: []byte("test"),
|
||||
},
|
||||
PaymentRequest: invoice.PaymentRequest,
|
||||
TimeoutSeconds: int32(wait.PaymentTimeout.Seconds()),
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
FirstHopCustomRecords: customRecords,
|
||||
}
|
||||
|
||||
_ = alice.RPC.SendPayment(sendReq)
|
||||
|
@ -499,14 +522,10 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) {
|
|||
require.True(ht, ok, "expected custom record")
|
||||
require.Equal(ht, []byte("test"), val)
|
||||
|
||||
// TODO(guggero): Actually modify the amount once we have the invoice
|
||||
// interceptor and can accept a lower amount.
|
||||
newOutAmountMsat := packet.OutgoingAmountMsat
|
||||
|
||||
// Just resume the payment on Bob.
|
||||
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||
IncomingCircuitKey: packet.IncomingCircuitKey,
|
||||
OutAmountMsat: newOutAmountMsat,
|
||||
Action: actionResumeModify,
|
||||
Action: actionResume,
|
||||
})
|
||||
require.NoError(ht, err, "failed to send request")
|
||||
|
||||
|
@ -515,13 +534,32 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) {
|
|||
packet = ht.ReceiveHtlcInterceptor(carolInterceptor)
|
||||
require.Len(ht, packet.InWireCustomRecords, 0)
|
||||
|
||||
// Just resume the payment on Carol.
|
||||
// We're going to tell Carol to forward 5k sats less to Dave. We need to
|
||||
// set custom records on the HTLC as well, to make sure the HTLC isn't
|
||||
// rejected outright and actually gets to the invoice acceptor.
|
||||
const modifyAmount = 5_000_000
|
||||
newOutAmountMsat := packet.OutgoingAmountMsat - modifyAmount
|
||||
err = carolInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||
IncomingCircuitKey: packet.IncomingCircuitKey,
|
||||
Action: actionResume,
|
||||
IncomingCircuitKey: packet.IncomingCircuitKey,
|
||||
OutAmountMsat: newOutAmountMsat,
|
||||
OutWireCustomRecords: customRecords,
|
||||
Action: actionResumeModify,
|
||||
})
|
||||
require.NoError(ht, err, "carol interceptor response")
|
||||
|
||||
// The payment should get to Dave, and we should be able to intercept
|
||||
// and modify it, telling Dave to accept it.
|
||||
invoicePacket := ht.ReceiveInvoiceHtlcModification(daveAcceptor)
|
||||
require.EqualValues(
|
||||
ht, newOutAmountMsat, invoicePacket.ExitHtlcAmt,
|
||||
)
|
||||
amtPaid := newOutAmountMsat + modifyAmount
|
||||
err = daveAcceptor.Send(&invoicesrpc.HtlcModifyResponse{
|
||||
CircuitKey: invoicePacket.ExitHtlcCircuitKey,
|
||||
AmtPaid: &amtPaid,
|
||||
})
|
||||
require.NoError(ht, err, "dave acceptor response")
|
||||
|
||||
// Assert that the payment was successful.
|
||||
var preimage lntypes.Preimage
|
||||
copy(preimage[:], invoice.RPreimage)
|
||||
|
|
358
itest/lnd_invoice_acceptor_test.go
Normal file
358
itest/lnd_invoice_acceptor_test.go
Normal file
|
@ -0,0 +1,358 @@
|
|||
package itest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/lntest/node"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testInvoiceHtlcModifierBasic tests the basic functionality of the invoice
|
||||
// HTLC modifier RPC server.
|
||||
func testInvoiceHtlcModifierBasic(ht *lntest.HarnessTest) {
|
||||
ts := newAcceptorTestScenario(ht)
|
||||
|
||||
alice, bob, carol := ts.alice, ts.bob, ts.carol
|
||||
|
||||
// Open and wait for channels.
|
||||
const chanAmt = btcutil.Amount(300000)
|
||||
p := lntest.OpenChannelParams{Amt: chanAmt}
|
||||
reqs := []*lntest.OpenChannelRequest{
|
||||
{Local: alice, Remote: bob, Param: p},
|
||||
{Local: bob, Remote: carol, Param: p},
|
||||
}
|
||||
resp := ht.OpenMultiChannelsAsync(reqs)
|
||||
cpAB, cpBC := resp[0], resp[1]
|
||||
|
||||
// Make sure Alice is aware of channel Bob=>Carol.
|
||||
ht.AssertTopologyChannelOpen(alice, cpBC)
|
||||
|
||||
// Initiate Carol's invoice HTLC modifier.
|
||||
invoiceModifier, cancelModifier := carol.RPC.InvoiceHtlcModifier()
|
||||
|
||||
// We need to wait a bit to make sure the gRPC stream is established
|
||||
// correctly.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Make sure we get an error if we try to register a second modifier and
|
||||
// then try to use it (the error won't be returned on connect, only on
|
||||
// the first _read_ interaction on the stream).
|
||||
mod2, err := carol.RPC.Invoice.HtlcModifier(context.Background())
|
||||
require.NoError(ht, err)
|
||||
_, err = mod2.Recv()
|
||||
require.ErrorContains(
|
||||
ht, err,
|
||||
invoices.ErrInterceptorClientAlreadyConnected.Error(),
|
||||
)
|
||||
|
||||
// We also add a normal (forwarding) HTLC interceptor at Bob, so we can
|
||||
// test that custom wire messages on the HTLC are forwarded correctly to
|
||||
// the invoice HTLC interceptor.
|
||||
bobInterceptor, bobInterceptorCancel := bob.RPC.HtlcInterceptor()
|
||||
|
||||
// Prepare the test cases.
|
||||
testCases := ts.prepareTestCases()
|
||||
|
||||
for tcIdx, tc := range testCases {
|
||||
ht.Logf("Running test case: %d", tcIdx)
|
||||
|
||||
// Initiate a payment from Alice to Carol in a separate
|
||||
// goroutine. We use a separate goroutine to avoid blocking the
|
||||
// main goroutine where we will make use of the invoice
|
||||
// acceptor.
|
||||
sendPaymentDone := make(chan struct{})
|
||||
go func() {
|
||||
// Signal that all the payments have been sent.
|
||||
defer close(sendPaymentDone)
|
||||
|
||||
_ = ts.sendPayment(tc)
|
||||
}()
|
||||
|
||||
// First, intercept the HTLC at Bob.
|
||||
packet := ht.ReceiveHtlcInterceptor(bobInterceptor)
|
||||
err := bobInterceptor.Send(
|
||||
&routerrpc.ForwardHtlcInterceptResponse{
|
||||
IncomingCircuitKey: packet.IncomingCircuitKey,
|
||||
OutAmountMsat: packet.OutgoingAmountMsat,
|
||||
OutWireCustomRecords: tc.lastHopCustomRecords,
|
||||
Action: actionResumeModify,
|
||||
},
|
||||
)
|
||||
require.NoError(ht, err, "failed to send request")
|
||||
|
||||
modifierRequest := ht.ReceiveInvoiceHtlcModification(
|
||||
invoiceModifier,
|
||||
)
|
||||
|
||||
// Sanity check the modifier request.
|
||||
require.EqualValues(
|
||||
ht, tc.invoiceAmountMsat,
|
||||
modifierRequest.Invoice.ValueMsat,
|
||||
)
|
||||
require.EqualValues(
|
||||
ht, tc.sendAmountMsat, modifierRequest.ExitHtlcAmt,
|
||||
)
|
||||
require.Equal(
|
||||
ht, tc.lastHopCustomRecords,
|
||||
modifierRequest.ExitHtlcWireCustomRecords,
|
||||
)
|
||||
|
||||
// For all other packets we resolve according to the test case.
|
||||
amtPaid := uint64(tc.invoiceAmountMsat)
|
||||
err = invoiceModifier.Send(
|
||||
&invoicesrpc.HtlcModifyResponse{
|
||||
CircuitKey: modifierRequest.ExitHtlcCircuitKey,
|
||||
AmtPaid: &amtPaid,
|
||||
},
|
||||
)
|
||||
require.NoError(ht, err, "failed to send request")
|
||||
|
||||
ht.Log("Waiting for payment send to complete")
|
||||
select {
|
||||
case <-sendPaymentDone:
|
||||
ht.Log("Payment send attempt complete")
|
||||
case <-time.After(defaultTimeout):
|
||||
require.Fail(ht, "timeout waiting for payment send")
|
||||
}
|
||||
|
||||
ht.Log("Ensure invoice status is settled")
|
||||
require.Eventually(ht, func() bool {
|
||||
updatedInvoice := carol.RPC.LookupInvoice(
|
||||
tc.invoice.RHash,
|
||||
)
|
||||
|
||||
return updatedInvoice.State == tc.finalInvoiceState
|
||||
}, defaultTimeout, 1*time.Second)
|
||||
|
||||
updatedInvoice := carol.RPC.LookupInvoice(
|
||||
tc.invoice.RHash,
|
||||
)
|
||||
|
||||
require.Len(ht, updatedInvoice.Htlcs, 1)
|
||||
require.Equal(
|
||||
ht, tc.lastHopCustomRecords,
|
||||
updatedInvoice.Htlcs[0].CustomRecords,
|
||||
)
|
||||
|
||||
// Make sure the custom channel data contains the encoded
|
||||
// version of the custom records.
|
||||
customRecords := lnwire.CustomRecords(
|
||||
updatedInvoice.Htlcs[0].CustomRecords,
|
||||
)
|
||||
encodedRecords, err := customRecords.Serialize()
|
||||
require.NoError(ht, err)
|
||||
|
||||
require.Equal(
|
||||
ht, encodedRecords,
|
||||
updatedInvoice.Htlcs[0].CustomChannelData,
|
||||
)
|
||||
}
|
||||
|
||||
// We don't need the HTLC interceptor at Bob anymore.
|
||||
bobInterceptorCancel()
|
||||
|
||||
// After the normal test cases, we test that we can shut down Carol
|
||||
// while an HTLC interception is going on. We initiate a payment from
|
||||
// Alice to Carol in a separate goroutine. We use a separate goroutine
|
||||
// to avoid blocking the main goroutine where we will make use of the
|
||||
// invoice acceptor.
|
||||
sendPaymentDone := make(chan struct{})
|
||||
tc := &acceptorTestCase{
|
||||
invoiceAmountMsat: 9000,
|
||||
sendAmountMsat: 9000,
|
||||
}
|
||||
ts.createInvoice(tc)
|
||||
|
||||
go func() {
|
||||
// Signal that all the payments have been sent.
|
||||
defer close(sendPaymentDone)
|
||||
|
||||
_ = ts.sendPayment(tc)
|
||||
}()
|
||||
|
||||
modifierRequest := ht.ReceiveInvoiceHtlcModification(invoiceModifier)
|
||||
|
||||
// Sanity check the modifier request.
|
||||
require.EqualValues(
|
||||
ht, tc.invoiceAmountMsat, modifierRequest.Invoice.ValueMsat,
|
||||
)
|
||||
require.EqualValues(
|
||||
ht, tc.sendAmountMsat, modifierRequest.ExitHtlcAmt,
|
||||
)
|
||||
|
||||
// We don't send a response to the modifier, but instead shut down and
|
||||
// restart Carol.
|
||||
restart := ht.SuspendNode(carol)
|
||||
require.NoError(ht, restart())
|
||||
|
||||
ht.Log("Waiting for payment send to complete")
|
||||
select {
|
||||
case <-sendPaymentDone:
|
||||
ht.Log("Payment send attempt complete")
|
||||
case <-time.After(defaultTimeout):
|
||||
require.Fail(ht, "timeout waiting for payment send")
|
||||
}
|
||||
|
||||
cancelModifier()
|
||||
|
||||
// Finally, close channels.
|
||||
ht.CloseChannel(alice, cpAB)
|
||||
ht.CloseChannel(bob, cpBC)
|
||||
}
|
||||
|
||||
// acceptorTestCase is a helper struct to hold test case data.
|
||||
type acceptorTestCase struct {
|
||||
// invoiceAmountMsat is the amount of the invoice.
|
||||
invoiceAmountMsat int64
|
||||
|
||||
// sendAmountMsat is the amount that will be sent in the payment.
|
||||
sendAmountMsat int64
|
||||
|
||||
// lastHopCustomRecords is a map of custom records that will be added
|
||||
// to the last hop of the payment, by an HTLC interceptor at Bob.
|
||||
lastHopCustomRecords map[uint64][]byte
|
||||
|
||||
// finalInvoiceState is the expected eventual final state of the
|
||||
// invoice.
|
||||
finalInvoiceState lnrpc.Invoice_InvoiceState
|
||||
|
||||
// payAddr is the payment address of the invoice.
|
||||
payAddr []byte
|
||||
|
||||
// invoice is the invoice that will be paid.
|
||||
invoice *lnrpc.Invoice
|
||||
}
|
||||
|
||||
// acceptorTestScenario is a helper struct to hold the test context and provides
|
||||
// helpful functionality.
|
||||
type acceptorTestScenario struct {
|
||||
ht *lntest.HarnessTest
|
||||
alice, bob, carol *node.HarnessNode
|
||||
}
|
||||
|
||||
// newAcceptorTestScenario initializes a new test scenario with three nodes and
|
||||
// connects them to have the following topology,
|
||||
//
|
||||
// Alice --> Bob --> Carol
|
||||
//
|
||||
// Among them, Alice and Bob are standby nodes and Carol is a new node.
|
||||
func newAcceptorTestScenario(ht *lntest.HarnessTest) *acceptorTestScenario {
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
carol := ht.NewNode("carol", nil)
|
||||
|
||||
ht.EnsureConnected(alice, bob)
|
||||
ht.EnsureConnected(bob, carol)
|
||||
|
||||
return &acceptorTestScenario{
|
||||
ht: ht,
|
||||
alice: alice,
|
||||
bob: bob,
|
||||
carol: carol,
|
||||
}
|
||||
}
|
||||
|
||||
// prepareTestCases prepares test cases.
|
||||
func (c *acceptorTestScenario) prepareTestCases() []*acceptorTestCase {
|
||||
cases := []*acceptorTestCase{
|
||||
// Send a payment with amount less than the invoice amount.
|
||||
// Amount checking is skipped during the invoice settlement
|
||||
// process. The sent payment should eventually result in the
|
||||
// invoice being settled.
|
||||
{
|
||||
invoiceAmountMsat: 9000,
|
||||
sendAmountMsat: 1000,
|
||||
finalInvoiceState: lnrpc.Invoice_SETTLED,
|
||||
},
|
||||
{
|
||||
invoiceAmountMsat: 9000,
|
||||
sendAmountMsat: 1000,
|
||||
finalInvoiceState: lnrpc.Invoice_SETTLED,
|
||||
lastHopCustomRecords: map[uint64][]byte{
|
||||
lnwire.MinCustomRecordsTlvType: {1, 2, 3},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, t := range cases {
|
||||
c.createInvoice(t)
|
||||
}
|
||||
|
||||
return cases
|
||||
}
|
||||
|
||||
// createInvoice creates an invoice for the given test case.
|
||||
func (c *acceptorTestScenario) createInvoice(tc *acceptorTestCase) {
|
||||
inv := &lnrpc.Invoice{ValueMsat: tc.invoiceAmountMsat}
|
||||
addResponse := c.carol.RPC.AddInvoice(inv)
|
||||
invoice := c.carol.RPC.LookupInvoice(addResponse.RHash)
|
||||
|
||||
// We'll need to also decode the returned invoice so we can grab the
|
||||
// payment address which is now required for ALL payments.
|
||||
payReq := c.carol.RPC.DecodePayReq(invoice.PaymentRequest)
|
||||
|
||||
tc.invoice = invoice
|
||||
tc.payAddr = payReq.PaymentAddr
|
||||
}
|
||||
|
||||
// buildRoute is a helper function to build a route with given hops.
|
||||
func (c *acceptorTestScenario) buildRoute(amtMsat int64,
|
||||
hops []*node.HarnessNode, payAddr []byte) *lnrpc.Route {
|
||||
|
||||
rpcHops := make([][]byte, 0, len(hops))
|
||||
for _, hop := range hops {
|
||||
k := hop.PubKeyStr
|
||||
pubkey, err := route.NewVertexFromStr(k)
|
||||
require.NoErrorf(c.ht, err, "error parsing %v: %v", k, err)
|
||||
rpcHops = append(rpcHops, pubkey[:])
|
||||
}
|
||||
|
||||
req := &routerrpc.BuildRouteRequest{
|
||||
AmtMsat: amtMsat,
|
||||
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
||||
HopPubkeys: rpcHops,
|
||||
PaymentAddr: payAddr,
|
||||
}
|
||||
|
||||
routeResp := c.alice.RPC.BuildRoute(req)
|
||||
|
||||
return routeResp.Route
|
||||
}
|
||||
|
||||
// sendPaymentAndAssertAction sends a payment from alice to carol.
|
||||
func (c *acceptorTestScenario) sendPayment(
|
||||
tc *acceptorTestCase) *lnrpc.HTLCAttempt {
|
||||
|
||||
// Build a route from alice to carol.
|
||||
aliceBobCarolRoute := c.buildRoute(
|
||||
tc.sendAmountMsat, []*node.HarnessNode{c.bob, c.carol},
|
||||
tc.payAddr,
|
||||
)
|
||||
|
||||
// We need to cheat a bit. We are attempting to pay an invoice with
|
||||
// amount X with an HTLC of amount Y that is less than X. And then we
|
||||
// use the invoice HTLC interceptor to simulate the HTLC actually
|
||||
// carrying amount X (even though the actual HTLC transaction output
|
||||
// only has amount Y). But in order for the invoice to be settled, we
|
||||
// need to make sure that the MPP total amount record in the last hop
|
||||
// is set to the invoice amount. This would also be the case in a normal
|
||||
// MPP payment, where each shard only pays a fraction of the invoice.
|
||||
aliceBobCarolRoute.Hops[1].MppRecord.TotalAmtMsat = tc.invoiceAmountMsat
|
||||
|
||||
// Send the payment.
|
||||
sendReq := &routerrpc.SendToRouteRequest{
|
||||
PaymentHash: tc.invoice.RHash,
|
||||
Route: aliceBobCarolRoute,
|
||||
}
|
||||
|
||||
return c.alice.RPC.SendToRouteV2(sendReq)
|
||||
}
|
1
lnd.go
1
lnd.go
|
@ -638,6 +638,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
|
|||
err = rpcServer.addDeps(
|
||||
server, interceptorChain.MacaroonService(), cfg.SubRPCServers,
|
||||
atplManager, server.invoices, tower, multiAcceptor,
|
||||
server.invoiceHtlcModifier,
|
||||
)
|
||||
if err != nil {
|
||||
return mkErr("unable to add deps to RPC server: %v", err)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/netann"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// Config is the primary configuration struct for the invoices RPC server. It
|
||||
|
@ -30,6 +31,11 @@ type Config struct {
|
|||
// created by the daemon.
|
||||
InvoiceRegistry *invoices.InvoiceRegistry
|
||||
|
||||
// HtlcModifier is a service which intercepts invoice HTLCs during the
|
||||
// settlement phase, enabling a subscribed client to modify certain
|
||||
// aspects of those HTLCs.
|
||||
HtlcModifier invoices.HtlcModifier
|
||||
|
||||
// IsChannelActive is used to generate valid hop hints.
|
||||
IsChannelActive func(chanID lnwire.ChannelID) bool
|
||||
|
||||
|
@ -64,4 +70,8 @@ type Config struct {
|
|||
// GetAlias returns the peer's alias SCID if it exists given the
|
||||
// 32-byte ChannelID.
|
||||
GetAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error)
|
||||
|
||||
// ParseAuxData is a function that can be used to parse the auxiliary
|
||||
// data from the invoice.
|
||||
ParseAuxData func(message proto.Message) error
|
||||
}
|
||||
|
|
86
lnrpc/invoicesrpc/htlc_modifier.go
Normal file
86
lnrpc/invoicesrpc/htlc_modifier.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package invoicesrpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
// htlcModifier is a helper struct that handles the lifecycle of an RPC invoice
|
||||
// HTLC modifier server instance.
|
||||
//
|
||||
// This struct handles passing send and receive RPC messages between the client
|
||||
// and the invoice service.
|
||||
type htlcModifier struct {
|
||||
// chainParams is required to properly marshall an invoice for RPC.
|
||||
chainParams *chaincfg.Params
|
||||
|
||||
// serverStream is a bidirectional RPC server stream to send invoices to
|
||||
// a client and receive accept responses from the client.
|
||||
serverStream Invoices_HtlcModifierServer
|
||||
}
|
||||
|
||||
// newHtlcModifier creates a new RPC invoice HTLC modifier handler.
|
||||
func newHtlcModifier(params *chaincfg.Params,
|
||||
serverStream Invoices_HtlcModifierServer) *htlcModifier {
|
||||
|
||||
return &htlcModifier{
|
||||
chainParams: params,
|
||||
serverStream: serverStream,
|
||||
}
|
||||
}
|
||||
|
||||
// onIntercept is called when an invoice HTLC is intercepted by the invoice HTLC
|
||||
// modifier. This method sends the invoice and the current HTLC to the client.
|
||||
func (r *htlcModifier) onIntercept(
|
||||
req invoices.HtlcModifyRequest) (*invoices.HtlcModifyResponse, error) {
|
||||
|
||||
// Convert the circuit key to an RPC circuit key.
|
||||
rpcCircuitKey := &CircuitKey{
|
||||
ChanId: req.ExitHtlcCircuitKey.ChanID.ToUint64(),
|
||||
HtlcId: req.ExitHtlcCircuitKey.HtlcID,
|
||||
}
|
||||
|
||||
// Convert the invoice to an RPC invoice.
|
||||
rpcInvoice, err := CreateRPCInvoice(&req.Invoice, r.chainParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Send the modification request to the client.
|
||||
err = r.serverStream.Send(&HtlcModifyRequest{
|
||||
Invoice: rpcInvoice,
|
||||
ExitHtlcCircuitKey: rpcCircuitKey,
|
||||
ExitHtlcAmt: uint64(req.ExitHtlcAmt),
|
||||
ExitHtlcExpiry: req.ExitHtlcExpiry,
|
||||
CurrentHeight: req.CurrentHeight,
|
||||
ExitHtlcWireCustomRecords: req.WireCustomRecords,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Then wait for the client to respond.
|
||||
resp, err := r.serverStream.Recv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.CircuitKey == nil {
|
||||
return nil, fmt.Errorf("missing circuit key")
|
||||
}
|
||||
|
||||
log.Tracef("Resolving invoice HTLC modifier response %v", resp)
|
||||
|
||||
// Pass the resolution to the modifier.
|
||||
var amtPaid lnwire.MilliSatoshi
|
||||
if resp.AmtPaid != nil {
|
||||
amtPaid = lnwire.MilliSatoshi(*resp.AmtPaid)
|
||||
}
|
||||
|
||||
return &invoices.HtlcModifyResponse{
|
||||
AmountPaid: amtPaid,
|
||||
}, nil
|
||||
}
|
|
@ -617,6 +617,219 @@ func (*LookupInvoiceMsg_PaymentAddr) isLookupInvoiceMsg_InvoiceRef() {}
|
|||
|
||||
func (*LookupInvoiceMsg_SetId) isLookupInvoiceMsg_InvoiceRef() {}
|
||||
|
||||
// CircuitKey is a unique identifier for an HTLC.
|
||||
type CircuitKey struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The id of the channel that the is part of this circuit.
|
||||
ChanId uint64 `protobuf:"varint,1,opt,name=chan_id,json=chanId,proto3" json:"chan_id,omitempty"`
|
||||
// The index of the incoming htlc in the incoming channel.
|
||||
HtlcId uint64 `protobuf:"varint,2,opt,name=htlc_id,json=htlcId,proto3" json:"htlc_id,omitempty"`
|
||||
}
|
||||
|
||||
func (x *CircuitKey) Reset() {
|
||||
*x = CircuitKey{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_invoicesrpc_invoices_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *CircuitKey) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CircuitKey) ProtoMessage() {}
|
||||
|
||||
func (x *CircuitKey) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_invoicesrpc_invoices_proto_msgTypes[8]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CircuitKey.ProtoReflect.Descriptor instead.
|
||||
func (*CircuitKey) Descriptor() ([]byte, []int) {
|
||||
return file_invoicesrpc_invoices_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *CircuitKey) GetChanId() uint64 {
|
||||
if x != nil {
|
||||
return x.ChanId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *CircuitKey) GetHtlcId() uint64 {
|
||||
if x != nil {
|
||||
return x.HtlcId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type HtlcModifyRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The invoice the intercepted HTLC is attempting to settle. The HTLCs in
|
||||
// the invoice are only HTLCs that have already been accepted or settled,
|
||||
// not including the current intercepted HTLC.
|
||||
Invoice *lnrpc.Invoice `protobuf:"bytes,1,opt,name=invoice,proto3" json:"invoice,omitempty"`
|
||||
// The unique identifier of the HTLC of this intercepted HTLC.
|
||||
ExitHtlcCircuitKey *CircuitKey `protobuf:"bytes,2,opt,name=exit_htlc_circuit_key,json=exitHtlcCircuitKey,proto3" json:"exit_htlc_circuit_key,omitempty"`
|
||||
// The amount in milli-satoshi that the exit HTLC is attempting to pay.
|
||||
ExitHtlcAmt uint64 `protobuf:"varint,3,opt,name=exit_htlc_amt,json=exitHtlcAmt,proto3" json:"exit_htlc_amt,omitempty"`
|
||||
// The absolute expiry height of the exit HTLC.
|
||||
ExitHtlcExpiry uint32 `protobuf:"varint,4,opt,name=exit_htlc_expiry,json=exitHtlcExpiry,proto3" json:"exit_htlc_expiry,omitempty"`
|
||||
// The current block height.
|
||||
CurrentHeight uint32 `protobuf:"varint,5,opt,name=current_height,json=currentHeight,proto3" json:"current_height,omitempty"`
|
||||
// The wire message custom records of the exit HTLC.
|
||||
ExitHtlcWireCustomRecords map[uint64][]byte `protobuf:"bytes,6,rep,name=exit_htlc_wire_custom_records,json=exitHtlcWireCustomRecords,proto3" json:"exit_htlc_wire_custom_records,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *HtlcModifyRequest) Reset() {
|
||||
*x = HtlcModifyRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_invoicesrpc_invoices_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *HtlcModifyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*HtlcModifyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *HtlcModifyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_invoicesrpc_invoices_proto_msgTypes[9]
|
||||
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 HtlcModifyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*HtlcModifyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_invoicesrpc_invoices_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *HtlcModifyRequest) GetInvoice() *lnrpc.Invoice {
|
||||
if x != nil {
|
||||
return x.Invoice
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *HtlcModifyRequest) GetExitHtlcCircuitKey() *CircuitKey {
|
||||
if x != nil {
|
||||
return x.ExitHtlcCircuitKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *HtlcModifyRequest) GetExitHtlcAmt() uint64 {
|
||||
if x != nil {
|
||||
return x.ExitHtlcAmt
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *HtlcModifyRequest) GetExitHtlcExpiry() uint32 {
|
||||
if x != nil {
|
||||
return x.ExitHtlcExpiry
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *HtlcModifyRequest) GetCurrentHeight() uint32 {
|
||||
if x != nil {
|
||||
return x.CurrentHeight
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *HtlcModifyRequest) GetExitHtlcWireCustomRecords() map[uint64][]byte {
|
||||
if x != nil {
|
||||
return x.ExitHtlcWireCustomRecords
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type HtlcModifyResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The circuit key of the HTLC that the client wants to modify.
|
||||
CircuitKey *CircuitKey `protobuf:"bytes,1,opt,name=circuit_key,json=circuitKey,proto3" json:"circuit_key,omitempty"`
|
||||
// The modified amount in milli-satoshi that the exit HTLC is paying. This
|
||||
// value can be different from the actual on-chain HTLC amount, in case the
|
||||
// HTLC carries other valuable items, as can be the case with custom channel
|
||||
// types.
|
||||
AmtPaid *uint64 `protobuf:"varint,2,opt,name=amt_paid,json=amtPaid,proto3,oneof" json:"amt_paid,omitempty"`
|
||||
}
|
||||
|
||||
func (x *HtlcModifyResponse) Reset() {
|
||||
*x = HtlcModifyResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_invoicesrpc_invoices_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *HtlcModifyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*HtlcModifyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *HtlcModifyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_invoicesrpc_invoices_proto_msgTypes[10]
|
||||
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 HtlcModifyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*HtlcModifyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_invoicesrpc_invoices_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *HtlcModifyResponse) GetCircuitKey() *CircuitKey {
|
||||
if x != nil {
|
||||
return x.CircuitKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *HtlcModifyResponse) GetAmtPaid() uint64 {
|
||||
if x != nil && x.AmtPaid != nil {
|
||||
return *x.AmtPaid
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_invoicesrpc_invoices_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_invoicesrpc_invoices_proto_rawDesc = []byte{
|
||||
|
@ -678,41 +891,87 @@ var file_invoicesrpc_invoices_proto_rawDesc = []byte{
|
|||
0x73, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4d, 0x6f, 0x64, 0x69, 0x66,
|
||||
0x69, 0x65, 0x72, 0x52, 0x0e, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4d, 0x6f, 0x64, 0x69, 0x66,
|
||||
0x69, 0x65, 0x72, 0x42, 0x0d, 0x0a, 0x0b, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x5f, 0x72,
|
||||
0x65, 0x66, 0x2a, 0x44, 0x0a, 0x0e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4d, 0x6f, 0x64, 0x69,
|
||||
0x66, 0x69, 0x65, 0x72, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10,
|
||||
0x00, 0x12, 0x11, 0x0a, 0x0d, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x4f, 0x4e,
|
||||
0x4c, 0x59, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x54,
|
||||
0x5f, 0x42, 0x4c, 0x41, 0x4e, 0x4b, 0x10, 0x02, 0x32, 0x9b, 0x03, 0x0a, 0x08, 0x49, 0x6e, 0x76,
|
||||
0x6f, 0x69, 0x63, 0x65, 0x73, 0x12, 0x56, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69,
|
||||
0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12,
|
||||
0x2a, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75,
|
||||
0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76,
|
||||
0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e,
|
||||
0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x30, 0x01, 0x12, 0x4e, 0x0a,
|
||||
0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1d,
|
||||
0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e,
|
||||
0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x1e, 0x2e,
|
||||
0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63,
|
||||
0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x55, 0x0a,
|
||||
0x0e, 0x41, 0x64, 0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12,
|
||||
0x22, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64,
|
||||
0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70,
|
||||
0x63, 0x2e, 0x41, 0x64, 0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65,
|
||||
0x52, 0x65, 0x73, 0x70, 0x12, 0x4e, 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e,
|
||||
0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73,
|
||||
0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63,
|
||||
0x65, 0x4d, 0x73, 0x67, 0x1a, 0x1e, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72,
|
||||
0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65,
|
||||
0x52, 0x65, 0x73, 0x70, 0x12, 0x40, 0x0a, 0x0f, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, 0x6e,
|
||||
0x76, 0x6f, 0x69, 0x63, 0x65, 0x56, 0x32, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63,
|
||||
0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, 0x6e, 0x76, 0x6f,
|
||||
0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49,
|
||||
0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x42, 0x33, 0x5a, 0x31, 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,
|
||||
0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x33,
|
||||
0x65, 0x66, 0x22, 0x3e, 0x0a, 0x0a, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x4b, 0x65, 0x79,
|
||||
0x12, 0x17, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x04, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x74, 0x6c,
|
||||
0x63, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x74, 0x6c, 0x63,
|
||||
0x49, 0x64, 0x22, 0xcd, 0x03, 0x0a, 0x11, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x6f, 0x64, 0x69, 0x66,
|
||||
0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x69, 0x6e, 0x76, 0x6f,
|
||||
0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70,
|
||||
0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x07, 0x69, 0x6e, 0x76, 0x6f, 0x69,
|
||||
0x63, 0x65, 0x12, 0x4a, 0x0a, 0x15, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f,
|
||||
0x63, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e,
|
||||
0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x12, 0x65, 0x78, 0x69, 0x74,
|
||||
0x48, 0x74, 0x6c, 0x63, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x22,
|
||||
0x0a, 0x0d, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x61, 0x6d, 0x74, 0x18,
|
||||
0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x65, 0x78, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x41,
|
||||
0x6d, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f,
|
||||
0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x65, 0x78,
|
||||
0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x25, 0x0a, 0x0e,
|
||||
0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x05,
|
||||
0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x48, 0x65, 0x69,
|
||||
0x67, 0x68, 0x74, 0x12, 0x7f, 0x0a, 0x1d, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x68, 0x74, 0x6c, 0x63,
|
||||
0x5f, 0x77, 0x69, 0x72, 0x65, 0x5f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x72, 0x65, 0x63,
|
||||
0x6f, 0x72, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x69, 0x6e, 0x76,
|
||||
0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x6f, 0x64,
|
||||
0x69, 0x66, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x45, 0x78, 0x69, 0x74, 0x48,
|
||||
0x74, 0x6c, 0x63, 0x57, 0x69, 0x72, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63,
|
||||
0x6f, 0x72, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x19, 0x65, 0x78, 0x69, 0x74, 0x48,
|
||||
0x74, 0x6c, 0x63, 0x57, 0x69, 0x72, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63,
|
||||
0x6f, 0x72, 0x64, 0x73, 0x1a, 0x4c, 0x0a, 0x1e, 0x45, 0x78, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63,
|
||||
0x57, 0x69, 0x72, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
|
||||
0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
|
||||
0x38, 0x01, 0x22, 0x7b, 0x0a, 0x12, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x0b, 0x63, 0x69, 0x72, 0x63,
|
||||
0x75, 0x69, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e,
|
||||
0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x69, 0x72, 0x63,
|
||||
0x75, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x63, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x4b,
|
||||
0x65, 0x79, 0x12, 0x1e, 0x0a, 0x08, 0x61, 0x6d, 0x74, 0x5f, 0x70, 0x61, 0x69, 0x64, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x07, 0x61, 0x6d, 0x74, 0x50, 0x61, 0x69, 0x64, 0x88,
|
||||
0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x61, 0x6d, 0x74, 0x5f, 0x70, 0x61, 0x69, 0x64, 0x2a,
|
||||
0x44, 0x0a, 0x0e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65,
|
||||
0x72, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x11,
|
||||
0x0a, 0x0d, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10,
|
||||
0x01, 0x12, 0x12, 0x0a, 0x0e, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x42, 0x4c,
|
||||
0x41, 0x4e, 0x4b, 0x10, 0x02, 0x32, 0xf0, 0x03, 0x0a, 0x08, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63,
|
||||
0x65, 0x73, 0x12, 0x56, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53,
|
||||
0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x2a, 0x2e, 0x69,
|
||||
0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63,
|
||||
0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63,
|
||||
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x30, 0x01, 0x12, 0x4e, 0x0a, 0x0d, 0x43, 0x61,
|
||||
0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1d, 0x2e, 0x69, 0x6e,
|
||||
0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c,
|
||||
0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x1e, 0x2e, 0x69, 0x6e, 0x76,
|
||||
0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49,
|
||||
0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x55, 0x0a, 0x0e, 0x41, 0x64,
|
||||
0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x22, 0x2e, 0x69,
|
||||
0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x48, 0x6f,
|
||||
0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x1f, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x41,
|
||||
0x64, 0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73,
|
||||
0x70, 0x12, 0x4e, 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69,
|
||||
0x63, 0x65, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x4d, 0x73,
|
||||
0x67, 0x1a, 0x1e, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e,
|
||||
0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73,
|
||||
0x70, 0x12, 0x40, 0x0a, 0x0f, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, 0x6e, 0x76, 0x6f, 0x69,
|
||||
0x63, 0x65, 0x56, 0x32, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72,
|
||||
0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65,
|
||||
0x4d, 0x73, 0x67, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f,
|
||||
0x69, 0x63, 0x65, 0x12, 0x53, 0x0a, 0x0c, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x6f, 0x64, 0x69, 0x66,
|
||||
0x69, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70,
|
||||
0x63, 0x2e, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x1e, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72,
|
||||
0x70, 0x63, 0x2e, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x42, 0x33, 0x5a, 0x31, 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, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -728,7 +987,7 @@ func file_invoicesrpc_invoices_proto_rawDescGZIP() []byte {
|
|||
}
|
||||
|
||||
var file_invoicesrpc_invoices_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_invoicesrpc_invoices_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
var file_invoicesrpc_invoices_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||
var file_invoicesrpc_invoices_proto_goTypes = []interface{}{
|
||||
(LookupModifier)(0), // 0: invoicesrpc.LookupModifier
|
||||
(*CancelInvoiceMsg)(nil), // 1: invoicesrpc.CancelInvoiceMsg
|
||||
|
@ -739,27 +998,37 @@ var file_invoicesrpc_invoices_proto_goTypes = []interface{}{
|
|||
(*SettleInvoiceResp)(nil), // 6: invoicesrpc.SettleInvoiceResp
|
||||
(*SubscribeSingleInvoiceRequest)(nil), // 7: invoicesrpc.SubscribeSingleInvoiceRequest
|
||||
(*LookupInvoiceMsg)(nil), // 8: invoicesrpc.LookupInvoiceMsg
|
||||
(*lnrpc.RouteHint)(nil), // 9: lnrpc.RouteHint
|
||||
(*lnrpc.Invoice)(nil), // 10: lnrpc.Invoice
|
||||
(*CircuitKey)(nil), // 9: invoicesrpc.CircuitKey
|
||||
(*HtlcModifyRequest)(nil), // 10: invoicesrpc.HtlcModifyRequest
|
||||
(*HtlcModifyResponse)(nil), // 11: invoicesrpc.HtlcModifyResponse
|
||||
nil, // 12: invoicesrpc.HtlcModifyRequest.ExitHtlcWireCustomRecordsEntry
|
||||
(*lnrpc.RouteHint)(nil), // 13: lnrpc.RouteHint
|
||||
(*lnrpc.Invoice)(nil), // 14: lnrpc.Invoice
|
||||
}
|
||||
var file_invoicesrpc_invoices_proto_depIdxs = []int32{
|
||||
9, // 0: invoicesrpc.AddHoldInvoiceRequest.route_hints:type_name -> lnrpc.RouteHint
|
||||
13, // 0: invoicesrpc.AddHoldInvoiceRequest.route_hints:type_name -> lnrpc.RouteHint
|
||||
0, // 1: invoicesrpc.LookupInvoiceMsg.lookup_modifier:type_name -> invoicesrpc.LookupModifier
|
||||
7, // 2: invoicesrpc.Invoices.SubscribeSingleInvoice:input_type -> invoicesrpc.SubscribeSingleInvoiceRequest
|
||||
1, // 3: invoicesrpc.Invoices.CancelInvoice:input_type -> invoicesrpc.CancelInvoiceMsg
|
||||
3, // 4: invoicesrpc.Invoices.AddHoldInvoice:input_type -> invoicesrpc.AddHoldInvoiceRequest
|
||||
5, // 5: invoicesrpc.Invoices.SettleInvoice:input_type -> invoicesrpc.SettleInvoiceMsg
|
||||
8, // 6: invoicesrpc.Invoices.LookupInvoiceV2:input_type -> invoicesrpc.LookupInvoiceMsg
|
||||
10, // 7: invoicesrpc.Invoices.SubscribeSingleInvoice:output_type -> lnrpc.Invoice
|
||||
2, // 8: invoicesrpc.Invoices.CancelInvoice:output_type -> invoicesrpc.CancelInvoiceResp
|
||||
4, // 9: invoicesrpc.Invoices.AddHoldInvoice:output_type -> invoicesrpc.AddHoldInvoiceResp
|
||||
6, // 10: invoicesrpc.Invoices.SettleInvoice:output_type -> invoicesrpc.SettleInvoiceResp
|
||||
10, // 11: invoicesrpc.Invoices.LookupInvoiceV2:output_type -> lnrpc.Invoice
|
||||
7, // [7:12] is the sub-list for method output_type
|
||||
2, // [2:7] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
14, // 2: invoicesrpc.HtlcModifyRequest.invoice:type_name -> lnrpc.Invoice
|
||||
9, // 3: invoicesrpc.HtlcModifyRequest.exit_htlc_circuit_key:type_name -> invoicesrpc.CircuitKey
|
||||
12, // 4: invoicesrpc.HtlcModifyRequest.exit_htlc_wire_custom_records:type_name -> invoicesrpc.HtlcModifyRequest.ExitHtlcWireCustomRecordsEntry
|
||||
9, // 5: invoicesrpc.HtlcModifyResponse.circuit_key:type_name -> invoicesrpc.CircuitKey
|
||||
7, // 6: invoicesrpc.Invoices.SubscribeSingleInvoice:input_type -> invoicesrpc.SubscribeSingleInvoiceRequest
|
||||
1, // 7: invoicesrpc.Invoices.CancelInvoice:input_type -> invoicesrpc.CancelInvoiceMsg
|
||||
3, // 8: invoicesrpc.Invoices.AddHoldInvoice:input_type -> invoicesrpc.AddHoldInvoiceRequest
|
||||
5, // 9: invoicesrpc.Invoices.SettleInvoice:input_type -> invoicesrpc.SettleInvoiceMsg
|
||||
8, // 10: invoicesrpc.Invoices.LookupInvoiceV2:input_type -> invoicesrpc.LookupInvoiceMsg
|
||||
11, // 11: invoicesrpc.Invoices.HtlcModifier:input_type -> invoicesrpc.HtlcModifyResponse
|
||||
14, // 12: invoicesrpc.Invoices.SubscribeSingleInvoice:output_type -> lnrpc.Invoice
|
||||
2, // 13: invoicesrpc.Invoices.CancelInvoice:output_type -> invoicesrpc.CancelInvoiceResp
|
||||
4, // 14: invoicesrpc.Invoices.AddHoldInvoice:output_type -> invoicesrpc.AddHoldInvoiceResp
|
||||
6, // 15: invoicesrpc.Invoices.SettleInvoice:output_type -> invoicesrpc.SettleInvoiceResp
|
||||
14, // 16: invoicesrpc.Invoices.LookupInvoiceV2:output_type -> lnrpc.Invoice
|
||||
10, // 17: invoicesrpc.Invoices.HtlcModifier:output_type -> invoicesrpc.HtlcModifyRequest
|
||||
12, // [12:18] is the sub-list for method output_type
|
||||
6, // [6:12] 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
|
||||
}
|
||||
|
||||
func init() { file_invoicesrpc_invoices_proto_init() }
|
||||
|
@ -864,19 +1133,56 @@ func file_invoicesrpc_invoices_proto_init() {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
file_invoicesrpc_invoices_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*CircuitKey); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_invoicesrpc_invoices_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*HtlcModifyRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_invoicesrpc_invoices_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*HtlcModifyResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
file_invoicesrpc_invoices_proto_msgTypes[7].OneofWrappers = []interface{}{
|
||||
(*LookupInvoiceMsg_PaymentHash)(nil),
|
||||
(*LookupInvoiceMsg_PaymentAddr)(nil),
|
||||
(*LookupInvoiceMsg_SetId)(nil),
|
||||
}
|
||||
file_invoicesrpc_invoices_proto_msgTypes[10].OneofWrappers = []interface{}{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_invoicesrpc_invoices_proto_rawDesc,
|
||||
NumEnums: 1,
|
||||
NumMessages: 8,
|
||||
NumMessages: 12,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
|
|
@ -203,6 +203,58 @@ func local_request_Invoices_LookupInvoiceV2_0(ctx context.Context, marshaler run
|
|||
|
||||
}
|
||||
|
||||
func request_Invoices_HtlcModifier_0(ctx context.Context, marshaler runtime.Marshaler, client InvoicesClient, req *http.Request, pathParams map[string]string) (Invoices_HtlcModifierClient, runtime.ServerMetadata, error) {
|
||||
var metadata runtime.ServerMetadata
|
||||
stream, err := client.HtlcModifier(ctx)
|
||||
if err != nil {
|
||||
grpclog.Infof("Failed to start streaming: %v", err)
|
||||
return nil, metadata, err
|
||||
}
|
||||
dec := marshaler.NewDecoder(req.Body)
|
||||
handleSend := func() error {
|
||||
var protoReq HtlcModifyResponse
|
||||
err := dec.Decode(&protoReq)
|
||||
if err == io.EOF {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
grpclog.Infof("Failed to decode request: %v", err)
|
||||
return err
|
||||
}
|
||||
if err := stream.Send(&protoReq); err != nil {
|
||||
grpclog.Infof("Failed to send request: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := handleSend(); err != nil {
|
||||
if cerr := stream.CloseSend(); cerr != nil {
|
||||
grpclog.Infof("Failed to terminate client stream: %v", cerr)
|
||||
}
|
||||
if err == io.EOF {
|
||||
return stream, metadata, nil
|
||||
}
|
||||
return nil, metadata, err
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
if err := handleSend(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := stream.CloseSend(); err != nil {
|
||||
grpclog.Infof("Failed to terminate client stream: %v", err)
|
||||
}
|
||||
}()
|
||||
header, err := stream.Header()
|
||||
if err != nil {
|
||||
grpclog.Infof("Failed to get header from client: %v", err)
|
||||
return nil, metadata, err
|
||||
}
|
||||
metadata.HeaderMD = header
|
||||
return stream, metadata, nil
|
||||
}
|
||||
|
||||
// RegisterInvoicesHandlerServer registers the http handlers for service Invoices to "mux".
|
||||
// UnaryRPC :call InvoicesServer directly.
|
||||
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
|
||||
|
@ -308,6 +360,13 @@ func RegisterInvoicesHandlerServer(ctx context.Context, mux *runtime.ServeMux, s
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Invoices_HtlcModifier_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport")
|
||||
_, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -449,6 +508,26 @@ func RegisterInvoicesHandlerClient(ctx context.Context, mux *runtime.ServeMux, c
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Invoices_HtlcModifier_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/invoicesrpc.Invoices/HtlcModifier", runtime.WithHTTPPathPattern("/v2/invoices/htlcmodifier"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Invoices_HtlcModifier_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Invoices_HtlcModifier_0(ctx, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -462,6 +541,8 @@ var (
|
|||
pattern_Invoices_SettleInvoice_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "invoices", "settle"}, ""))
|
||||
|
||||
pattern_Invoices_LookupInvoiceV2_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "invoices", "lookup"}, ""))
|
||||
|
||||
pattern_Invoices_HtlcModifier_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "invoices", "htlcmodifier"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -474,4 +555,6 @@ var (
|
|||
forward_Invoices_SettleInvoice_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Invoices_LookupInvoiceV2_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Invoices_HtlcModifier_0 = runtime.ForwardResponseStream
|
||||
)
|
||||
|
|
|
@ -55,10 +55,19 @@ service Invoices {
|
|||
rpc SettleInvoice (SettleInvoiceMsg) returns (SettleInvoiceResp);
|
||||
|
||||
/*
|
||||
LookupInvoiceV2 attempts to look up at invoice. An invoice can be refrenced
|
||||
LookupInvoiceV2 attempts to look up at invoice. An invoice can be referenced
|
||||
using either its payment hash, payment address, or set ID.
|
||||
*/
|
||||
rpc LookupInvoiceV2 (LookupInvoiceMsg) returns (lnrpc.Invoice);
|
||||
|
||||
/*
|
||||
HtlcModifier is a bidirectional streaming RPC that allows a client to
|
||||
intercept and modify the HTLCs that attempt to settle the given invoice. The
|
||||
server will send HTLCs of invoices to the client and the client can modify
|
||||
some aspects of the HTLC in order to pass the invoice acceptance tests.
|
||||
*/
|
||||
rpc HtlcModifier (stream HtlcModifyResponse)
|
||||
returns (stream HtlcModifyRequest);
|
||||
}
|
||||
|
||||
message CancelInvoiceMsg {
|
||||
|
@ -192,3 +201,45 @@ message LookupInvoiceMsg {
|
|||
|
||||
LookupModifier lookup_modifier = 4;
|
||||
}
|
||||
|
||||
// CircuitKey is a unique identifier for an HTLC.
|
||||
message CircuitKey {
|
||||
// The id of the channel that the is part of this circuit.
|
||||
uint64 chan_id = 1;
|
||||
|
||||
// The index of the incoming htlc in the incoming channel.
|
||||
uint64 htlc_id = 2;
|
||||
}
|
||||
|
||||
message HtlcModifyRequest {
|
||||
// The invoice the intercepted HTLC is attempting to settle. The HTLCs in
|
||||
// the invoice are only HTLCs that have already been accepted or settled,
|
||||
// not including the current intercepted HTLC.
|
||||
lnrpc.Invoice invoice = 1;
|
||||
|
||||
// The unique identifier of the HTLC of this intercepted HTLC.
|
||||
CircuitKey exit_htlc_circuit_key = 2;
|
||||
|
||||
// The amount in milli-satoshi that the exit HTLC is attempting to pay.
|
||||
uint64 exit_htlc_amt = 3;
|
||||
|
||||
// The absolute expiry height of the exit HTLC.
|
||||
uint32 exit_htlc_expiry = 4;
|
||||
|
||||
// The current block height.
|
||||
uint32 current_height = 5;
|
||||
|
||||
// The wire message custom records of the exit HTLC.
|
||||
map<uint64, bytes> exit_htlc_wire_custom_records = 6;
|
||||
}
|
||||
|
||||
message HtlcModifyResponse {
|
||||
// The circuit key of the HTLC that the client wants to modify.
|
||||
CircuitKey circuit_key = 1;
|
||||
|
||||
// The modified amount in milli-satoshi that the exit HTLC is paying. This
|
||||
// value can be different from the actual on-chain HTLC amount, in case the
|
||||
// HTLC carries other valuable items, as can be the case with custom channel
|
||||
// types.
|
||||
optional uint64 amt_paid = 2;
|
||||
}
|
||||
|
|
|
@ -82,9 +82,52 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/v2/invoices/htlcmodifier": {
|
||||
"post": {
|
||||
"summary": "HtlcModifier is a bidirectional streaming RPC that allows a client to\nintercept and modify the HTLCs that attempt to settle the given invoice. The\nserver will send HTLCs of invoices to the client and the client can modify\nsome aspects of the HTLC in order to pass the invoice acceptance tests.",
|
||||
"operationId": "Invoices_HtlcModifier",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.(streaming responses)",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/invoicesrpcHtlcModifyRequest"
|
||||
},
|
||||
"error": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
},
|
||||
"title": "Stream result of invoicesrpcHtlcModifyRequest"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": " (streaming inputs)",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/invoicesrpcHtlcModifyResponse"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Invoices"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/invoices/lookup": {
|
||||
"get": {
|
||||
"summary": "LookupInvoiceV2 attempts to look up at invoice. An invoice can be refrenced\nusing either its payment hash, payment address, or set ID.",
|
||||
"summary": "LookupInvoiceV2 attempts to look up at invoice. An invoice can be referenced\nusing either its payment hash, payment address, or set ID.",
|
||||
"operationId": "Invoices_LookupInvoiceV2",
|
||||
"responses": {
|
||||
"200": {
|
||||
|
@ -317,6 +360,72 @@
|
|||
"invoicesrpcCancelInvoiceResp": {
|
||||
"type": "object"
|
||||
},
|
||||
"invoicesrpcCircuitKey": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chan_id": {
|
||||
"type": "string",
|
||||
"format": "uint64",
|
||||
"description": "The id of the channel that the is part of this circuit."
|
||||
},
|
||||
"htlc_id": {
|
||||
"type": "string",
|
||||
"format": "uint64",
|
||||
"description": "The index of the incoming htlc in the incoming channel."
|
||||
}
|
||||
},
|
||||
"description": "CircuitKey is a unique identifier for an HTLC."
|
||||
},
|
||||
"invoicesrpcHtlcModifyRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"invoice": {
|
||||
"$ref": "#/definitions/lnrpcInvoice",
|
||||
"description": "The invoice the intercepted HTLC is attempting to settle. The HTLCs in\nthe invoice are only HTLCs that have already been accepted or settled,\nnot including the current intercepted HTLC."
|
||||
},
|
||||
"exit_htlc_circuit_key": {
|
||||
"$ref": "#/definitions/invoicesrpcCircuitKey",
|
||||
"description": "The unique identifier of the HTLC of this intercepted HTLC."
|
||||
},
|
||||
"exit_htlc_amt": {
|
||||
"type": "string",
|
||||
"format": "uint64",
|
||||
"description": "The amount in milli-satoshi that the exit HTLC is attempting to pay."
|
||||
},
|
||||
"exit_htlc_expiry": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "The absolute expiry height of the exit HTLC."
|
||||
},
|
||||
"current_height": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "The current block height."
|
||||
},
|
||||
"exit_htlc_wire_custom_records": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"format": "byte"
|
||||
},
|
||||
"description": "The wire message custom records of the exit HTLC."
|
||||
}
|
||||
}
|
||||
},
|
||||
"invoicesrpcHtlcModifyResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"circuit_key": {
|
||||
"$ref": "#/definitions/invoicesrpcCircuitKey",
|
||||
"description": "The circuit key of the HTLC that the client wants to modify."
|
||||
},
|
||||
"amt_paid": {
|
||||
"type": "string",
|
||||
"format": "uint64",
|
||||
"description": "The modified amount in milli-satoshi that the exit HTLC is paying. This\nvalue can be different from the actual on-chain HTLC amount, in case the\nHTLC carries other valuable items, as can be the case with custom channel\ntypes."
|
||||
}
|
||||
}
|
||||
},
|
||||
"invoicesrpcLookupModifier": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
@ -675,6 +784,11 @@
|
|||
"amp": {
|
||||
"$ref": "#/definitions/lnrpcAMP",
|
||||
"description": "Details relevant to AMP HTLCs, only populated if this is an AMP HTLC."
|
||||
},
|
||||
"custom_channel_data": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "Custom channel data that might be populated in custom channels."
|
||||
}
|
||||
},
|
||||
"title": "Details of an HTLC that paid to an invoice"
|
||||
|
|
|
@ -16,3 +16,6 @@ http:
|
|||
body: "*"
|
||||
- selector: invoicesrpc.Invoices.LookupInvoiceV2
|
||||
get: "/v2/invoices/lookup"
|
||||
- selector: invoicesrpc.Invoices.HtlcModifier
|
||||
post: "/v2/invoices/htlcmodifier"
|
||||
body: "*"
|
|
@ -36,9 +36,14 @@ type InvoicesClient interface {
|
|||
// SettleInvoice settles an accepted invoice. If the invoice is already
|
||||
// settled, this call will succeed.
|
||||
SettleInvoice(ctx context.Context, in *SettleInvoiceMsg, opts ...grpc.CallOption) (*SettleInvoiceResp, error)
|
||||
// LookupInvoiceV2 attempts to look up at invoice. An invoice can be refrenced
|
||||
// LookupInvoiceV2 attempts to look up at invoice. An invoice can be referenced
|
||||
// using either its payment hash, payment address, or set ID.
|
||||
LookupInvoiceV2(ctx context.Context, in *LookupInvoiceMsg, opts ...grpc.CallOption) (*lnrpc.Invoice, error)
|
||||
// HtlcModifier is a bidirectional streaming RPC that allows a client to
|
||||
// intercept and modify the HTLCs that attempt to settle the given invoice. The
|
||||
// server will send HTLCs of invoices to the client and the client can modify
|
||||
// some aspects of the HTLC in order to pass the invoice acceptance tests.
|
||||
HtlcModifier(ctx context.Context, opts ...grpc.CallOption) (Invoices_HtlcModifierClient, error)
|
||||
}
|
||||
|
||||
type invoicesClient struct {
|
||||
|
@ -117,6 +122,37 @@ func (c *invoicesClient) LookupInvoiceV2(ctx context.Context, in *LookupInvoiceM
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *invoicesClient) HtlcModifier(ctx context.Context, opts ...grpc.CallOption) (Invoices_HtlcModifierClient, error) {
|
||||
stream, err := c.cc.NewStream(ctx, &Invoices_ServiceDesc.Streams[1], "/invoicesrpc.Invoices/HtlcModifier", opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &invoicesHtlcModifierClient{stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
type Invoices_HtlcModifierClient interface {
|
||||
Send(*HtlcModifyResponse) error
|
||||
Recv() (*HtlcModifyRequest, error)
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
type invoicesHtlcModifierClient struct {
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
func (x *invoicesHtlcModifierClient) Send(m *HtlcModifyResponse) error {
|
||||
return x.ClientStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func (x *invoicesHtlcModifierClient) Recv() (*HtlcModifyRequest, error) {
|
||||
m := new(HtlcModifyRequest)
|
||||
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// InvoicesServer is the server API for Invoices service.
|
||||
// All implementations must embed UnimplementedInvoicesServer
|
||||
// for forward compatibility
|
||||
|
@ -138,9 +174,14 @@ type InvoicesServer interface {
|
|||
// SettleInvoice settles an accepted invoice. If the invoice is already
|
||||
// settled, this call will succeed.
|
||||
SettleInvoice(context.Context, *SettleInvoiceMsg) (*SettleInvoiceResp, error)
|
||||
// LookupInvoiceV2 attempts to look up at invoice. An invoice can be refrenced
|
||||
// LookupInvoiceV2 attempts to look up at invoice. An invoice can be referenced
|
||||
// using either its payment hash, payment address, or set ID.
|
||||
LookupInvoiceV2(context.Context, *LookupInvoiceMsg) (*lnrpc.Invoice, error)
|
||||
// HtlcModifier is a bidirectional streaming RPC that allows a client to
|
||||
// intercept and modify the HTLCs that attempt to settle the given invoice. The
|
||||
// server will send HTLCs of invoices to the client and the client can modify
|
||||
// some aspects of the HTLC in order to pass the invoice acceptance tests.
|
||||
HtlcModifier(Invoices_HtlcModifierServer) error
|
||||
mustEmbedUnimplementedInvoicesServer()
|
||||
}
|
||||
|
||||
|
@ -163,6 +204,9 @@ func (UnimplementedInvoicesServer) SettleInvoice(context.Context, *SettleInvoice
|
|||
func (UnimplementedInvoicesServer) LookupInvoiceV2(context.Context, *LookupInvoiceMsg) (*lnrpc.Invoice, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method LookupInvoiceV2 not implemented")
|
||||
}
|
||||
func (UnimplementedInvoicesServer) HtlcModifier(Invoices_HtlcModifierServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method HtlcModifier not implemented")
|
||||
}
|
||||
func (UnimplementedInvoicesServer) mustEmbedUnimplementedInvoicesServer() {}
|
||||
|
||||
// UnsafeInvoicesServer may be embedded to opt out of forward compatibility for this service.
|
||||
|
@ -269,6 +313,32 @@ func _Invoices_LookupInvoiceV2_Handler(srv interface{}, ctx context.Context, dec
|
|||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Invoices_HtlcModifier_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(InvoicesServer).HtlcModifier(&invoicesHtlcModifierServer{stream})
|
||||
}
|
||||
|
||||
type Invoices_HtlcModifierServer interface {
|
||||
Send(*HtlcModifyRequest) error
|
||||
Recv() (*HtlcModifyResponse, error)
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
type invoicesHtlcModifierServer struct {
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
func (x *invoicesHtlcModifierServer) Send(m *HtlcModifyRequest) error {
|
||||
return x.ServerStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func (x *invoicesHtlcModifierServer) Recv() (*HtlcModifyResponse, error) {
|
||||
m := new(HtlcModifyResponse)
|
||||
if err := x.ServerStream.RecvMsg(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Invoices_ServiceDesc is the grpc.ServiceDesc for Invoices service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
|
@ -299,6 +369,12 @@ var Invoices_ServiceDesc = grpc.ServiceDesc{
|
|||
Handler: _Invoices_SubscribeSingleInvoice_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "HtlcModifier",
|
||||
Handler: _Invoices_HtlcModifier_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "invoicesrpc/invoices.proto",
|
||||
}
|
||||
|
|
|
@ -30,6 +30,9 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
// ErrServerShuttingDown is returned when the server is shutting down.
|
||||
ErrServerShuttingDown = errors.New("server shutting down")
|
||||
|
||||
// macaroonOps are the set of capabilities that our minted macaroon (if
|
||||
// it doesn't already exist) will have.
|
||||
macaroonOps = []bakery.Op{
|
||||
|
@ -65,6 +68,10 @@ var (
|
|||
Entity: "invoices",
|
||||
Action: "write",
|
||||
}},
|
||||
"/invoicesrpc.Invoices/HtlcModifier": {{
|
||||
Entity: "invoices",
|
||||
Action: "write",
|
||||
}},
|
||||
}
|
||||
|
||||
// DefaultInvoicesMacFilename is the default name of the invoices
|
||||
|
@ -215,8 +222,9 @@ func (r *ServerShell) RegisterWithRestServer(ctx context.Context,
|
|||
// methods routed towards it.
|
||||
//
|
||||
// NOTE: This is part of the lnrpc.GrpcHandler interface.
|
||||
func (r *ServerShell) CreateSubServer(configRegistry lnrpc.SubServerConfigDispatcher) (
|
||||
lnrpc.SubServer, lnrpc.MacaroonPerms, error) {
|
||||
func (r *ServerShell) CreateSubServer(
|
||||
configRegistry lnrpc.SubServerConfigDispatcher) (lnrpc.SubServer,
|
||||
lnrpc.MacaroonPerms, error) {
|
||||
|
||||
subServer, macPermissions, err := createNewSubServer(configRegistry)
|
||||
if err != nil {
|
||||
|
@ -257,6 +265,14 @@ func (s *Server) SubscribeSingleInvoice(req *SubscribeSingleInvoiceRequest,
|
|||
return err
|
||||
}
|
||||
|
||||
// Give the aux data parser a chance to format the
|
||||
// custom data in the invoice HTLCs.
|
||||
err = s.cfg.ParseAuxData(rpcInvoice)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing custom data: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
if err := updateStream.Send(rpcInvoice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -444,5 +460,50 @@ func (s *Server) LookupInvoiceV2(ctx context.Context,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return CreateRPCInvoice(&invoice, s.cfg.ChainParams)
|
||||
rpcInvoice, err := CreateRPCInvoice(&invoice, s.cfg.ChainParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Give the aux data parser a chance to format the custom data in the
|
||||
// invoice HTLCs.
|
||||
err = s.cfg.ParseAuxData(rpcInvoice)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing custom data: %w", err)
|
||||
}
|
||||
|
||||
return rpcInvoice, nil
|
||||
}
|
||||
|
||||
// HtlcModifier is a bidirectional streaming RPC that allows a client to
|
||||
// intercept and modify the HTLCs that attempt to settle the given invoice. The
|
||||
// server will send HTLCs of invoices to the client and the client can modify
|
||||
// some aspects of the HTLC in order to pass the invoice acceptance tests.
|
||||
func (s *Server) HtlcModifier(
|
||||
modifierServer Invoices_HtlcModifierServer) error {
|
||||
|
||||
modifier := newHtlcModifier(s.cfg.ChainParams, modifierServer)
|
||||
reset, modifierQuit, err := s.cfg.HtlcModifier.RegisterInterceptor(
|
||||
modifier.onIntercept,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot register interceptor: %w", err)
|
||||
}
|
||||
|
||||
defer reset()
|
||||
|
||||
log.Debugf("Invoice HTLC modifier client connected")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-modifierServer.Context().Done():
|
||||
return modifierServer.Context().Err()
|
||||
|
||||
case <-modifierQuit:
|
||||
return ErrServerShuttingDown
|
||||
|
||||
case <-s.quit:
|
||||
return ErrServerShuttingDown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,6 +123,16 @@ func CreateRPCInvoice(invoice *invoices.Invoice,
|
|||
MppTotalAmtMsat: uint64(htlc.MppTotalAmt),
|
||||
}
|
||||
|
||||
// The custom channel data is currently just the raw bytes of
|
||||
// the encoded custom records.
|
||||
customData, err := lnwire.CustomRecords(
|
||||
htlc.CustomRecords,
|
||||
).Serialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rpcHtlc.CustomChannelData = customData
|
||||
|
||||
// Populate any fields relevant to AMP payments.
|
||||
if htlc.AMP != nil {
|
||||
rootShare := htlc.AMP.Record.RootShare()
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2033,10 +2033,38 @@ message ChannelOpenUpdate {
|
|||
ChannelPoint channel_point = 1;
|
||||
}
|
||||
|
||||
message CloseOutput {
|
||||
// The amount in satoshi of this close output. This amount is the final
|
||||
// commitment balance of the channel and the actual amount paid out on chain
|
||||
// might be smaller due to subtracted fees.
|
||||
int64 amount_sat = 1;
|
||||
|
||||
// The pkScript of the close output.
|
||||
bytes pk_script = 2;
|
||||
|
||||
// Whether this output is for the local or remote node.
|
||||
bool is_local = 3;
|
||||
|
||||
// The TLV encoded custom channel data records for this output, which might
|
||||
// be set for custom channels.
|
||||
bytes custom_channel_data = 4;
|
||||
}
|
||||
|
||||
message ChannelCloseUpdate {
|
||||
bytes closing_txid = 1;
|
||||
|
||||
bool success = 2;
|
||||
|
||||
// The local channel close output. If the local channel balance was dust to
|
||||
// begin with, this output will not be set.
|
||||
CloseOutput local_close_output = 3;
|
||||
|
||||
// The remote channel close output. If the remote channel balance was dust
|
||||
// to begin with, this output will not be set.
|
||||
CloseOutput remote_close_output = 4;
|
||||
|
||||
// Any additional outputs that might be added for custom channel types.
|
||||
repeated CloseOutput additional_outputs = 5;
|
||||
}
|
||||
|
||||
message CloseChannelRequest {
|
||||
|
@ -3952,6 +3980,11 @@ message InvoiceHTLC {
|
|||
|
||||
// Details relevant to AMP HTLCs, only populated if this is an AMP HTLC.
|
||||
AMP amp = 11;
|
||||
|
||||
/*
|
||||
Custom channel data that might be populated in custom channels.
|
||||
*/
|
||||
bytes custom_channel_data = 12;
|
||||
}
|
||||
|
||||
// Details specific to AMP HTLCs.
|
||||
|
|
|
@ -4156,6 +4156,21 @@
|
|||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"local_close_output": {
|
||||
"$ref": "#/definitions/lnrpcCloseOutput",
|
||||
"description": "The local channel close output. If the local channel balance was dust to\nbegin with, this output will not be set."
|
||||
},
|
||||
"remote_close_output": {
|
||||
"$ref": "#/definitions/lnrpcCloseOutput",
|
||||
"description": "The remote channel close output. If the remote channel balance was dust\nto begin with, this output will not be set."
|
||||
},
|
||||
"additional_outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/lnrpcCloseOutput"
|
||||
},
|
||||
"description": "Any additional outputs that might be added for custom channel types."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -4465,6 +4480,30 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"lnrpcCloseOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount_sat": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "The amount in satoshi of this close output. This amount is the final\ncommitment balance of the channel and the actual amount paid out on chain\nmight be smaller due to subtracted fees."
|
||||
},
|
||||
"pk_script": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "The pkScript of the close output."
|
||||
},
|
||||
"is_local": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this output is for the local or remote node."
|
||||
},
|
||||
"custom_channel_data": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "The TLV encoded custom channel data records for this output, which might\nbe set for custom channels."
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcCloseStatusUpdate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -5602,6 +5641,11 @@
|
|||
"amp": {
|
||||
"$ref": "#/definitions/lnrpcAMP",
|
||||
"description": "Details relevant to AMP HTLCs, only populated if this is an AMP HTLC."
|
||||
},
|
||||
"custom_channel_data": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "Custom channel data that might be populated in custom channels."
|
||||
}
|
||||
},
|
||||
"title": "Details of an HTLC that paid to an invoice"
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/kvdb/etcd"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntest/miner"
|
||||
|
@ -2071,8 +2072,42 @@ func (h *HarnessTest) ReceiveHtlcInterceptor(
|
|||
require.Fail(h, "timeout", "timeout intercepting htlc")
|
||||
|
||||
case err := <-errChan:
|
||||
require.Failf(h, "err from stream",
|
||||
"received err from stream: %v", err)
|
||||
require.Failf(h, "err from HTLC interceptor stream",
|
||||
"received err from HTLC interceptor stream: %v", err)
|
||||
|
||||
case updateMsg := <-chanMsg:
|
||||
return updateMsg
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReceiveInvoiceHtlcModification waits until a message is received on the
|
||||
// invoice HTLC modifier stream or the timeout is reached.
|
||||
func (h *HarnessTest) ReceiveInvoiceHtlcModification(
|
||||
stream rpc.InvoiceHtlcModifierClient) *invoicesrpc.HtlcModifyRequest {
|
||||
|
||||
chanMsg := make(chan *invoicesrpc.HtlcModifyRequest)
|
||||
errChan := make(chan error)
|
||||
go func() {
|
||||
// Consume one message. This will block until the message is
|
||||
// received.
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
chanMsg <- resp
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(DefaultTimeout):
|
||||
require.Fail(h, "timeout", "timeout invoice HTLC modifier")
|
||||
|
||||
case err := <-errChan:
|
||||
require.Failf(h, "err from invoice HTLC modifier stream",
|
||||
"received err from invoice HTLC modifier stream: %v",
|
||||
err)
|
||||
|
||||
case updateMsg := <-chanMsg:
|
||||
return updateMsg
|
||||
|
|
|
@ -78,9 +78,8 @@ func (w *WalletController) ConfirmedBalance(int32, string) (btcutil.Amount,
|
|||
func (w *WalletController) NewAddress(lnwallet.AddressType, bool,
|
||||
string) (btcutil.Address, error) {
|
||||
|
||||
addr, _ := btcutil.NewAddressPubKey(
|
||||
w.RootKey.PubKey().SerializeCompressed(), &chaincfg.MainNetParams,
|
||||
)
|
||||
pkh := btcutil.Hash160(w.RootKey.PubKey().SerializeCompressed())
|
||||
addr, _ := btcutil.NewAddressPubKeyHash(pkh, &chaincfg.MainNetParams)
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
)
|
||||
|
||||
// =====================
|
||||
|
@ -84,18 +83,19 @@ func (h *HarnessRPC) SubscribeSingleInvoice(rHash []byte) SingleInvoiceClient {
|
|||
return client
|
||||
}
|
||||
|
||||
type TrackPaymentClient routerrpc.Router_TrackPaymentV2Client
|
||||
type InvoiceHtlcModifierClient invoicesrpc.Invoices_HtlcModifierClient
|
||||
|
||||
// TrackPaymentV2 creates a subscription client for given invoice and
|
||||
// asserts its creation.
|
||||
func (h *HarnessRPC) TrackPaymentV2(payHash []byte) TrackPaymentClient {
|
||||
req := &routerrpc.TrackPaymentRequest{PaymentHash: payHash}
|
||||
// InvoiceHtlcModifier makes an RPC call to the node's RouterClient and asserts.
|
||||
func (h *HarnessRPC) InvoiceHtlcModifier() (InvoiceHtlcModifierClient,
|
||||
context.CancelFunc) {
|
||||
|
||||
// TrackPaymentV2 needs to have the context alive for the entire test
|
||||
// case as the returned client will be used for send and receive events
|
||||
// stream. Thus we use runCtx here instead of a timeout context.
|
||||
client, err := h.Router.TrackPaymentV2(h.runCtx, req)
|
||||
h.NoError(err, "TrackPaymentV2")
|
||||
// InvoiceHtlcModifier needs to have the context alive for the entire
|
||||
// test case as the returned client will be used for send and receive
|
||||
// events stream. Therefore, we use cancel context here instead of a
|
||||
// timeout context.
|
||||
ctxt, cancel := context.WithCancel(h.runCtx)
|
||||
resp, err := h.Invoice.HtlcModifier(ctxt)
|
||||
h.NoError(err, "InvoiceHtlcModifier")
|
||||
|
||||
return client
|
||||
return resp, cancel
|
||||
}
|
||||
|
|
|
@ -267,3 +267,19 @@ func (h *HarnessRPC) TrackPayments(
|
|||
|
||||
return resp
|
||||
}
|
||||
|
||||
type TrackPaymentClient routerrpc.Router_TrackPaymentV2Client
|
||||
|
||||
// TrackPaymentV2 creates a subscription client for given invoice and
|
||||
// asserts its creation.
|
||||
func (h *HarnessRPC) TrackPaymentV2(payHash []byte) TrackPaymentClient {
|
||||
req := &routerrpc.TrackPaymentRequest{PaymentHash: payHash}
|
||||
|
||||
// TrackPaymentV2 needs to have the context alive for the entire test
|
||||
// case as the returned client will be used for send and receive events
|
||||
// stream. Thus we use runCtx here instead of a timeout context.
|
||||
client, err := h.Router.TrackPaymentV2(h.runCtx, req)
|
||||
h.NoError(err, "TrackPaymentV2")
|
||||
|
||||
return client
|
||||
}
|
||||
|
|
104
lnwallet/chancloser/aux_closer.go
Normal file
104
lnwallet/chancloser/aux_closer.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package chancloser
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
// CloseOutput represents an output that should be included in the close
|
||||
// transaction.
|
||||
type CloseOutput struct {
|
||||
// Amt is the amount of the output.
|
||||
Amt btcutil.Amount
|
||||
|
||||
// DustLimit is the dust limit for the local node.
|
||||
DustLimit btcutil.Amount
|
||||
|
||||
// PkScript is the script that should be used to pay to the output.
|
||||
PkScript []byte
|
||||
|
||||
// ShutdownRecords is the set of custom records that may result in
|
||||
// extra close outputs being added.
|
||||
ShutdownRecords lnwire.CustomRecords
|
||||
}
|
||||
|
||||
// AuxShutdownReq is used to request a set of extra custom records to include
|
||||
// in the shutdown message.
|
||||
type AuxShutdownReq struct {
|
||||
// ChanPoint is the channel point of the channel that is being shut
|
||||
// down.
|
||||
ChanPoint wire.OutPoint
|
||||
|
||||
// ShortChanID is the short channel ID of the channel that is being
|
||||
// closed.
|
||||
ShortChanID lnwire.ShortChannelID
|
||||
|
||||
// Initiator is true if the local node is the initiator of the channel.
|
||||
Initiator bool
|
||||
|
||||
// InternalKey is the internal key for the shutdown addr. This will
|
||||
// only be set for taproot shutdown addrs.
|
||||
InternalKey fn.Option[btcec.PublicKey]
|
||||
|
||||
// CommitBlob is the blob that was included in the last commitment.
|
||||
CommitBlob fn.Option[tlv.Blob]
|
||||
|
||||
// FundingBlob is the blob that was included in the funding state.
|
||||
FundingBlob fn.Option[tlv.Blob]
|
||||
}
|
||||
|
||||
// AuxCloseDesc is used to describe the channel close that is being performed.
|
||||
type AuxCloseDesc struct {
|
||||
AuxShutdownReq
|
||||
|
||||
// CloseFee is the closing fee to be paid for this state.
|
||||
CloseFee btcutil.Amount
|
||||
|
||||
// CommitFee is the fee that was paid for the last commitment.
|
||||
CommitFee btcutil.Amount
|
||||
|
||||
// LocalCloseOutput is the output that the local node should be paid
|
||||
// to. This is None if the local party will not have an output on the
|
||||
// co-op close transaction.
|
||||
LocalCloseOutput fn.Option[CloseOutput]
|
||||
|
||||
// RemoteCloseOutput is the output that the remote node should be paid
|
||||
// to. This will be None if the remote party will not have an output on
|
||||
// the co-op close transaction.
|
||||
RemoteCloseOutput fn.Option[CloseOutput]
|
||||
}
|
||||
|
||||
// AuxCloseOutputs is used to specify extra outputs that should be used when
|
||||
// constructing the co-op close transaction.
|
||||
type AuxCloseOutputs struct {
|
||||
// ExtraCloseOutputs is a set of extra outputs that should be included
|
||||
// in the close transaction.
|
||||
ExtraCloseOutputs []lnwallet.CloseOutput
|
||||
|
||||
// CustomSort is a custom function that can be used to sort the
|
||||
// transaction outputs. If this isn't set, then the default BIP-69
|
||||
// sorting is used.
|
||||
CustomSort lnwallet.CloseSortFunc
|
||||
}
|
||||
|
||||
// AuxChanCloser is used to allow an external caller to modify the co-op close
|
||||
// transaction.
|
||||
type AuxChanCloser interface {
|
||||
// ShutdownBlob returns the set of custom records that should be
|
||||
// included in the shutdown message.
|
||||
ShutdownBlob(req AuxShutdownReq) (fn.Option[lnwire.CustomRecords],
|
||||
error)
|
||||
|
||||
// AuxCloseOutputs returns the set of custom outputs that should be used
|
||||
// to construct the co-op close transaction.
|
||||
AuxCloseOutputs(desc AuxCloseDesc) (fn.Option[AuxCloseOutputs], error)
|
||||
|
||||
// FinalizeClose is called after the close transaction has been agreed
|
||||
// upon.
|
||||
FinalizeClose(desc AuxCloseDesc, closeTx *wire.MsgTx) error
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
|
@ -106,6 +107,18 @@ const (
|
|||
defaultMaxFeeMultiplier = 3
|
||||
)
|
||||
|
||||
// DeliveryAddrWithKey wraps a normal delivery addr, but also includes the
|
||||
// internal key for the delivery addr if known.
|
||||
type DeliveryAddrWithKey struct {
|
||||
// DeliveryAddress is the raw, serialized pkScript of the delivery
|
||||
// address.
|
||||
lnwire.DeliveryAddress
|
||||
|
||||
// InternalKey is the Taproot internal key of the delivery address, if
|
||||
// the address is a P2TR output.
|
||||
InternalKey fn.Option[btcec.PublicKey]
|
||||
}
|
||||
|
||||
// ChanCloseCfg holds all the items that a ChanCloser requires to carry out its
|
||||
// duties.
|
||||
type ChanCloseCfg struct {
|
||||
|
@ -140,6 +153,10 @@ type ChanCloseCfg struct {
|
|||
// FeeEstimator is used to estimate the absolute starting co-op close
|
||||
// fee.
|
||||
FeeEstimator CoopFeeEstimator
|
||||
|
||||
// AuxCloser is an optional interface that can be used to modify the
|
||||
// way the co-op close process proceeds.
|
||||
AuxCloser fn.Option[AuxChanCloser]
|
||||
}
|
||||
|
||||
// ChanCloser is a state machine that handles the cooperative channel closure
|
||||
|
@ -204,6 +221,10 @@ type ChanCloser struct {
|
|||
// funds to.
|
||||
localDeliveryScript []byte
|
||||
|
||||
// localInternalKey is the local delivery address Taproot internal key,
|
||||
// if the local delivery script is a P2TR output.
|
||||
localInternalKey fn.Option[btcec.PublicKey]
|
||||
|
||||
// remoteDeliveryScript is the script that we'll send the remote party's
|
||||
// settled channel funds to.
|
||||
remoteDeliveryScript []byte
|
||||
|
@ -215,6 +236,20 @@ type ChanCloser struct {
|
|||
// we use to handle a specific race condition caused by the independent
|
||||
// message processing queues.
|
||||
cachedClosingSigned fn.Option[lnwire.ClosingSigned]
|
||||
|
||||
// localCloseOutput is the local output on the closing transaction that
|
||||
// the local party should be paid to. This will only be populated if the
|
||||
// local balance isn't dust.
|
||||
localCloseOutput fn.Option[CloseOutput]
|
||||
|
||||
// remoteCloseOutput is the remote output on the closing transaction
|
||||
// that the remote party should be paid to. This will only be populated
|
||||
// if the remote balance isn't dust.
|
||||
remoteCloseOutput fn.Option[CloseOutput]
|
||||
|
||||
// auxOutputs are the optional additional outputs that might be added to
|
||||
// the closing transaction.
|
||||
auxOutputs fn.Option[AuxCloseOutputs]
|
||||
}
|
||||
|
||||
// calcCoopCloseFee computes an "ideal" absolute co-op close fee given the
|
||||
|
@ -266,7 +301,7 @@ func (d *SimpleCoopFeeEstimator) EstimateFee(chanType channeldb.ChannelType,
|
|||
// NewChanCloser creates a new instance of the channel closure given the passed
|
||||
// configuration, and delivery+fee preference. The final argument should only
|
||||
// be populated iff, we're the initiator of this closing request.
|
||||
func NewChanCloser(cfg ChanCloseCfg, deliveryScript []byte,
|
||||
func NewChanCloser(cfg ChanCloseCfg, deliveryScript DeliveryAddrWithKey,
|
||||
idealFeePerKw chainfee.SatPerKWeight, negotiationHeight uint32,
|
||||
closeReq *htlcswitch.ChanClose,
|
||||
closer lntypes.ChannelParty) *ChanCloser {
|
||||
|
@ -281,7 +316,8 @@ func NewChanCloser(cfg ChanCloseCfg, deliveryScript []byte,
|
|||
cfg: cfg,
|
||||
negotiationHeight: negotiationHeight,
|
||||
idealFeeRate: idealFeePerKw,
|
||||
localDeliveryScript: deliveryScript,
|
||||
localInternalKey: deliveryScript.InternalKey,
|
||||
localDeliveryScript: deliveryScript.DeliveryAddress,
|
||||
priorFeeOffers: make(
|
||||
map[btcutil.Amount]*lnwire.ClosingSigned,
|
||||
),
|
||||
|
@ -295,13 +331,13 @@ func (c *ChanCloser) initFeeBaseline() {
|
|||
// Depending on if a balance ends up being dust or not, we'll pass a
|
||||
// nil TxOut into the EstimateFee call which can handle it.
|
||||
var localTxOut, remoteTxOut *wire.TxOut
|
||||
if !c.cfg.Channel.LocalBalanceDust() {
|
||||
if isDust, _ := c.cfg.Channel.LocalBalanceDust(); !isDust {
|
||||
localTxOut = &wire.TxOut{
|
||||
PkScript: c.localDeliveryScript,
|
||||
Value: 0,
|
||||
}
|
||||
}
|
||||
if !c.cfg.Channel.RemoteBalanceDust() {
|
||||
if isDust, _ := c.cfg.Channel.RemoteBalanceDust(); !isDust {
|
||||
remoteTxOut = &wire.TxOut{
|
||||
PkScript: c.remoteDeliveryScript,
|
||||
Value: 0,
|
||||
|
@ -337,6 +373,31 @@ func (c *ChanCloser) initChanShutdown() (*lnwire.Shutdown, error) {
|
|||
// desired closing script.
|
||||
shutdown := lnwire.NewShutdown(c.cid, c.localDeliveryScript)
|
||||
|
||||
// At this point, we'll check to see if we have any custom records to
|
||||
// add to the shutdown message.
|
||||
err := fn.MapOptionZ(c.cfg.AuxCloser, func(a AuxChanCloser) error {
|
||||
shutdownCustomRecords, err := a.ShutdownBlob(AuxShutdownReq{
|
||||
ChanPoint: c.chanPoint,
|
||||
ShortChanID: c.cfg.Channel.ShortChanID(),
|
||||
Initiator: c.cfg.Channel.IsInitiator(),
|
||||
InternalKey: c.localInternalKey,
|
||||
CommitBlob: c.cfg.Channel.LocalCommitmentBlob(),
|
||||
FundingBlob: c.cfg.Channel.FundingBlob(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shutdownCustomRecords.WhenSome(func(cr lnwire.CustomRecords) {
|
||||
shutdown.CustomRecords = cr
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If this is a taproot channel, then we'll need to also generate a
|
||||
// nonce that'll be used sign the co-op close transaction offer.
|
||||
if c.cfg.Channel.ChanType().IsTaproot() {
|
||||
|
@ -370,11 +431,22 @@ func (c *ChanCloser) initChanShutdown() (*lnwire.Shutdown, error) {
|
|||
shutdownInfo := channeldb.NewShutdownInfo(
|
||||
c.localDeliveryScript, c.closer.IsLocal(),
|
||||
)
|
||||
err := c.cfg.Channel.MarkShutdownSent(shutdownInfo)
|
||||
err = c.cfg.Channel.MarkShutdownSent(shutdownInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We'll track our local close output, even if it's dust in BTC terms,
|
||||
// it might still carry value in custom channel terms.
|
||||
_, dustAmt := c.cfg.Channel.LocalBalanceDust()
|
||||
localBalance, _ := c.cfg.Channel.CommitBalances()
|
||||
c.localCloseOutput = fn.Some(CloseOutput{
|
||||
Amt: localBalance,
|
||||
DustLimit: dustAmt,
|
||||
PkScript: c.localDeliveryScript,
|
||||
ShutdownRecords: shutdown.CustomRecords,
|
||||
})
|
||||
|
||||
return shutdown, nil
|
||||
}
|
||||
|
||||
|
@ -444,6 +516,21 @@ func (c *ChanCloser) NegotiationHeight() uint32 {
|
|||
return c.negotiationHeight
|
||||
}
|
||||
|
||||
// LocalCloseOutput returns the local close output.
|
||||
func (c *ChanCloser) LocalCloseOutput() fn.Option[CloseOutput] {
|
||||
return c.localCloseOutput
|
||||
}
|
||||
|
||||
// RemoteCloseOutput returns the remote close output.
|
||||
func (c *ChanCloser) RemoteCloseOutput() fn.Option[CloseOutput] {
|
||||
return c.remoteCloseOutput
|
||||
}
|
||||
|
||||
// AuxOutputs returns optional extra outputs.
|
||||
func (c *ChanCloser) AuxOutputs() fn.Option[AuxCloseOutputs] {
|
||||
return c.auxOutputs
|
||||
}
|
||||
|
||||
// validateShutdownScript attempts to match and validate the script provided in
|
||||
// our peer's shutdown message with the upfront shutdown script we have on
|
||||
// record. For any script specified, we also make sure it matches our
|
||||
|
@ -503,6 +590,17 @@ func (c *ChanCloser) ReceiveShutdown(msg lnwire.Shutdown) (
|
|||
|
||||
noShutdown := fn.None[lnwire.Shutdown]()
|
||||
|
||||
// We'll track their remote close output, even if it's dust in BTC
|
||||
// terms, it might still carry value in custom channel terms.
|
||||
_, dustAmt := c.cfg.Channel.RemoteBalanceDust()
|
||||
_, remoteBalance := c.cfg.Channel.CommitBalances()
|
||||
c.remoteCloseOutput = fn.Some(CloseOutput{
|
||||
Amt: remoteBalance,
|
||||
DustLimit: dustAmt,
|
||||
PkScript: msg.Address,
|
||||
ShutdownRecords: msg.CustomRecords,
|
||||
})
|
||||
|
||||
switch c.state {
|
||||
// If we're in the close idle state, and we're receiving a channel
|
||||
// closure related message, then this indicates that we're on the
|
||||
|
@ -850,6 +948,25 @@ func (c *ChanCloser) ReceiveClosingSigned( //nolint:funlen
|
|||
}
|
||||
}
|
||||
|
||||
// Before we complete the cooperative close, we'll see if we
|
||||
// have any extra aux options.
|
||||
c.auxOutputs, err = c.auxCloseOutputs(remoteProposedFee)
|
||||
if err != nil {
|
||||
return noClosing, err
|
||||
}
|
||||
c.auxOutputs.WhenSome(func(outs AuxCloseOutputs) {
|
||||
closeOpts = append(
|
||||
closeOpts, lnwallet.WithExtraCloseOutputs(
|
||||
outs.ExtraCloseOutputs,
|
||||
),
|
||||
)
|
||||
closeOpts = append(
|
||||
closeOpts, lnwallet.WithCustomCoopSort(
|
||||
outs.CustomSort,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
closeTx, _, err := c.cfg.Channel.CompleteCooperativeClose(
|
||||
localSig, remoteSig, c.localDeliveryScript,
|
||||
c.remoteDeliveryScript, remoteProposedFee, closeOpts...,
|
||||
|
@ -859,6 +976,33 @@ func (c *ChanCloser) ReceiveClosingSigned( //nolint:funlen
|
|||
}
|
||||
c.closingTx = closeTx
|
||||
|
||||
// If there's an aux chan closer, then we'll finalize with it
|
||||
// before we write to disk.
|
||||
err = fn.MapOptionZ(
|
||||
c.cfg.AuxCloser, func(aux AuxChanCloser) error {
|
||||
channel := c.cfg.Channel
|
||||
//nolint:lll
|
||||
req := AuxShutdownReq{
|
||||
ChanPoint: c.chanPoint,
|
||||
ShortChanID: c.cfg.Channel.ShortChanID(),
|
||||
InternalKey: c.localInternalKey,
|
||||
Initiator: channel.IsInitiator(),
|
||||
CommitBlob: channel.LocalCommitmentBlob(),
|
||||
FundingBlob: channel.FundingBlob(),
|
||||
}
|
||||
desc := AuxCloseDesc{
|
||||
AuxShutdownReq: req,
|
||||
LocalCloseOutput: c.localCloseOutput,
|
||||
RemoteCloseOutput: c.remoteCloseOutput,
|
||||
}
|
||||
|
||||
return aux.FinalizeClose(desc, closeTx)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return noClosing, err
|
||||
}
|
||||
|
||||
// Before publishing the closing tx, we persist it to the
|
||||
// database, such that it can be republished if something goes
|
||||
// wrong.
|
||||
|
@ -908,9 +1052,46 @@ func (c *ChanCloser) ReceiveClosingSigned( //nolint:funlen
|
|||
}
|
||||
}
|
||||
|
||||
// auxCloseOutputs returns any additional outputs that should be used when
|
||||
// closing the channel.
|
||||
func (c *ChanCloser) auxCloseOutputs(
|
||||
closeFee btcutil.Amount) (fn.Option[AuxCloseOutputs], error) {
|
||||
|
||||
var closeOuts fn.Option[AuxCloseOutputs]
|
||||
err := fn.MapOptionZ(c.cfg.AuxCloser, func(aux AuxChanCloser) error {
|
||||
req := AuxShutdownReq{
|
||||
ChanPoint: c.chanPoint,
|
||||
ShortChanID: c.cfg.Channel.ShortChanID(),
|
||||
InternalKey: c.localInternalKey,
|
||||
Initiator: c.cfg.Channel.IsInitiator(),
|
||||
CommitBlob: c.cfg.Channel.LocalCommitmentBlob(),
|
||||
FundingBlob: c.cfg.Channel.FundingBlob(),
|
||||
}
|
||||
outs, err := aux.AuxCloseOutputs(AuxCloseDesc{
|
||||
AuxShutdownReq: req,
|
||||
CloseFee: closeFee,
|
||||
CommitFee: c.cfg.Channel.CommitFee(),
|
||||
LocalCloseOutput: c.localCloseOutput,
|
||||
RemoteCloseOutput: c.remoteCloseOutput,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
closeOuts = outs
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return closeOuts, err
|
||||
}
|
||||
|
||||
return closeOuts, nil
|
||||
}
|
||||
|
||||
// proposeCloseSigned attempts to propose a new signature for the closing
|
||||
// transaction for a channel based on the prior fee negotiations and our current
|
||||
// compromise fee.
|
||||
// transaction for a channel based on the prior fee negotiations and our
|
||||
// current compromise fee.
|
||||
func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (
|
||||
*lnwire.ClosingSigned, error) {
|
||||
|
||||
|
@ -928,6 +1109,26 @@ func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (
|
|||
}
|
||||
}
|
||||
|
||||
// We'll also now see if the aux chan closer has any additional options
|
||||
// for the closing purpose.
|
||||
c.auxOutputs, err = c.auxCloseOutputs(fee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.auxOutputs.WhenSome(func(outs AuxCloseOutputs) {
|
||||
closeOpts = append(
|
||||
closeOpts, lnwallet.WithExtraCloseOutputs(
|
||||
outs.ExtraCloseOutputs,
|
||||
),
|
||||
)
|
||||
closeOpts = append(
|
||||
closeOpts, lnwallet.WithCustomCoopSort(
|
||||
outs.CustomSort,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
// With all our options added, we'll attempt to co-op close now.
|
||||
rawSig, _, _, err := c.cfg.Channel.CreateCloseProposal(
|
||||
fee, c.localDeliveryScript, c.remoteDeliveryScript,
|
||||
closeOpts...,
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -152,6 +153,14 @@ func (m *mockChannel) ChannelPoint() wire.OutPoint {
|
|||
return m.chanPoint
|
||||
}
|
||||
|
||||
func (m *mockChannel) LocalCommitmentBlob() fn.Option[tlv.Blob] {
|
||||
return fn.None[tlv.Blob]()
|
||||
}
|
||||
|
||||
func (m *mockChannel) FundingBlob() fn.Option[tlv.Blob] {
|
||||
return fn.None[tlv.Blob]()
|
||||
}
|
||||
|
||||
func (m *mockChannel) MarkCoopBroadcasted(*wire.MsgTx,
|
||||
lntypes.ChannelParty) error {
|
||||
|
||||
|
@ -205,12 +214,20 @@ func (m *mockChannel) CompleteCooperativeClose(localSig,
|
|||
return &wire.MsgTx{}, 0, nil
|
||||
}
|
||||
|
||||
func (m *mockChannel) LocalBalanceDust() bool {
|
||||
return false
|
||||
func (m *mockChannel) LocalBalanceDust() (bool, btcutil.Amount) {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
func (m *mockChannel) RemoteBalanceDust() bool {
|
||||
return false
|
||||
func (m *mockChannel) RemoteBalanceDust() (bool, btcutil.Amount) {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
func (m *mockChannel) CommitBalances() (btcutil.Amount, btcutil.Amount) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func (m *mockChannel) CommitFee() btcutil.Amount {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *mockChannel) ChanType() channeldb.ChannelType {
|
||||
|
@ -344,7 +361,8 @@ func TestMaxFeeClamp(t *testing.T) {
|
|||
Channel: &channel,
|
||||
MaxFee: test.inputMaxFee,
|
||||
FeeEstimator: &SimpleCoopFeeEstimator{},
|
||||
}, nil, test.idealFee, 0, nil, lntypes.Remote,
|
||||
}, DeliveryAddrWithKey{}, test.idealFee, 0, nil,
|
||||
lntypes.Remote,
|
||||
)
|
||||
|
||||
// We'll call initFeeBaseline early here since we need
|
||||
|
@ -385,7 +403,8 @@ func TestMaxFeeBailOut(t *testing.T) {
|
|||
MaxFee: idealFee * 2,
|
||||
}
|
||||
chanCloser := NewChanCloser(
|
||||
closeCfg, nil, idealFee, 0, nil, lntypes.Remote,
|
||||
closeCfg, DeliveryAddrWithKey{}, idealFee, 0,
|
||||
nil, lntypes.Remote,
|
||||
)
|
||||
|
||||
// We'll now force the channel state into the
|
||||
|
@ -509,7 +528,7 @@ func TestTaprootFastClose(t *testing.T) {
|
|||
DisableChannel: func(wire.OutPoint) error {
|
||||
return nil
|
||||
},
|
||||
}, nil, idealFee, 0, nil, lntypes.Local,
|
||||
}, DeliveryAddrWithKey{}, idealFee, 0, nil, lntypes.Local,
|
||||
)
|
||||
aliceCloser.initFeeBaseline()
|
||||
|
||||
|
@ -526,7 +545,7 @@ func TestTaprootFastClose(t *testing.T) {
|
|||
DisableChannel: func(wire.OutPoint) error {
|
||||
return nil
|
||||
},
|
||||
}, nil, idealFee, 0, nil, lntypes.Remote,
|
||||
}, DeliveryAddrWithKey{}, idealFee, 0, nil, lntypes.Remote,
|
||||
)
|
||||
bobCloser.initFeeBaseline()
|
||||
|
||||
|
|
|
@ -6,11 +6,13 @@ import (
|
|||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
// CoopFeeEstimator is used to estimate the fee of a co-op close transaction.
|
||||
|
@ -32,6 +34,14 @@ type Channel interface { //nolint:interfacebloat
|
|||
// ChannelPoint returns the channel point of the target channel.
|
||||
ChannelPoint() wire.OutPoint
|
||||
|
||||
// LocalCommitmentBlob may return the auxiliary data storage blob for
|
||||
// the local commitment transaction.
|
||||
LocalCommitmentBlob() fn.Option[tlv.Blob]
|
||||
|
||||
// FundingBlob may return the auxiliary data storage blob related to
|
||||
// funding details for the channel.
|
||||
FundingBlob() fn.Option[tlv.Blob]
|
||||
|
||||
// MarkCoopBroadcasted persistently marks that the channel close
|
||||
// transaction has been broadcast.
|
||||
MarkCoopBroadcasted(*wire.MsgTx, lntypes.ChannelParty) error
|
||||
|
@ -60,13 +70,23 @@ type Channel interface { //nolint:interfacebloat
|
|||
|
||||
// LocalBalanceDust returns true if when creating a co-op close
|
||||
// transaction, the balance of the local party will be dust after
|
||||
// accounting for any anchor outputs.
|
||||
LocalBalanceDust() bool
|
||||
// accounting for any anchor outputs. The dust value for the local
|
||||
// party is also returned.
|
||||
LocalBalanceDust() (bool, btcutil.Amount)
|
||||
|
||||
// RemoteBalanceDust returns true if when creating a co-op close
|
||||
// transaction, the balance of the remote party will be dust after
|
||||
// accounting for any anchor outputs.
|
||||
RemoteBalanceDust() bool
|
||||
// accounting for any anchor outputs. The dust value the remote party
|
||||
// is also returned.
|
||||
RemoteBalanceDust() (bool, btcutil.Amount)
|
||||
|
||||
// CommitBalances returns the local and remote balances in the current
|
||||
// commitment state.
|
||||
CommitBalances() (btcutil.Amount, btcutil.Amount)
|
||||
|
||||
// CommitFee returns the commitment fee for the current commitment
|
||||
// state.
|
||||
CommitFee() btcutil.Amount
|
||||
|
||||
// RemoteUpfrontShutdownScript returns the upfront shutdown script of
|
||||
// the remote party. If the remote party didn't specify such a script,
|
||||
|
|
|
@ -7691,10 +7691,30 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
|
|||
}, nil
|
||||
}
|
||||
|
||||
// CloseOutput wraps a normal tx out with additional metadata that indicates if
|
||||
// the output belongs to the initiator of the channel or not.
|
||||
type CloseOutput struct {
|
||||
wire.TxOut
|
||||
|
||||
// IsLocal indicates if the output belong to the local party.
|
||||
IsLocal bool
|
||||
}
|
||||
|
||||
// CloseSortFunc is a function type alias for a function that sorts the closing
|
||||
// transaction.
|
||||
type CloseSortFunc func(*wire.MsgTx) error
|
||||
|
||||
// chanCloseOpt is a functional option that can be used to modify the co-op
|
||||
// close process.
|
||||
type chanCloseOpt struct {
|
||||
musigSession *MusigSession
|
||||
|
||||
extraCloseOutputs []CloseOutput
|
||||
|
||||
// customSort is a custom function that can be used to sort the
|
||||
// transaction outputs. If this isn't set, then the default BIP-69
|
||||
// sorting is used.
|
||||
customSort CloseSortFunc
|
||||
}
|
||||
|
||||
// ChanCloseOpt is a closure type that cen be used to modify the set of default
|
||||
|
@ -7715,6 +7735,22 @@ func WithCoopCloseMusigSession(session *MusigSession) ChanCloseOpt {
|
|||
}
|
||||
}
|
||||
|
||||
// WithExtraCloseOutputs can be used to add extra outputs to the cooperative
|
||||
// transaction.
|
||||
func WithExtraCloseOutputs(extraOutputs []CloseOutput) ChanCloseOpt {
|
||||
return func(opts *chanCloseOpt) {
|
||||
opts.extraCloseOutputs = extraOutputs
|
||||
}
|
||||
}
|
||||
|
||||
// WithCustomCoopSort can be used to modify the way the co-op close transaction
|
||||
// is sorted.
|
||||
func WithCustomCoopSort(sorter CloseSortFunc) ChanCloseOpt {
|
||||
return func(opts *chanCloseOpt) {
|
||||
opts.customSort = sorter
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCloseProposal is used by both parties in a cooperative channel close
|
||||
// workflow to generate proposed close transactions and signatures. This method
|
||||
// should only be executed once all pending HTLCs (if any) on the channel have
|
||||
|
@ -7722,9 +7758,6 @@ func WithCoopCloseMusigSession(session *MusigSession) ChanCloseOpt {
|
|||
// the "closing" state, which indicates that all incoming/outgoing HTLC
|
||||
// requests should be rejected. A signature for the closing transaction is
|
||||
// returned.
|
||||
//
|
||||
// TODO(roasbeef): caller should initiate signal to reject all incoming HTLCs,
|
||||
// settle any in flight.
|
||||
func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||
localDeliveryScript []byte, remoteDeliveryScript []byte,
|
||||
closeOpts ...ChanCloseOpt) (input.Signature, *chainhash.Hash,
|
||||
|
@ -7735,7 +7768,6 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
|||
|
||||
// If we're already closing the channel, then ignore this request.
|
||||
if lc.isClosed {
|
||||
// TODO(roasbeef): check to ensure no pending payments
|
||||
return nil, nil, 0, ErrChanClosing
|
||||
}
|
||||
|
||||
|
@ -7749,7 +7781,10 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
|||
// during the channel closing process.
|
||||
ourBalance, theirBalance, err := CoopCloseBalance(
|
||||
lc.channelState.ChanType, lc.channelState.IsInitiator,
|
||||
proposedFee, lc.channelState.LocalCommitment,
|
||||
proposedFee,
|
||||
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
||||
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
||||
lc.channelState.LocalCommitment.CommitFee,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
|
@ -7762,11 +7797,27 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
|||
closeTxOpts = append(closeTxOpts, WithRBFCloseTx())
|
||||
}
|
||||
|
||||
closeTx := CreateCooperativeCloseTx(
|
||||
// If we have any extra outputs to pass along, then we'll map that to
|
||||
// the co-op close option txn type.
|
||||
if opts.extraCloseOutputs != nil {
|
||||
closeTxOpts = append(closeTxOpts, WithExtraTxCloseOutputs(
|
||||
opts.extraCloseOutputs,
|
||||
))
|
||||
}
|
||||
if opts.customSort != nil {
|
||||
closeTxOpts = append(
|
||||
closeTxOpts, WithCustomTxSort(opts.customSort),
|
||||
)
|
||||
}
|
||||
|
||||
closeTx, err := CreateCooperativeCloseTx(
|
||||
fundingTxIn(lc.channelState), lc.channelState.LocalChanCfg.DustLimit,
|
||||
lc.channelState.RemoteChanCfg.DustLimit, ourBalance, theirBalance,
|
||||
localDeliveryScript, remoteDeliveryScript, closeTxOpts...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
// Ensure that the transaction doesn't explicitly violate any
|
||||
// consensus rules such as being too big, or having any value with a
|
||||
|
@ -7831,7 +7882,10 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
|||
// Get the final balances after subtracting the proposed fee.
|
||||
ourBalance, theirBalance, err := CoopCloseBalance(
|
||||
lc.channelState.ChanType, lc.channelState.IsInitiator,
|
||||
proposedFee, lc.channelState.LocalCommitment,
|
||||
proposedFee,
|
||||
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
||||
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
||||
lc.channelState.LocalCommitment.CommitFee,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
|
@ -7844,14 +7898,30 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
|||
closeTxOpts = append(closeTxOpts, WithRBFCloseTx())
|
||||
}
|
||||
|
||||
// If we have any extra outputs to pass along, then we'll map that to
|
||||
// the co-op close option txn type.
|
||||
if opts.extraCloseOutputs != nil {
|
||||
closeTxOpts = append(closeTxOpts, WithExtraTxCloseOutputs(
|
||||
opts.extraCloseOutputs,
|
||||
))
|
||||
}
|
||||
if opts.customSort != nil {
|
||||
closeTxOpts = append(
|
||||
closeTxOpts, WithCustomTxSort(opts.customSort),
|
||||
)
|
||||
}
|
||||
|
||||
// Create the transaction used to return the current settled balance
|
||||
// on this active channel back to both parties. In this current model,
|
||||
// the initiator pays full fees for the cooperative close transaction.
|
||||
closeTx := CreateCooperativeCloseTx(
|
||||
closeTx, err := CreateCooperativeCloseTx(
|
||||
fundingTxIn(lc.channelState), lc.channelState.LocalChanCfg.DustLimit,
|
||||
lc.channelState.RemoteChanCfg.DustLimit, ourBalance, theirBalance,
|
||||
localDeliveryScript, remoteDeliveryScript, closeTxOpts...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Ensure that the transaction doesn't explicitly validate any
|
||||
// consensus rules such as being too big, or having any value with a
|
||||
|
@ -8549,6 +8619,15 @@ type closeTxOpts struct {
|
|||
// enableRBF indicates whether the cooperative close tx should signal
|
||||
// RBF or not.
|
||||
enableRBF bool
|
||||
|
||||
// extraCloseOutputs is a set of additional outputs that should be
|
||||
// added the co-op close transaction.
|
||||
extraCloseOutputs []CloseOutput
|
||||
|
||||
// customSort is a custom function that can be used to sort the
|
||||
// transaction outputs. If this isn't set, then the default BIP-69
|
||||
// sorting is used.
|
||||
customSort CloseSortFunc
|
||||
}
|
||||
|
||||
// defaultCloseTxOpts returns a closeTxOpts struct with default values.
|
||||
|
@ -8569,6 +8648,22 @@ func WithRBFCloseTx() CloseTxOpt {
|
|||
}
|
||||
}
|
||||
|
||||
// WithExtraTxCloseOutputs can be used to add extra outputs to the cooperative
|
||||
// transaction.
|
||||
func WithExtraTxCloseOutputs(extraOutputs []CloseOutput) CloseTxOpt {
|
||||
return func(o *closeTxOpts) {
|
||||
o.extraCloseOutputs = extraOutputs
|
||||
}
|
||||
}
|
||||
|
||||
// WithCustomTxSort can be used to modify the way the close transaction is
|
||||
// sorted.
|
||||
func WithCustomTxSort(sorter CloseSortFunc) CloseTxOpt {
|
||||
return func(opts *closeTxOpts) {
|
||||
opts.customSort = sorter
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCooperativeCloseTx creates a transaction which if signed by both
|
||||
// parties, then broadcast cooperatively closes an active channel. The creation
|
||||
// of the closure transaction is modified by a boolean indicating if the party
|
||||
|
@ -8578,7 +8673,7 @@ func WithRBFCloseTx() CloseTxOpt {
|
|||
func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
|
||||
localDust, remoteDust, ourBalance, theirBalance btcutil.Amount,
|
||||
ourDeliveryScript, theirDeliveryScript []byte,
|
||||
closeOpts ...CloseTxOpt) *wire.MsgTx {
|
||||
closeOpts ...CloseTxOpt) (*wire.MsgTx, error) {
|
||||
|
||||
opts := defaultCloseTxOpts()
|
||||
for _, optFunc := range closeOpts {
|
||||
|
@ -8600,28 +8695,132 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
|
|||
|
||||
// Create both cooperative closure outputs, properly respecting the
|
||||
// dust limits of both parties.
|
||||
if ourBalance >= localDust {
|
||||
var localOutputIdx fn.Option[int]
|
||||
haveLocalOutput := ourBalance >= localDust
|
||||
if haveLocalOutput {
|
||||
closeTx.AddTxOut(&wire.TxOut{
|
||||
PkScript: ourDeliveryScript,
|
||||
Value: int64(ourBalance),
|
||||
})
|
||||
|
||||
localOutputIdx = fn.Some(len(closeTx.TxOut) - 1)
|
||||
}
|
||||
if theirBalance >= remoteDust {
|
||||
|
||||
var remoteOutputIdx fn.Option[int]
|
||||
haveRemoteOutput := theirBalance >= remoteDust
|
||||
if haveRemoteOutput {
|
||||
closeTx.AddTxOut(&wire.TxOut{
|
||||
PkScript: theirDeliveryScript,
|
||||
Value: int64(theirBalance),
|
||||
})
|
||||
|
||||
remoteOutputIdx = fn.Some(len(closeTx.TxOut) - 1)
|
||||
}
|
||||
|
||||
txsort.InPlaceSort(closeTx)
|
||||
// If we have extra outputs to add to the co-op close transaction, then
|
||||
// we'll examine them now. We'll deduct the output's value from the
|
||||
// owning party. In the case that a party can't pay for the output, then
|
||||
// their normal output will be omitted.
|
||||
for _, extraTxOut := range opts.extraCloseOutputs {
|
||||
switch {
|
||||
// For additional local outputs, add the output, then deduct
|
||||
// the balance from our local balance.
|
||||
case extraTxOut.IsLocal:
|
||||
// The extraCloseOutputs in the options just indicate if
|
||||
// an extra output should be added in general. But we
|
||||
// only add one if we actually _need_ one, based on the
|
||||
// balance. If we don't have enough local balance to
|
||||
// cover the extra output, then localOutputIdx is None.
|
||||
localOutputIdx.WhenSome(func(idx int) {
|
||||
// The output that currently represents the
|
||||
// local balance, which means:
|
||||
// txOut.Value == ourBalance.
|
||||
txOut := closeTx.TxOut[idx]
|
||||
|
||||
return closeTx
|
||||
// The extra output (if one exists) is the more
|
||||
// important one, as in custom channels it might
|
||||
// carry some additional values. The normal
|
||||
// output is just an address that sends the
|
||||
// local balance back to our wallet. The extra
|
||||
// one also goes to our wallet, but might also
|
||||
// carry other values, so it has higher
|
||||
// priority. Do we have enough balance to have
|
||||
// both the extra output with the given value
|
||||
// (which is subtracted from our balance) and
|
||||
// still an above-dust normal output? If not, we
|
||||
// skip the extra output and just overwrite the
|
||||
// existing output script with the one from the
|
||||
// extra output.
|
||||
amtAfterOutput := btcutil.Amount(
|
||||
txOut.Value - extraTxOut.Value,
|
||||
)
|
||||
if amtAfterOutput <= localDust {
|
||||
txOut.PkScript = extraTxOut.PkScript
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
txOut.Value -= extraTxOut.Value
|
||||
closeTx.AddTxOut(&extraTxOut.TxOut)
|
||||
})
|
||||
|
||||
// For extra remote outputs, we'll do the opposite.
|
||||
case !extraTxOut.IsLocal:
|
||||
// The extraCloseOutputs in the options just indicate if
|
||||
// an extra output should be added in general. But we
|
||||
// only add one if we actually _need_ one, based on the
|
||||
// balance. If we don't have enough remote balance to
|
||||
// cover the extra output, then remoteOutputIdx is None.
|
||||
remoteOutputIdx.WhenSome(func(idx int) {
|
||||
// The output that currently represents the
|
||||
// remote balance, which means:
|
||||
// txOut.Value == theirBalance.
|
||||
txOut := closeTx.TxOut[idx]
|
||||
|
||||
// The extra output (if one exists) is the more
|
||||
// important one, as in custom channels it might
|
||||
// carry some additional values. The normal
|
||||
// output is just an address that sends the
|
||||
// remote balance back to their wallet. The
|
||||
// extra one also goes to their wallet, but
|
||||
// might also carry other values, so it has
|
||||
// higher priority. Do they have enough balance
|
||||
// to have both the extra output with the given
|
||||
// value (which is subtracted from their
|
||||
// balance) and still an above-dust normal
|
||||
// output? If not, we skip the extra output and
|
||||
// just overwrite the existing output script
|
||||
// with the one from the extra output.
|
||||
amtAfterOutput := btcutil.Amount(
|
||||
txOut.Value - extraTxOut.Value,
|
||||
)
|
||||
if amtAfterOutput <= remoteDust {
|
||||
txOut.PkScript = extraTxOut.PkScript
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
txOut.Value -= extraTxOut.Value
|
||||
closeTx.AddTxOut(&extraTxOut.TxOut)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if opts.customSort != nil {
|
||||
if err := opts.customSort(closeTx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
txsort.InPlaceSort(closeTx)
|
||||
}
|
||||
|
||||
return closeTx, nil
|
||||
}
|
||||
|
||||
// LocalBalanceDust returns true if when creating a co-op close transaction,
|
||||
// the balance of the local party will be dust after accounting for any anchor
|
||||
// outputs.
|
||||
func (lc *LightningChannel) LocalBalanceDust() bool {
|
||||
func (lc *LightningChannel) LocalBalanceDust() (bool, btcutil.Amount) {
|
||||
lc.RLock()
|
||||
defer lc.RUnlock()
|
||||
|
||||
|
@ -8635,13 +8834,15 @@ func (lc *LightningChannel) LocalBalanceDust() bool {
|
|||
localBalance += 2 * AnchorSize
|
||||
}
|
||||
|
||||
return localBalance <= chanState.LocalChanCfg.DustLimit
|
||||
localDust := chanState.LocalChanCfg.DustLimit
|
||||
|
||||
return localBalance <= localDust, localDust
|
||||
}
|
||||
|
||||
// RemoteBalanceDust returns true if when creating a co-op close transaction,
|
||||
// the balance of the remote party will be dust after accounting for any anchor
|
||||
// outputs.
|
||||
func (lc *LightningChannel) RemoteBalanceDust() bool {
|
||||
func (lc *LightningChannel) RemoteBalanceDust() (bool, btcutil.Amount) {
|
||||
lc.RLock()
|
||||
defer lc.RUnlock()
|
||||
|
||||
|
@ -8655,7 +8856,40 @@ func (lc *LightningChannel) RemoteBalanceDust() bool {
|
|||
remoteBalance += 2 * AnchorSize
|
||||
}
|
||||
|
||||
return remoteBalance <= chanState.RemoteChanCfg.DustLimit
|
||||
remoteDust := chanState.RemoteChanCfg.DustLimit
|
||||
|
||||
return remoteBalance <= remoteDust, remoteDust
|
||||
}
|
||||
|
||||
// CommitBalances returns the local and remote balances in the current
|
||||
// commitment state.
|
||||
func (lc *LightningChannel) CommitBalances() (btcutil.Amount, btcutil.Amount) {
|
||||
lc.RLock()
|
||||
defer lc.RUnlock()
|
||||
|
||||
chanState := lc.channelState
|
||||
localCommit := lc.channelState.LocalCommitment
|
||||
|
||||
localBalance := localCommit.LocalBalance.ToSatoshis()
|
||||
remoteBalance := localCommit.RemoteBalance.ToSatoshis()
|
||||
|
||||
if chanState.ChanType.HasAnchors() {
|
||||
if chanState.IsInitiator {
|
||||
localBalance += 2 * AnchorSize
|
||||
} else {
|
||||
remoteBalance += 2 * AnchorSize
|
||||
}
|
||||
}
|
||||
|
||||
return localBalance, remoteBalance
|
||||
}
|
||||
|
||||
// CommitFee returns the commitment fee for the current commitment state.
|
||||
func (lc *LightningChannel) CommitFee() btcutil.Amount {
|
||||
lc.RLock()
|
||||
defer lc.RUnlock()
|
||||
|
||||
return lc.channelState.LocalCommitment.CommitFee
|
||||
}
|
||||
|
||||
// CalcFee returns the commitment fee to use for the given fee rate
|
||||
|
@ -9095,3 +9329,16 @@ func (lc *LightningChannel) LocalCommitmentBlob() fn.Option[tlv.Blob] {
|
|||
return newBlob
|
||||
})(localBalance)
|
||||
}
|
||||
|
||||
// FundingBlob returns the funding custom blob.
|
||||
func (lc *LightningChannel) FundingBlob() fn.Option[tlv.Blob] {
|
||||
lc.RLock()
|
||||
defer lc.RUnlock()
|
||||
|
||||
return fn.MapOption(func(b tlv.Blob) tlv.Blob {
|
||||
newBlob := make([]byte, len(b))
|
||||
copy(newBlob, b)
|
||||
|
||||
return newBlob
|
||||
})(lc.channelState.CustomBlob)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/txsort"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
|
@ -11435,3 +11436,316 @@ func TestBlindingPointPersistence(t *testing.T) {
|
|||
require.Equal(t, blinding,
|
||||
bobCommit.incomingHTLCs[0].BlindingPoint.UnwrapOrFailV(t))
|
||||
}
|
||||
|
||||
// TestCreateCooperativeCloseTx tests that the cooperative close transaction is
|
||||
// properly created based on the standard and also optional parameters.
|
||||
func TestCreateCooperativeCloseTx(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fundingTxIn := &wire.TxIn{}
|
||||
|
||||
localDust := btcutil.Amount(400)
|
||||
remoteDust := btcutil.Amount(400)
|
||||
|
||||
localScript := []byte{0}
|
||||
localExtraScript := []byte{2}
|
||||
|
||||
remoteScript := []byte{1}
|
||||
remoteExtraScript := []byte{3}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
enableRBF bool
|
||||
|
||||
localBalance btcutil.Amount
|
||||
remoteBalance btcutil.Amount
|
||||
|
||||
extraCloseOutputs []CloseOutput
|
||||
|
||||
expectedTx *wire.MsgTx
|
||||
}{
|
||||
{
|
||||
name: "no dust, no extra outputs",
|
||||
localBalance: 1_000,
|
||||
remoteBalance: 1_000,
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: localScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: remoteScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local dust, no extra outputs",
|
||||
localBalance: 100,
|
||||
remoteBalance: 1_000,
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: remoteScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote dust, no extra outputs",
|
||||
localBalance: 1_000,
|
||||
remoteBalance: 100,
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: localScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no dust, local extra output",
|
||||
localBalance: 10_000,
|
||||
remoteBalance: 10_000,
|
||||
extraCloseOutputs: []CloseOutput{
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
IsLocal: true,
|
||||
},
|
||||
},
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 10_000,
|
||||
PkScript: remoteScript,
|
||||
},
|
||||
{
|
||||
Value: 9_000,
|
||||
PkScript: localScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no dust, remote extra output",
|
||||
localBalance: 10_000,
|
||||
remoteBalance: 10_000,
|
||||
extraCloseOutputs: []CloseOutput{
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
IsLocal: false,
|
||||
},
|
||||
},
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 10_000,
|
||||
PkScript: localScript,
|
||||
},
|
||||
{
|
||||
Value: 9_000,
|
||||
PkScript: remoteScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no dust, local+remote extra output",
|
||||
localBalance: 10_000,
|
||||
remoteBalance: 10_000,
|
||||
extraCloseOutputs: []CloseOutput{
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
IsLocal: false,
|
||||
},
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
IsLocal: true,
|
||||
},
|
||||
},
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 9_000,
|
||||
PkScript: localScript,
|
||||
},
|
||||
{
|
||||
Value: 9_000,
|
||||
PkScript: remoteScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no dust, local+remote extra output, " +
|
||||
"remote can't afford",
|
||||
localBalance: 10_000,
|
||||
remoteBalance: 1_000,
|
||||
extraCloseOutputs: []CloseOutput{
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
IsLocal: false,
|
||||
},
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
IsLocal: true,
|
||||
},
|
||||
},
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 9_000,
|
||||
PkScript: localScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no dust, local+remote extra output, " +
|
||||
"local can't afford",
|
||||
localBalance: 1_000,
|
||||
remoteBalance: 10_000,
|
||||
extraCloseOutputs: []CloseOutput{
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
IsLocal: false,
|
||||
},
|
||||
{
|
||||
TxOut: wire.TxOut{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
IsLocal: true,
|
||||
},
|
||||
},
|
||||
expectedTx: &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
fundingTxIn,
|
||||
},
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 9_000,
|
||||
PkScript: remoteScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: remoteExtraScript,
|
||||
},
|
||||
{
|
||||
Value: 1_000,
|
||||
PkScript: localExtraScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var opts []CloseTxOpt
|
||||
if test.extraCloseOutputs != nil {
|
||||
opts = append(
|
||||
opts,
|
||||
WithExtraTxCloseOutputs(
|
||||
test.extraCloseOutputs,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
closeTx, err := CreateCooperativeCloseTx(
|
||||
*fundingTxIn, localDust, remoteDust,
|
||||
test.localBalance, test.remoteBalance,
|
||||
localScript, remoteScript, opts...,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
txsort.InPlaceSort(test.expectedTx)
|
||||
|
||||
require.Equal(
|
||||
t, test.expectedTx, closeTx,
|
||||
"expected %v, got %v",
|
||||
spew.Sdump(test.expectedTx),
|
||||
spew.Sdump(closeTx),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1033,16 +1033,12 @@ func CreateCommitTx(chanType channeldb.ChannelType,
|
|||
// CoopCloseBalance returns the final balances that should be used to create
|
||||
// the cooperative close tx, given the channel type and transaction fee.
|
||||
func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
|
||||
coopCloseFee btcutil.Amount, localCommit channeldb.ChannelCommitment) (
|
||||
btcutil.Amount, btcutil.Amount, error) {
|
||||
|
||||
// Get both parties' balances from the latest commitment.
|
||||
ourBalance := localCommit.LocalBalance.ToSatoshis()
|
||||
theirBalance := localCommit.RemoteBalance.ToSatoshis()
|
||||
coopCloseFee, ourBalance, theirBalance,
|
||||
commitFee btcutil.Amount) (btcutil.Amount, btcutil.Amount, error) {
|
||||
|
||||
// We'll make sure we account for the complete balance by adding the
|
||||
// current dangling commitment fee to the balance of the initiator.
|
||||
initiatorDelta := localCommit.CommitFee
|
||||
initiatorDelta := commitFee
|
||||
|
||||
// Since the initiator's balance also is stored after subtracting the
|
||||
// anchor values, add that back in case this was an anchor commitment.
|
||||
|
|
|
@ -89,6 +89,22 @@ func (c CustomRecords) Copy() CustomRecords {
|
|||
return customRecords
|
||||
}
|
||||
|
||||
// MergedCopy creates a copy of the records and merges them with the given
|
||||
// records. If the same key is present in both sets, the value from the other
|
||||
// records will be used.
|
||||
func (c CustomRecords) MergedCopy(other CustomRecords) CustomRecords {
|
||||
copiedRecords := make(CustomRecords, len(c))
|
||||
for k, v := range c {
|
||||
copiedRecords[k] = v
|
||||
}
|
||||
|
||||
for k, v := range other {
|
||||
copiedRecords[k] = v
|
||||
}
|
||||
|
||||
return copiedRecords
|
||||
}
|
||||
|
||||
// ExtendRecordProducers extends the given records slice with the custom
|
||||
// records. The resultant records slice will be sorted if the given records
|
||||
// slice contains TLV types greater than or equal to MinCustomRecordsTlvType.
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -194,3 +195,54 @@ func serializeRecordProducers(t *testing.T,
|
|||
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func TestCustomRecordsMergedCopy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
c CustomRecords
|
||||
other CustomRecords
|
||||
want CustomRecords
|
||||
}{
|
||||
{
|
||||
name: "nil records",
|
||||
want: make(CustomRecords),
|
||||
},
|
||||
{
|
||||
name: "empty records",
|
||||
c: make(CustomRecords),
|
||||
other: make(CustomRecords),
|
||||
want: make(CustomRecords),
|
||||
},
|
||||
{
|
||||
name: "distinct records",
|
||||
c: CustomRecords{
|
||||
1: {1, 2, 3},
|
||||
},
|
||||
other: CustomRecords{
|
||||
2: {4, 5, 6},
|
||||
},
|
||||
want: CustomRecords{
|
||||
1: {1, 2, 3},
|
||||
2: {4, 5, 6},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same records, different values",
|
||||
c: CustomRecords{
|
||||
1: {1, 2, 3},
|
||||
},
|
||||
other: CustomRecords{
|
||||
1: {4, 5, 6},
|
||||
},
|
||||
want: CustomRecords{
|
||||
1: {4, 5, 6},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.c.MergedCopy(tt.other)
|
||||
assert.Equal(t, tt.want, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -428,7 +428,7 @@ func randCustomRecords(t *testing.T, r *rand.Rand) CustomRecords {
|
|||
key := MinCustomRecordsTlvType + keyOffset
|
||||
|
||||
// Values are byte slices of any length.
|
||||
value := make([]byte, r.Intn(100))
|
||||
value := make([]byte, r.Intn(10))
|
||||
_, err := r.Read(value)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -771,7 +771,6 @@ func TestLightningWireProtocol(t *testing.T) {
|
|||
req := Shutdown{
|
||||
ChannelID: ChannelID(c),
|
||||
Address: shutdownAddr,
|
||||
ExtraData: make([]byte, 0),
|
||||
}
|
||||
|
||||
if r.Int31()%2 == 0 {
|
||||
|
@ -933,12 +932,14 @@ func TestLightningWireProtocol(t *testing.T) {
|
|||
// Only create the slice if there will be any signatures
|
||||
// in it to prevent false positive test failures due to
|
||||
// an empty slice versus a nil slice.
|
||||
numSigs := uint16(r.Int31n(1019))
|
||||
numSigs := uint16(r.Int31n(500))
|
||||
if numSigs > 0 {
|
||||
req.HtlcSigs = make([]Sig, numSigs)
|
||||
}
|
||||
for i := 0; i < int(numSigs); i++ {
|
||||
req.HtlcSigs[i], err = NewSigFromSignature(testSig)
|
||||
req.HtlcSigs[i], err = NewSigFromSignature(
|
||||
testSig,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to parse sig: %v", err)
|
||||
return
|
||||
|
|
|
@ -38,6 +38,11 @@ type Shutdown struct {
|
|||
// co-op sign offer.
|
||||
ShutdownNonce ShutdownNonceTLV
|
||||
|
||||
// CustomRecords maps TLV types to byte slices, storing arbitrary data
|
||||
// intended for inclusion in the ExtraData field of the Shutdown
|
||||
// message.
|
||||
CustomRecords CustomRecords
|
||||
|
||||
// ExtraData is the set of data that was appended to this message to
|
||||
// fill out the full maximum transport message size. These fields can
|
||||
// be used to specify optional data such as custom TLV fields.
|
||||
|
@ -56,7 +61,7 @@ func NewShutdown(cid ChannelID, addr DeliveryAddress) *Shutdown {
|
|||
// interface.
|
||||
var _ Message = (*Shutdown)(nil)
|
||||
|
||||
// Decode deserializes a serialized Shutdown stored in the passed io.Reader
|
||||
// Decode deserializes a serialized Shutdown from the passed io.Reader,
|
||||
// observing the specified protocol version.
|
||||
//
|
||||
// This is part of the lnwire.Message interface.
|
||||
|
@ -71,20 +76,23 @@ func (s *Shutdown) Decode(r io.Reader, pver uint32) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Extract TLV records from the extra data field.
|
||||
musigNonce := s.ShutdownNonce.Zero()
|
||||
typeMap, err := tlvRecords.ExtractRecords(&musigNonce)
|
||||
|
||||
customRecords, parsed, extraData, err := ParseAndExtractCustomRecords(
|
||||
tlvRecords, &musigNonce,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the corresponding TLV types if they were included in the stream.
|
||||
if val, ok := typeMap[s.ShutdownNonce.TlvType()]; ok && val == nil {
|
||||
// Assign the parsed records back to the message.
|
||||
if _, ok := parsed[musigNonce.TlvType()]; ok {
|
||||
s.ShutdownNonce = tlv.SomeRecordT(musigNonce)
|
||||
}
|
||||
|
||||
if len(tlvRecords) != 0 {
|
||||
s.ExtraData = tlvRecords
|
||||
}
|
||||
s.CustomRecords = customRecords
|
||||
s.ExtraData = extraData
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -94,17 +102,6 @@ func (s *Shutdown) Decode(r io.Reader, pver uint32) error {
|
|||
//
|
||||
// This is part of the lnwire.Message interface.
|
||||
func (s *Shutdown) Encode(w *bytes.Buffer, pver uint32) error {
|
||||
recordProducers := make([]tlv.RecordProducer, 0, 1)
|
||||
s.ShutdownNonce.WhenSome(
|
||||
func(nonce tlv.RecordT[ShutdownNonceType, Musig2Nonce]) {
|
||||
recordProducers = append(recordProducers, &nonce)
|
||||
},
|
||||
)
|
||||
err := EncodeMessageExtraData(&s.ExtraData, recordProducers...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := WriteChannelID(w, s.ChannelID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -113,7 +110,20 @@ func (s *Shutdown) Encode(w *bytes.Buffer, pver uint32) error {
|
|||
return err
|
||||
}
|
||||
|
||||
return WriteBytes(w, s.ExtraData)
|
||||
// Only include nonce in extra data if present.
|
||||
var records []tlv.RecordProducer
|
||||
s.ShutdownNonce.WhenSome(
|
||||
func(nonce tlv.RecordT[ShutdownNonceType, Musig2Nonce]) {
|
||||
records = append(records, &nonce)
|
||||
},
|
||||
)
|
||||
|
||||
extraData, err := MergeAndEncode(records, s.ExtraData, s.CustomRecords)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return WriteBytes(w, extraData)
|
||||
}
|
||||
|
||||
// MsgType returns the integer uniquely identifying this message type on the
|
||||
|
|
145
lnwire/shutdown_test.go
Normal file
145
lnwire/shutdown_test.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
package lnwire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testCaseShutdown is a test case for the Shutdown message.
|
||||
type testCaseShutdown struct {
|
||||
// Msg is the message to be encoded and decoded.
|
||||
Msg Shutdown
|
||||
|
||||
// ExpectEncodeError is a flag that indicates whether we expect the
|
||||
// encoding of the message to fail.
|
||||
ExpectEncodeError bool
|
||||
}
|
||||
|
||||
// generateShutdownTestCases generates a set of Shutdown message test cases.
|
||||
func generateShutdownTestCases(t *testing.T) []testCaseShutdown {
|
||||
// Firstly, we'll set basic values for the message fields.
|
||||
//
|
||||
// Generate random channel ID.
|
||||
chanIDBytes, err := generateRandomBytes(32)
|
||||
require.NoError(t, err)
|
||||
|
||||
var chanID ChannelID
|
||||
copy(chanID[:], chanIDBytes)
|
||||
|
||||
// Generate random payment preimage.
|
||||
paymentPreimageBytes, err := generateRandomBytes(32)
|
||||
require.NoError(t, err)
|
||||
|
||||
var paymentPreimage [32]byte
|
||||
copy(paymentPreimage[:], paymentPreimageBytes)
|
||||
|
||||
deliveryAddr, err := generateRandomBytes(16)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Define custom records.
|
||||
recordKey1 := uint64(MinCustomRecordsTlvType + 1)
|
||||
recordValue1, err := generateRandomBytes(10)
|
||||
require.NoError(t, err)
|
||||
|
||||
recordKey2 := uint64(MinCustomRecordsTlvType + 2)
|
||||
recordValue2, err := generateRandomBytes(10)
|
||||
require.NoError(t, err)
|
||||
|
||||
customRecords := CustomRecords{
|
||||
recordKey1: recordValue1,
|
||||
recordKey2: recordValue2,
|
||||
}
|
||||
|
||||
dummyPubKey, err := pubkeyFromHex(
|
||||
"0228f2af0abe322403480fb3ee172f7f1601e67d1da6cad40b54c4468d4" +
|
||||
"8236c39",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
muSig2Nonce, err := musig2.GenNonces(musig2.WithPublicKey(dummyPubKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Construct an instance of extra data that contains records with TLV
|
||||
// types below the minimum custom records threshold and that lack
|
||||
// corresponding fields in the message struct. Content should persist in
|
||||
// the extra data field after encoding and decoding.
|
||||
var (
|
||||
recordBytes45 = []byte("recordBytes45")
|
||||
tlvRecord45 = tlv.NewPrimitiveRecord[tlv.TlvType45](
|
||||
recordBytes45,
|
||||
)
|
||||
|
||||
recordBytes55 = []byte("recordBytes55")
|
||||
tlvRecord55 = tlv.NewPrimitiveRecord[tlv.TlvType55](
|
||||
recordBytes55,
|
||||
)
|
||||
)
|
||||
|
||||
var extraData ExtraOpaqueData
|
||||
err = extraData.PackRecords(
|
||||
[]tlv.RecordProducer{&tlvRecord45, &tlvRecord55}...,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
return []testCaseShutdown{
|
||||
{
|
||||
Msg: Shutdown{
|
||||
ChannelID: chanID,
|
||||
CustomRecords: customRecords,
|
||||
ExtraData: extraData,
|
||||
Address: deliveryAddr,
|
||||
},
|
||||
},
|
||||
{
|
||||
Msg: Shutdown{
|
||||
ChannelID: chanID,
|
||||
CustomRecords: customRecords,
|
||||
ExtraData: extraData,
|
||||
Address: deliveryAddr,
|
||||
ShutdownNonce: SomeShutdownNonce(
|
||||
muSig2Nonce.PubNonce,
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestShutdownEncodeDecode tests Shutdown message encoding and decoding for all
|
||||
// supported field values.
|
||||
func TestShutdownEncodeDecode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Generate test cases.
|
||||
testCases := generateShutdownTestCases(t)
|
||||
|
||||
// Execute test cases.
|
||||
for tcIdx, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("testcase-%d", tcIdx), func(t *testing.T) {
|
||||
// Encode test case message.
|
||||
var buf bytes.Buffer
|
||||
err := tc.Msg.Encode(&buf, 0)
|
||||
|
||||
// Check if we expect an encoding error.
|
||||
if tc.ExpectEncodeError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// Decode the encoded message bytes message.
|
||||
var actualMsg Shutdown
|
||||
decodeReader := bytes.NewReader(buf.Bytes())
|
||||
err = actualMsg.Decode(decodeReader, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Compare the two messages to ensure equality.
|
||||
require.Equal(t, tc.Msg, actualMsg)
|
||||
})
|
||||
}
|
||||
}
|
143
peer/brontide.go
143
peer/brontide.go
|
@ -13,11 +13,13 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/connmgr"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/buffer"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
|
@ -143,6 +145,21 @@ type PendingUpdate struct {
|
|||
type ChannelCloseUpdate struct {
|
||||
ClosingTxid []byte
|
||||
Success bool
|
||||
|
||||
// LocalCloseOutput is an optional, additional output on the closing
|
||||
// transaction that the local party should be paid to. This will only be
|
||||
// populated if the local balance isn't dust.
|
||||
LocalCloseOutput fn.Option[chancloser.CloseOutput]
|
||||
|
||||
// RemoteCloseOutput is an optional, additional output on the closing
|
||||
// transaction that the remote party should be paid to. This will only
|
||||
// be populated if the remote balance isn't dust.
|
||||
RemoteCloseOutput fn.Option[chancloser.CloseOutput]
|
||||
|
||||
// AuxOutputs is an optional set of additional outputs that might be
|
||||
// included in the closing transaction. These are used for custom
|
||||
// channel types.
|
||||
AuxOutputs fn.Option[chancloser.AuxCloseOutputs]
|
||||
}
|
||||
|
||||
// TimestampedError is a timestamped error that is used to store the most recent
|
||||
|
@ -400,6 +417,10 @@ type Config struct {
|
|||
// in place.
|
||||
MsgRouter fn.Option[msgmux.Router]
|
||||
|
||||
// AuxChanCloser is an optional instance of an abstraction that can be
|
||||
// used to modify the way the co-op close transaction is constructed.
|
||||
AuxChanCloser fn.Option[chancloser.AuxChanCloser]
|
||||
|
||||
// Quit is the server's quit channel. If this is closed, we halt operation.
|
||||
Quit chan struct{}
|
||||
}
|
||||
|
@ -881,6 +902,73 @@ func (p *Brontide) QuitSignal() <-chan struct{} {
|
|||
return p.quit
|
||||
}
|
||||
|
||||
// internalKeyForAddr returns the internal key associated with a taproot
|
||||
// address.
|
||||
func internalKeyForAddr(wallet *lnwallet.LightningWallet,
|
||||
deliveryScript []byte) (fn.Option[btcec.PublicKey], error) {
|
||||
|
||||
none := fn.None[btcec.PublicKey]()
|
||||
|
||||
pkScript, err := txscript.ParsePkScript(deliveryScript)
|
||||
if err != nil {
|
||||
return none, err
|
||||
}
|
||||
addr, err := pkScript.Address(&wallet.Cfg.NetParams)
|
||||
if err != nil {
|
||||
return none, err
|
||||
}
|
||||
|
||||
// If it's not a taproot address, we don't require to know the internal
|
||||
// key in the first place. So we don't return an error here, but also no
|
||||
// internal key.
|
||||
_, isTaproot := addr.(*btcutil.AddressTaproot)
|
||||
if !isTaproot {
|
||||
return none, nil
|
||||
}
|
||||
|
||||
walletAddr, err := wallet.AddressInfo(addr)
|
||||
if err != nil {
|
||||
return none, err
|
||||
}
|
||||
|
||||
// If the address isn't known to the wallet, we can't determine the
|
||||
// internal key.
|
||||
if walletAddr == nil {
|
||||
return none, nil
|
||||
}
|
||||
|
||||
pubKeyAddr, ok := walletAddr.(waddrmgr.ManagedPubKeyAddress)
|
||||
if !ok {
|
||||
return none, fmt.Errorf("expected pubkey addr, got %T",
|
||||
pubKeyAddr)
|
||||
}
|
||||
|
||||
return fn.Some(*pubKeyAddr.PubKey()), nil
|
||||
}
|
||||
|
||||
// addrWithInternalKey takes a delivery script, then attempts to supplement it
|
||||
// with information related to the internal key for the addr, but only if it's
|
||||
// a taproot addr.
|
||||
func (p *Brontide) addrWithInternalKey(
|
||||
deliveryScript []byte) fn.Result[chancloser.DeliveryAddrWithKey] {
|
||||
|
||||
// TODO(roasbeef): not compatible with external shutdown addr?
|
||||
// Currently, custom channels cannot be created with external upfront
|
||||
// shutdown addresses, so this shouldn't be an issue. We only require
|
||||
// the internal key for taproot addresses to be able to provide a non
|
||||
// inclusion proof of any scripts.
|
||||
|
||||
internalKey, err := internalKeyForAddr(p.cfg.Wallet, deliveryScript)
|
||||
if err != nil {
|
||||
return fn.Err[chancloser.DeliveryAddrWithKey](err)
|
||||
}
|
||||
|
||||
return fn.Ok(chancloser.DeliveryAddrWithKey{
|
||||
DeliveryAddress: deliveryScript,
|
||||
InternalKey: internalKey,
|
||||
})
|
||||
}
|
||||
|
||||
// loadActiveChannels creates indexes within the peer for tracking all active
|
||||
// channels returned by the database. It returns a slice of channel reestablish
|
||||
// messages that should be sent to the peer immediately, in case we have borked
|
||||
|
@ -1121,9 +1209,16 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) (
|
|||
return
|
||||
}
|
||||
|
||||
addr, err := p.addrWithInternalKey(
|
||||
info.DeliveryScript.Val,
|
||||
).Unpack()
|
||||
if err != nil {
|
||||
shutdownInfoErr = fmt.Errorf("unable to make "+
|
||||
"delivery addr: %w", err)
|
||||
return
|
||||
}
|
||||
chanCloser, err := p.createChanCloser(
|
||||
lnChan, info.DeliveryScript.Val, feePerKw, nil,
|
||||
info.Closer(),
|
||||
lnChan, addr, feePerKw, nil, info.Closer(),
|
||||
)
|
||||
if err != nil {
|
||||
shutdownInfoErr = fmt.Errorf("unable to "+
|
||||
|
@ -2882,8 +2977,12 @@ func (p *Brontide) fetchActiveChanCloser(chanID lnwire.ChannelID) (
|
|||
return nil, fmt.Errorf("unable to estimate fee")
|
||||
}
|
||||
|
||||
addr, err := p.addrWithInternalKey(deliveryScript).Unpack()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse addr: %w", err)
|
||||
}
|
||||
chanCloser, err = p.createChanCloser(
|
||||
channel, deliveryScript, feePerKw, nil, lntypes.Remote,
|
||||
channel, addr, feePerKw, nil, lntypes.Remote,
|
||||
)
|
||||
if err != nil {
|
||||
p.log.Errorf("unable to create chan closer: %v", err)
|
||||
|
@ -3125,8 +3224,12 @@ func (p *Brontide) restartCoopClose(lnChan *lnwallet.LightningChannel) (
|
|||
closingParty = lntypes.Local
|
||||
}
|
||||
|
||||
addr, err := p.addrWithInternalKey(deliveryScript).Unpack()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse addr: %w", err)
|
||||
}
|
||||
chanCloser, err := p.createChanCloser(
|
||||
lnChan, deliveryScript, feePerKw, nil, closingParty,
|
||||
lnChan, addr, feePerKw, nil, closingParty,
|
||||
)
|
||||
if err != nil {
|
||||
p.log.Errorf("unable to create chan closer: %v", err)
|
||||
|
@ -3153,8 +3256,8 @@ func (p *Brontide) restartCoopClose(lnChan *lnwallet.LightningChannel) (
|
|||
// createChanCloser constructs a ChanCloser from the passed parameters and is
|
||||
// used to de-duplicate code.
|
||||
func (p *Brontide) createChanCloser(channel *lnwallet.LightningChannel,
|
||||
deliveryScript lnwire.DeliveryAddress, fee chainfee.SatPerKWeight,
|
||||
req *htlcswitch.ChanClose,
|
||||
deliveryScript chancloser.DeliveryAddrWithKey,
|
||||
fee chainfee.SatPerKWeight, req *htlcswitch.ChanClose,
|
||||
closer lntypes.ChannelParty) (*chancloser.ChanCloser, error) {
|
||||
|
||||
_, startingHeight, err := p.cfg.ChainIO.GetBestBlock()
|
||||
|
@ -3175,6 +3278,7 @@ func (p *Brontide) createChanCloser(channel *lnwallet.LightningChannel,
|
|||
MusigSession: NewMusigChanCloser(channel),
|
||||
FeeEstimator: &chancloser.SimpleCoopFeeEstimator{},
|
||||
BroadcastTx: p.cfg.Wallet.PublishTransaction,
|
||||
AuxCloser: p.cfg.AuxChanCloser,
|
||||
DisableChannel: func(op wire.OutPoint) error {
|
||||
return p.cfg.ChanStatusMgr.RequestDisable(
|
||||
op, false,
|
||||
|
@ -3246,10 +3350,17 @@ func (p *Brontide) handleLocalCloseReq(req *htlcswitch.ChanClose) {
|
|||
return
|
||||
}
|
||||
}
|
||||
addr, err := p.addrWithInternalKey(deliveryScript).Unpack()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to parse addr for channel "+
|
||||
"%v: %w", req.ChanPoint, err)
|
||||
p.log.Errorf(err.Error())
|
||||
req.Err <- err
|
||||
|
||||
return
|
||||
}
|
||||
chanCloser, err := p.createChanCloser(
|
||||
channel, deliveryScript, req.TargetFeePerKw, req,
|
||||
lntypes.Local,
|
||||
channel, addr, req.TargetFeePerKw, req, lntypes.Local,
|
||||
)
|
||||
if err != nil {
|
||||
p.log.Errorf(err.Error())
|
||||
|
@ -3471,17 +3582,25 @@ func (p *Brontide) finalizeChanClosure(chanCloser *chancloser.ChanCloser) {
|
|||
}
|
||||
}
|
||||
|
||||
go WaitForChanToClose(chanCloser.NegotiationHeight(), notifier, errChan,
|
||||
localOut := chanCloser.LocalCloseOutput()
|
||||
remoteOut := chanCloser.RemoteCloseOutput()
|
||||
auxOut := chanCloser.AuxOutputs()
|
||||
go WaitForChanToClose(
|
||||
chanCloser.NegotiationHeight(), notifier, errChan,
|
||||
&chanPoint, &closingTxid, closingTx.TxOut[0].PkScript, func() {
|
||||
// Respond to the local subsystem which requested the
|
||||
// channel closure.
|
||||
if closeReq != nil {
|
||||
closeReq.Updates <- &ChannelCloseUpdate{
|
||||
ClosingTxid: closingTxid[:],
|
||||
Success: true,
|
||||
ClosingTxid: closingTxid[:],
|
||||
Success: true,
|
||||
LocalCloseOutput: localOut,
|
||||
RemoteCloseOutput: remoteOut,
|
||||
AuxOutputs: auxOut,
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// WaitForChanToClose uses the passed notifier to wait until the channel has
|
||||
|
|
163
rpcserver.go
163
rpcserver.go
|
@ -684,7 +684,8 @@ func newRPCServer(cfg *Config, interceptorChain *rpcperms.InterceptorChain,
|
|||
func (r *rpcServer) addDeps(s *server, macService *macaroons.Service,
|
||||
subServerCgs *subRPCServerConfigs, atpl *autopilot.Manager,
|
||||
invoiceRegistry *invoices.InvoiceRegistry, tower *watchtower.Standalone,
|
||||
chanPredicate chanacceptor.MultiplexAcceptor) error {
|
||||
chanPredicate chanacceptor.MultiplexAcceptor,
|
||||
invoiceHtlcModifier *invoices.HtlcModificationInterceptor) error {
|
||||
|
||||
// Set up router rpc backend.
|
||||
selfNode, err := s.graphDB.SourceNode()
|
||||
|
@ -787,7 +788,8 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service,
|
|||
s.sweeper, tower, s.towerClientMgr, r.cfg.net.ResolveTCPAddr,
|
||||
genInvoiceFeatures, genAmpInvoiceFeatures,
|
||||
s.getNodeAnnouncement, s.updateAndBrodcastSelfNode, parseAddr,
|
||||
rpcsLog, s.aliasMgr,
|
||||
rpcsLog, s.aliasMgr, r.implCfg.AuxDataParser,
|
||||
invoiceHtlcModifier,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2683,7 +2685,6 @@ func (r *rpcServer) CloseChannel(in *lnrpc.CloseChannelRequest,
|
|||
// transaction here rather than going to the switch as we don't require
|
||||
// interaction from the peer.
|
||||
if force {
|
||||
|
||||
// As we're force closing this channel, as a precaution, we'll
|
||||
// ensure that the switch doesn't continue to see this channel
|
||||
// as eligible for forwarding HTLC's. If the peer is online,
|
||||
|
@ -2723,15 +2724,19 @@ func (r *rpcServer) CloseChannel(in *lnrpc.CloseChannelRequest,
|
|||
|
||||
errChan = make(chan error, 1)
|
||||
notifier := r.server.cc.ChainNotifier
|
||||
go peer.WaitForChanToClose(uint32(bestHeight), notifier, errChan, chanPoint,
|
||||
go peer.WaitForChanToClose(
|
||||
uint32(bestHeight), notifier, errChan, chanPoint,
|
||||
&closingTxid, closingTx.TxOut[0].PkScript, func() {
|
||||
// Respond to the local subsystem which
|
||||
// requested the channel closure.
|
||||
updateChan <- &peer.ChannelCloseUpdate{
|
||||
ClosingTxid: closingTxid[:],
|
||||
Success: true,
|
||||
// Force closure transactions don't have
|
||||
// additional local/remote outputs.
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
} else {
|
||||
// If this is a frozen channel, then we only allow the co-op
|
||||
// close to proceed if we were the responder to this channel if
|
||||
|
@ -2855,6 +2860,19 @@ out:
|
|||
return err
|
||||
}
|
||||
|
||||
err = fn.MapOptionZ(
|
||||
r.server.implCfg.AuxDataParser,
|
||||
func(parser AuxDataParser) error {
|
||||
return parser.InlineParseCustomData(
|
||||
rpcClosingUpdate,
|
||||
)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing custom data: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
rpcsLog.Tracef("[closechannel] sending update: %v",
|
||||
rpcClosingUpdate)
|
||||
|
||||
|
@ -2880,19 +2898,84 @@ out:
|
|||
return nil
|
||||
}
|
||||
|
||||
func createRPCCloseUpdate(update interface{}) (
|
||||
*lnrpc.CloseStatusUpdate, error) {
|
||||
func createRPCCloseUpdate(
|
||||
update interface{}) (*lnrpc.CloseStatusUpdate, error) {
|
||||
|
||||
switch u := update.(type) {
|
||||
case *peer.ChannelCloseUpdate:
|
||||
ccu := &lnrpc.ChannelCloseUpdate{
|
||||
ClosingTxid: u.ClosingTxid,
|
||||
Success: u.Success,
|
||||
}
|
||||
|
||||
err := fn.MapOptionZ(
|
||||
u.LocalCloseOutput,
|
||||
func(closeOut chancloser.CloseOutput) error {
|
||||
cr, err := closeOut.ShutdownRecords.Serialize()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing "+
|
||||
"local close out custom "+
|
||||
"records: %w", err)
|
||||
}
|
||||
|
||||
rpcCloseOut := &lnrpc.CloseOutput{
|
||||
AmountSat: int64(closeOut.Amt),
|
||||
PkScript: closeOut.PkScript,
|
||||
IsLocal: true,
|
||||
CustomChannelData: cr,
|
||||
}
|
||||
ccu.LocalCloseOutput = rpcCloseOut
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = fn.MapOptionZ(
|
||||
u.RemoteCloseOutput,
|
||||
func(closeOut chancloser.CloseOutput) error {
|
||||
cr, err := closeOut.ShutdownRecords.Serialize()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing "+
|
||||
"remote close out custom "+
|
||||
"records: %w", err)
|
||||
}
|
||||
|
||||
rpcCloseOut := &lnrpc.CloseOutput{
|
||||
AmountSat: int64(closeOut.Amt),
|
||||
PkScript: closeOut.PkScript,
|
||||
CustomChannelData: cr,
|
||||
}
|
||||
ccu.RemoteCloseOutput = rpcCloseOut
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u.AuxOutputs.WhenSome(func(outs chancloser.AuxCloseOutputs) {
|
||||
for _, out := range outs.ExtraCloseOutputs {
|
||||
ccu.AdditionalOutputs = append(
|
||||
ccu.AdditionalOutputs,
|
||||
&lnrpc.CloseOutput{
|
||||
AmountSat: out.Value,
|
||||
PkScript: out.PkScript,
|
||||
IsLocal: out.IsLocal,
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return &lnrpc.CloseStatusUpdate{
|
||||
Update: &lnrpc.CloseStatusUpdate_ChanClose{
|
||||
ChanClose: &lnrpc.ChannelCloseUpdate{
|
||||
ClosingTxid: u.ClosingTxid,
|
||||
Success: u.Success,
|
||||
},
|
||||
ChanClose: ccu,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case *peer.PendingUpdate:
|
||||
return &lnrpc.CloseStatusUpdate{
|
||||
Update: &lnrpc.CloseStatusUpdate_ClosePending{
|
||||
|
@ -6123,6 +6206,19 @@ func (r *rpcServer) LookupInvoice(ctx context.Context,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Give the aux data parser a chance to format the custom data in the
|
||||
// invoice HTLCs.
|
||||
err = fn.MapOptionZ(
|
||||
r.server.implCfg.AuxDataParser,
|
||||
func(parser AuxDataParser) error {
|
||||
return parser.InlineParseCustomData(rpcInvoice)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing custom data: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
return rpcInvoice, nil
|
||||
}
|
||||
|
||||
|
@ -6178,6 +6274,21 @@ func (r *rpcServer) ListInvoices(ctx context.Context,
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Give the aux data parser a chance to format the custom data
|
||||
// in the invoice HTLCs.
|
||||
err = fn.MapOptionZ(
|
||||
r.server.implCfg.AuxDataParser,
|
||||
func(parser AuxDataParser) error {
|
||||
return parser.InlineParseCustomData(
|
||||
resp.Invoices[i],
|
||||
)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing custom data: %w",
|
||||
err)
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
@ -6206,6 +6317,21 @@ func (r *rpcServer) SubscribeInvoices(req *lnrpc.InvoiceSubscription,
|
|||
return err
|
||||
}
|
||||
|
||||
// Give the aux data parser a chance to format the
|
||||
// custom data in the invoice HTLCs.
|
||||
err = fn.MapOptionZ(
|
||||
r.server.implCfg.AuxDataParser,
|
||||
func(parser AuxDataParser) error {
|
||||
return parser.InlineParseCustomData(
|
||||
rpcInvoice,
|
||||
)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing custom data: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
if err := updateStream.Send(rpcInvoice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -6218,6 +6344,21 @@ func (r *rpcServer) SubscribeInvoices(req *lnrpc.InvoiceSubscription,
|
|||
return err
|
||||
}
|
||||
|
||||
// Give the aux data parser a chance to format the
|
||||
// custom data in the invoice HTLCs.
|
||||
err = fn.MapOptionZ(
|
||||
r.server.implCfg.AuxDataParser,
|
||||
func(parser AuxDataParser) error {
|
||||
return parser.InlineParseCustomData(
|
||||
rpcInvoice,
|
||||
)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing custom data: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
if err := updateStream.Send(rpcInvoice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
13
server.go
13
server.go
|
@ -263,6 +263,8 @@ type server struct {
|
|||
|
||||
invoices *invoices.InvoiceRegistry
|
||||
|
||||
invoiceHtlcModifier *invoices.HtlcModificationInterceptor
|
||||
|
||||
channelNotifier *channelnotifier.ChannelNotifier
|
||||
|
||||
peerNotifier *peernotifier.PeerNotifier
|
||||
|
@ -561,6 +563,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
invoiceHtlcModifier := invoices.NewHtlcModificationInterceptor()
|
||||
registryConfig := invoices.RegistryConfig{
|
||||
FinalCltvRejectDelta: lncfg.DefaultFinalCltvRejectDelta,
|
||||
HtlcHoldDuration: invoices.DefaultHtlcHoldDuration,
|
||||
|
@ -570,6 +573,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
|
|||
GcCanceledInvoicesOnStartup: cfg.GcCanceledInvoicesOnStartup,
|
||||
GcCanceledInvoicesOnTheFly: cfg.GcCanceledInvoicesOnTheFly,
|
||||
KeysendHoldTime: cfg.KeysendHoldTime,
|
||||
HtlcInterceptor: invoiceHtlcModifier,
|
||||
}
|
||||
|
||||
s := &server{
|
||||
|
@ -618,6 +622,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
|
|||
peerConnectedListeners: make(map[string][]chan<- lnpeer.Peer),
|
||||
peerDisconnectedListeners: make(map[string][]chan<- struct{}),
|
||||
|
||||
invoiceHtlcModifier: invoiceHtlcModifier,
|
||||
|
||||
customMessageServer: subscribe.NewServer(),
|
||||
|
||||
tlsManager: tlsManager,
|
||||
|
@ -2089,6 +2095,12 @@ func (s *server) Start() error {
|
|||
return
|
||||
}
|
||||
|
||||
cleanup = cleanup.add(s.invoiceHtlcModifier.Stop)
|
||||
if err := s.invoiceHtlcModifier.Start(); err != nil {
|
||||
startErr = err
|
||||
return
|
||||
}
|
||||
|
||||
cleanup = cleanup.add(s.chainArb.Stop)
|
||||
if err := s.chainArb.Start(); err != nil {
|
||||
startErr = err
|
||||
|
@ -4063,6 +4075,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
|
|||
AuxLeafStore: s.implCfg.AuxLeafStore,
|
||||
AuxSigner: s.implCfg.AuxSigner,
|
||||
MsgRouter: s.implCfg.MsgRouter,
|
||||
AuxChanCloser: s.implCfg.AuxChanCloser,
|
||||
}
|
||||
|
||||
copy(pCfg.PubKeyBytes[:], peerAddr.IdentityKey.SerializeCompressed())
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/lightningnetwork/lnd/autopilot"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
|
@ -32,6 +33,7 @@ import (
|
|||
"github.com/lightningnetwork/lnd/sweep"
|
||||
"github.com/lightningnetwork/lnd/watchtower"
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtclient"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// subRPCServerConfigs is special sub-config in the main configuration that
|
||||
|
@ -122,7 +124,9 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
|
|||
updateNodeAnnouncement func(features *lnwire.RawFeatureVector,
|
||||
modifiers ...netann.NodeAnnModifier) error,
|
||||
parseAddr func(addr string) (net.Addr, error),
|
||||
rpcLogger btclog.Logger, aliasMgr *aliasmgr.Manager) error {
|
||||
rpcLogger btclog.Logger, aliasMgr *aliasmgr.Manager,
|
||||
auxDataParser fn.Option[AuxDataParser],
|
||||
invoiceHtlcModifier *invoices.HtlcModificationInterceptor) error {
|
||||
|
||||
// First, we'll use reflect to obtain a version of the config struct
|
||||
// that allows us to programmatically inspect its fields.
|
||||
|
@ -238,6 +242,9 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
|
|||
subCfgValue.FieldByName("InvoiceRegistry").Set(
|
||||
reflect.ValueOf(invoiceRegistry),
|
||||
)
|
||||
subCfgValue.FieldByName("HtlcModifier").Set(
|
||||
reflect.ValueOf(invoiceHtlcModifier),
|
||||
)
|
||||
subCfgValue.FieldByName("IsChannelActive").Set(
|
||||
reflect.ValueOf(htlcSwitch.HasActiveLink),
|
||||
)
|
||||
|
@ -267,6 +274,20 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
|
|||
reflect.ValueOf(aliasMgr.GetPeerAlias),
|
||||
)
|
||||
|
||||
parseAuxData := func(m proto.Message) error {
|
||||
return fn.MapOptionZ(
|
||||
auxDataParser,
|
||||
func(p AuxDataParser) error {
|
||||
return p.InlineParseCustomData(
|
||||
m,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
subCfgValue.FieldByName("ParseAuxData").Set(
|
||||
reflect.ValueOf(parseAuxData),
|
||||
)
|
||||
|
||||
case *neutrinorpc.Config:
|
||||
subCfgValue := extractReflectValue(subCfg)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue