2021-08-24 11:21:51 +02:00
|
|
|
package funding
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"testing"
|
|
|
|
|
2022-02-23 14:48:00 +01:00
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
|
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
2021-08-24 11:21:51 +02:00
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
|
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
|
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
errFundingFailed = errors.New("funding failed")
|
|
|
|
|
|
|
|
testPubKey1Hex = "02e1ce77dfdda9fd1cf5e9d796faf57d1cedef9803aec84a6d7" +
|
|
|
|
"f8487d32781341e"
|
|
|
|
testPubKey1Bytes, _ = hex.DecodeString(testPubKey1Hex)
|
|
|
|
|
|
|
|
testPubKey2Hex = "039ddfc912035417b24aefe8da155267d71c3cf9e35405fc390" +
|
|
|
|
"df8357c5da7a5eb"
|
|
|
|
testPubKey2Bytes, _ = hex.DecodeString(testPubKey2Hex)
|
|
|
|
|
|
|
|
testOutPoint = wire.OutPoint{
|
|
|
|
Hash: [32]byte{1, 2, 3},
|
|
|
|
Index: 2,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
type fundingIntent struct {
|
|
|
|
chanIndex uint32
|
|
|
|
updateChan chan *lnrpc.OpenStatusUpdate
|
|
|
|
errChan chan error
|
|
|
|
}
|
|
|
|
|
|
|
|
type testHarness struct {
|
|
|
|
t *testing.T
|
|
|
|
batcher *Batcher
|
|
|
|
|
|
|
|
failUpdate1 bool
|
|
|
|
failUpdate2 bool
|
|
|
|
failPublish bool
|
|
|
|
|
|
|
|
intentsCreated map[[32]byte]*fundingIntent
|
|
|
|
intentsCanceled map[[32]byte]struct{}
|
|
|
|
abandonedChannels map[wire.OutPoint]struct{}
|
|
|
|
releasedUTXOs map[wire.OutPoint]struct{}
|
|
|
|
|
|
|
|
pendingPacket *psbt.Packet
|
|
|
|
pendingTx *wire.MsgTx
|
|
|
|
|
|
|
|
txPublished bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func newTestHarness(t *testing.T, failUpdate1, failUpdate2,
|
|
|
|
failPublish bool) *testHarness {
|
|
|
|
|
|
|
|
h := &testHarness{
|
|
|
|
t: t,
|
|
|
|
failUpdate1: failUpdate1,
|
|
|
|
failUpdate2: failUpdate2,
|
|
|
|
failPublish: failPublish,
|
|
|
|
intentsCreated: make(map[[32]byte]*fundingIntent),
|
|
|
|
intentsCanceled: make(map[[32]byte]struct{}),
|
|
|
|
abandonedChannels: make(map[wire.OutPoint]struct{}),
|
|
|
|
releasedUTXOs: make(map[wire.OutPoint]struct{}),
|
|
|
|
pendingTx: &wire.MsgTx{
|
|
|
|
Version: 2,
|
|
|
|
TxIn: []*wire.TxIn{{
|
|
|
|
// Our one input that pays for everything.
|
|
|
|
PreviousOutPoint: testOutPoint,
|
|
|
|
}},
|
|
|
|
TxOut: []*wire.TxOut{{
|
|
|
|
// Our static change output.
|
|
|
|
PkScript: []byte{1, 2, 3},
|
|
|
|
Value: 99,
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
h.batcher = NewBatcher(&BatchConfig{
|
|
|
|
RequestParser: h.parseRequest,
|
|
|
|
ChannelOpener: h.openChannel,
|
|
|
|
ChannelAbandoner: h.abandonChannel,
|
|
|
|
WalletKitServer: h,
|
|
|
|
Wallet: h,
|
|
|
|
Quit: make(chan struct{}),
|
|
|
|
})
|
|
|
|
return h
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *testHarness) parseRequest(
|
|
|
|
in *lnrpc.OpenChannelRequest) (*InitFundingMsg, error) {
|
|
|
|
|
2022-02-23 14:48:00 +01:00
|
|
|
pubKey, err := btcec.ParsePubKey(in.NodePubkey)
|
2021-08-24 11:21:51 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &InitFundingMsg{
|
|
|
|
TargetPubkey: pubKey,
|
|
|
|
LocalFundingAmt: btcutil.Amount(in.LocalFundingAmount),
|
|
|
|
PushAmt: lnwire.NewMSatFromSatoshis(
|
|
|
|
btcutil.Amount(in.PushSat),
|
|
|
|
),
|
|
|
|
FundingFeePerKw: chainfee.SatPerKVByte(
|
|
|
|
in.SatPerVbyte * 1000,
|
|
|
|
).FeePerKWeight(),
|
|
|
|
Private: in.Private,
|
|
|
|
RemoteCsvDelay: uint16(in.RemoteCsvDelay),
|
|
|
|
MinConfs: in.MinConfs,
|
|
|
|
MaxLocalCsv: uint16(in.MaxLocalCsv),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *testHarness) openChannel(
|
|
|
|
req *InitFundingMsg) (chan *lnrpc.OpenStatusUpdate, chan error) {
|
|
|
|
|
|
|
|
updateChan := make(chan *lnrpc.OpenStatusUpdate, 2)
|
|
|
|
errChan := make(chan error, 1)
|
|
|
|
|
|
|
|
// The change output is always index 0.
|
|
|
|
chanIndex := uint32(len(h.intentsCreated) + 1)
|
|
|
|
|
|
|
|
h.intentsCreated[req.PendingChanID] = &fundingIntent{
|
|
|
|
chanIndex: chanIndex,
|
|
|
|
updateChan: updateChan,
|
|
|
|
errChan: errChan,
|
|
|
|
}
|
|
|
|
h.pendingTx.TxOut = append(h.pendingTx.TxOut, &wire.TxOut{
|
|
|
|
PkScript: []byte{1, 2, 3, byte(chanIndex)},
|
|
|
|
Value: int64(req.LocalFundingAmt),
|
|
|
|
})
|
|
|
|
|
|
|
|
if h.failUpdate1 {
|
|
|
|
errChan <- errFundingFailed
|
|
|
|
|
|
|
|
// Once we fail we don't send any more updates.
|
|
|
|
return updateChan, errChan
|
|
|
|
}
|
|
|
|
|
|
|
|
updateChan <- &lnrpc.OpenStatusUpdate{
|
|
|
|
PendingChanId: req.PendingChanID[:],
|
|
|
|
Update: &lnrpc.OpenStatusUpdate_PsbtFund{
|
|
|
|
PsbtFund: &lnrpc.ReadyForPsbtFunding{
|
|
|
|
FundingAmount: int64(
|
|
|
|
req.LocalFundingAmt,
|
|
|
|
),
|
|
|
|
FundingAddress: fmt.Sprintf("foo%d", chanIndex),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
return updateChan, errChan
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *testHarness) abandonChannel(op *wire.OutPoint) error {
|
|
|
|
h.abandonedChannels[*op] = struct{}{}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *testHarness) FundPsbt(context.Context,
|
|
|
|
*walletrpc.FundPsbtRequest) (*walletrpc.FundPsbtResponse, error) {
|
|
|
|
|
|
|
|
packet, err := psbt.NewFromUnsignedTx(h.pendingTx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
h.pendingPacket = packet
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
if err := packet.Serialize(&buf); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &walletrpc.FundPsbtResponse{
|
|
|
|
FundedPsbt: buf.Bytes(),
|
|
|
|
LockedUtxos: []*walletrpc.UtxoLease{{
|
|
|
|
Id: []byte{1, 2, 3},
|
|
|
|
Outpoint: &lnrpc.OutPoint{
|
|
|
|
TxidBytes: testOutPoint.Hash[:],
|
|
|
|
OutputIndex: testOutPoint.Index,
|
|
|
|
},
|
|
|
|
}},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *testHarness) FinalizePsbt(context.Context,
|
|
|
|
*walletrpc.FinalizePsbtRequest) (*walletrpc.FinalizePsbtResponse,
|
|
|
|
error) {
|
|
|
|
|
|
|
|
var psbtBuf bytes.Buffer
|
|
|
|
if err := h.pendingPacket.Serialize(&psbtBuf); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var txBuf bytes.Buffer
|
|
|
|
if err := h.pendingTx.Serialize(&txBuf); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &walletrpc.FinalizePsbtResponse{
|
|
|
|
SignedPsbt: psbtBuf.Bytes(),
|
|
|
|
RawFinalTx: txBuf.Bytes(),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *testHarness) ReleaseOutput(_ context.Context,
|
|
|
|
r *walletrpc.ReleaseOutputRequest) (*walletrpc.ReleaseOutputResponse,
|
|
|
|
error) {
|
|
|
|
|
|
|
|
hash, err := chainhash.NewHash(r.Outpoint.TxidBytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
op := wire.OutPoint{
|
|
|
|
Hash: *hash,
|
|
|
|
Index: r.Outpoint.OutputIndex,
|
|
|
|
}
|
|
|
|
|
|
|
|
h.releasedUTXOs[op] = struct{}{}
|
|
|
|
|
|
|
|
return &walletrpc.ReleaseOutputResponse{}, nil
|
|
|
|
}
|
|
|
|
|
2021-06-07 11:16:38 +02:00
|
|
|
func (h *testHarness) PsbtFundingVerify([32]byte, *psbt.Packet, bool) error {
|
2021-08-24 11:21:51 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *testHarness) PsbtFundingFinalize(pid [32]byte, _ *psbt.Packet,
|
|
|
|
_ *wire.MsgTx) error {
|
|
|
|
|
|
|
|
// During the finalize phase we can now prepare the next update to send.
|
|
|
|
// For this we first need to find the intent that has the channels we
|
|
|
|
// need to send on.
|
|
|
|
intent, ok := h.intentsCreated[pid]
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("intent %x not found", pid)
|
|
|
|
}
|
|
|
|
|
|
|
|
// We should now also have the final TX, let's get its hash.
|
|
|
|
hash := h.pendingTx.TxHash()
|
|
|
|
|
|
|
|
// For the second update we fail on the second channel only so the first
|
|
|
|
// is actually pending.
|
|
|
|
if h.failUpdate2 && intent.chanIndex == 2 {
|
|
|
|
intent.errChan <- errFundingFailed
|
|
|
|
} else {
|
|
|
|
intent.updateChan <- &lnrpc.OpenStatusUpdate{
|
|
|
|
PendingChanId: pid[:],
|
|
|
|
Update: &lnrpc.OpenStatusUpdate_ChanPending{
|
|
|
|
ChanPending: &lnrpc.PendingUpdate{
|
|
|
|
Txid: hash[:],
|
|
|
|
OutputIndex: intent.chanIndex,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *testHarness) PublishTransaction(*wire.MsgTx, string) error {
|
|
|
|
if h.failPublish {
|
|
|
|
return errFundingFailed
|
|
|
|
}
|
|
|
|
|
|
|
|
h.txPublished = true
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *testHarness) CancelFundingIntent(pid [32]byte) error {
|
|
|
|
h.intentsCanceled[pid] = struct{}{}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// TestBatchFund tests different success and error scenarios of the atomic batch
|
|
|
|
// channel funding.
|
|
|
|
func TestBatchFund(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
testCases := []struct {
|
|
|
|
name string
|
|
|
|
failUpdate1 bool
|
|
|
|
failUpdate2 bool
|
|
|
|
failPublish bool
|
|
|
|
channels []*lnrpc.BatchOpenChannel
|
|
|
|
expectedErr string
|
|
|
|
}{{
|
|
|
|
name: "happy path",
|
|
|
|
channels: []*lnrpc.BatchOpenChannel{{
|
|
|
|
NodePubkey: testPubKey1Bytes,
|
|
|
|
LocalFundingAmount: 1234,
|
|
|
|
}, {
|
|
|
|
NodePubkey: testPubKey2Bytes,
|
|
|
|
LocalFundingAmount: 4321,
|
|
|
|
}},
|
|
|
|
}, {
|
|
|
|
name: "initial negotiation failure",
|
|
|
|
failUpdate1: true,
|
|
|
|
channels: []*lnrpc.BatchOpenChannel{{
|
|
|
|
NodePubkey: testPubKey1Bytes,
|
|
|
|
LocalFundingAmount: 1234,
|
|
|
|
}, {
|
|
|
|
NodePubkey: testPubKey2Bytes,
|
|
|
|
LocalFundingAmount: 4321,
|
|
|
|
}},
|
|
|
|
expectedErr: "initial negotiation failed",
|
|
|
|
}, {
|
|
|
|
name: "final negotiation failure",
|
|
|
|
failUpdate2: true,
|
|
|
|
channels: []*lnrpc.BatchOpenChannel{{
|
|
|
|
NodePubkey: testPubKey1Bytes,
|
|
|
|
LocalFundingAmount: 1234,
|
|
|
|
}, {
|
|
|
|
NodePubkey: testPubKey2Bytes,
|
|
|
|
LocalFundingAmount: 4321,
|
|
|
|
}},
|
|
|
|
expectedErr: "final negotiation failed",
|
|
|
|
}, {
|
|
|
|
name: "publish failure",
|
|
|
|
failPublish: true,
|
|
|
|
channels: []*lnrpc.BatchOpenChannel{{
|
|
|
|
NodePubkey: testPubKey1Bytes,
|
|
|
|
LocalFundingAmount: 1234,
|
|
|
|
}, {
|
|
|
|
NodePubkey: testPubKey2Bytes,
|
|
|
|
LocalFundingAmount: 4321,
|
|
|
|
}},
|
|
|
|
expectedErr: "error publishing final batch transaction",
|
|
|
|
}}
|
|
|
|
|
|
|
|
for _, tc := range testCases {
|
|
|
|
tc := tc
|
|
|
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
h := newTestHarness(
|
|
|
|
t, tc.failUpdate1, tc.failUpdate2,
|
|
|
|
tc.failPublish,
|
|
|
|
)
|
|
|
|
|
|
|
|
req := &lnrpc.BatchOpenChannelRequest{
|
|
|
|
Channels: tc.channels,
|
|
|
|
SatPerVbyte: 5,
|
|
|
|
MinConfs: 1,
|
|
|
|
}
|
|
|
|
updates, err := h.batcher.BatchFund(
|
|
|
|
context.Background(), req,
|
|
|
|
)
|
|
|
|
|
|
|
|
if tc.failUpdate1 || tc.failUpdate2 || tc.failPublish {
|
|
|
|
require.Error(t, err)
|
|
|
|
require.Contains(t, err.Error(), tc.expectedErr)
|
|
|
|
} else {
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, updates, len(tc.channels))
|
|
|
|
}
|
|
|
|
|
|
|
|
if tc.failUpdate1 {
|
|
|
|
require.Len(t, h.releasedUTXOs, 0)
|
|
|
|
require.Len(t, h.intentsCreated, 2)
|
|
|
|
for pid := range h.intentsCreated {
|
|
|
|
require.Contains(
|
|
|
|
t, h.intentsCanceled, pid,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
hash := h.pendingTx.TxHash()
|
|
|
|
if tc.failUpdate2 {
|
|
|
|
require.Len(t, h.releasedUTXOs, 1)
|
|
|
|
require.Len(t, h.intentsCreated, 2)
|
|
|
|
|
|
|
|
// If we fail on update 2 we do so on the second
|
|
|
|
// channel so one will be pending and one not
|
|
|
|
// yet.
|
|
|
|
require.Len(t, h.intentsCanceled, 1)
|
|
|
|
require.Len(t, h.abandonedChannels, 1)
|
|
|
|
require.Contains(
|
|
|
|
t, h.abandonedChannels, wire.OutPoint{
|
|
|
|
Hash: hash,
|
|
|
|
Index: 1,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if tc.failPublish {
|
|
|
|
require.Len(t, h.releasedUTXOs, 1)
|
|
|
|
require.Len(t, h.intentsCreated, 2)
|
|
|
|
|
|
|
|
require.Len(t, h.intentsCanceled, 0)
|
|
|
|
require.Len(t, h.abandonedChannels, 2)
|
|
|
|
require.Contains(
|
|
|
|
t, h.abandonedChannels, wire.OutPoint{
|
|
|
|
Hash: hash,
|
|
|
|
Index: 1,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
require.Contains(
|
|
|
|
t, h.abandonedChannels, wire.OutPoint{
|
|
|
|
Hash: hash,
|
|
|
|
Index: 2,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|