From c4f20dada4f997c9f48b6ed5b7da871dea37ef7c Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 31 Mar 2020 09:13:21 +0200 Subject: [PATCH] lntest: test PSBT channel funding --- lntest/itest/lnd_test.go | 4 + lntest/itest/psbt.go | 350 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 lntest/itest/psbt.go diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 61645517..f0ccc407 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -14875,6 +14875,10 @@ var testsCases = []*testCase{ name: "external channel funding", test: testExternalFundingChanPoint, }, + { + name: "psbt channel funding", + test: testPsbtChanFunding, + }, } // TestLightningNetworkDaemon performs a series of integration tests amongst a diff --git a/lntest/itest/psbt.go b/lntest/itest/psbt.go new file mode 100644 index 00000000..86f24624 --- /dev/null +++ b/lntest/itest/psbt.go @@ -0,0 +1,350 @@ +// +build rpctest + +package itest + +import ( + "bytes" + "context" + "crypto/rand" + "fmt" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/psbt" + "github.com/lightningnetwork/lnd" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lntest" +) + +// testPsbtChanFunding makes sure a channel can be opened between carol and dave +// by using a Partially Signed Bitcoin Transaction that funds the channel +// multisig funding output. +func testPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest) { + ctxb := context.Background() + const chanSize = lnd.MaxBtcFundingAmount + + // First, we'll create two new nodes that we'll use to open channel + // between for this test. + carol, err := net.NewNode("carol", nil) + if err != nil { + t.Fatalf("unable to start new node: %v", err) + } + defer shutdownAndAssert(net, t, carol) + + dave, err := net.NewNode("dave", nil) + if err != nil { + t.Fatalf("unable to start new node: %v", err) + } + defer shutdownAndAssert(net, t, dave) + + // Before we start the test, we'll ensure both sides are connected so + // the funding flow can be properly executed. + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + err = net.EnsureConnected(ctxt, carol, dave) + if err != nil { + t.Fatalf("unable to connect peers: %v", err) + } + + // At this point, we can begin our PSBT channel funding workflow. We'll + // start by generating a pending channel ID externally that will be used + // to track this new funding type. + var pendingChanID [32]byte + if _, err := rand.Read(pendingChanID[:]); err != nil { + t.Fatalf("unable to gen pending chan ID: %v", err) + } + + // Now that we have the pending channel ID, Carol will open the channel + // by specifying a PSBT shim. + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + chanUpdates, psbtBytes, err := openChannelPsbt( + ctxt, carol, dave, lntest.OpenChannelParams{ + Amt: chanSize, + FundingShim: &lnrpc.FundingShim{ + Shim: &lnrpc.FundingShim_PsbtShim{ + PsbtShim: &lnrpc.PsbtShim{ + PendingChanId: pendingChanID[:], + }, + }, + }, + }, + ) + if err != nil { + t.Fatalf("unable to open channel: %v", err) + } + packet, err := psbt.NewFromRawBytes(bytes.NewReader(psbtBytes), false) + if err != nil { + t.Fatalf("unable to parse returned PSBT: %v", err) + } + + // We'll now create a fully signed transaction that sends to the outputs + // encoded in the PSBT. We'll let the miner do it and convert the final + // TX into a PSBT, that's way easier than assembling a PSBT manually. + tx, err := net.Miner.CreateTransaction(packet.UnsignedTx.TxOut, 5, true) + if err != nil { + t.Fatalf("unable to create funding transaction: %v", err) + } + + // The helper function splits the final TX into the non-witness data + // encoded in a PSBT and the witness data returned separately. + unsignedPsbt, scripts, witnesses, err := createPsbtFromSignedTx(tx) + if err != nil { + t.Fatalf("unable to convert funding transaction into PSBT: %v", + err) + } + + // The PSBT will also be checked if there are large enough inputs + // present. We need to add some fake UTXO information to the PSBT to + // tell it what size of inputs we have. + for idx, txIn := range unsignedPsbt.UnsignedTx.TxIn { + utxPrevOut := txIn.PreviousOutPoint.Index + fakeUtxo := &wire.MsgTx{ + Version: 2, + TxIn: []*wire.TxIn{{}}, + TxOut: make([]*wire.TxOut, utxPrevOut+1), + } + for idx := range fakeUtxo.TxOut { + fakeUtxo.TxOut[idx] = &wire.TxOut{} + } + fakeUtxo.TxOut[utxPrevOut].Value = 10000000000 + unsignedPsbt.Inputs[idx].NonWitnessUtxo = fakeUtxo + } + + // Serialize the PSBT with the faked UTXO information. + var buf bytes.Buffer + err = unsignedPsbt.Serialize(&buf) + if err != nil { + t.Fatalf("error serializing PSBT: %v", err) + } + + // We have a PSBT that has no witness data yet, which is exactly what we + // need for the next step: Verify the PSBT with the funding intent. + _, err = carol.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{ + Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ + PsbtVerify: &lnrpc.FundingPsbtVerify{ + PendingChanId: pendingChanID[:], + FundedPsbt: buf.Bytes(), + }, + }, + }) + if err != nil { + t.Fatalf("error verifying PSBT with funding intent: %v", err) + } + + // Now we'll add the witness data back into the PSBT to make it a + // complete and signed transaction that can be finalized. We'll trick + // a bit by putting the script sig back directly, because we know we + // will only get non-witness outputs from the miner wallet. + for idx := range tx.TxIn { + if len(witnesses[idx]) > 0 { + t.Fatalf("unexpected witness inputs in wallet TX") + } + unsignedPsbt.Inputs[idx].FinalScriptSig = scripts[idx] + } + + // We've signed our PSBT now, let's pass it to the intent again. + buf.Reset() + err = unsignedPsbt.Serialize(&buf) + if err != nil { + t.Fatalf("error serializing PSBT: %v", err) + } + _, err = carol.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{ + Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{ + PsbtFinalize: &lnrpc.FundingPsbtFinalize{ + PendingChanId: pendingChanID[:], + SignedPsbt: buf.Bytes(), + }, + }, + }) + if err != nil { + t.Fatalf("error finalizing PSBT with funding intent: %v", err) + } + + // Consume the "channel pending" update. This waits until the funding + // transaction has been published. + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + updateResp, err := receiveChanUpdate(ctxt, chanUpdates) + if err != nil { + t.Fatalf("unable to consume channel update message: %v", err) + } + upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending) + if !ok { + t.Fatalf("expected PSBT funding update, instead got %v", + updateResp) + } + chanPoint := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ + FundingTxidBytes: upd.ChanPending.Txid, + }, + OutputIndex: upd.ChanPending.OutputIndex, + } + + // Great, now we can mine a block to get the transaction confirmed, then + // wait for the new channel to be propagated through the network. + txHash := tx.TxHash() + block := mineBlocks(t, net, 6, 1)[0] + assertTxInBlock(t, block, &txHash) + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint) + if err != nil { + t.Fatalf("carol didn't report channel: %v", err) + } + + // With the channel open, ensure that it is counted towards Carol's + // total channel balance. + balReq := &lnrpc.ChannelBalanceRequest{} + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + balRes, err := carol.ChannelBalance(ctxt, balReq) + if err != nil { + t.Fatalf("unable to get carol's balance: %v", err) + } + if balRes.Balance == 0 { + t.Fatalf("carol has an empty channel balance") + } + + // Next, to make sure the channel functions as normal, we'll make some + // payments within the channel. + payAmt := btcutil.Amount(100000) + invoice := &lnrpc.Invoice{ + Memo: "new chans", + Value: int64(payAmt), + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + resp, err := dave.AddInvoice(ctxt, invoice) + if err != nil { + t.Fatalf("unable to add invoice: %v", err) + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + err = completePaymentRequests( + ctxt, carol, []string{resp.PaymentRequest}, true, + ) + if err != nil { + t.Fatalf("unable to make payments between Carol and Dave") + } + + // To conclude, we'll close the newly created channel between Carol and + // Dave. This function will also block until the channel is closed and + // will additionally assert the relevant channel closing post + // conditions. + ctxt, cancel = context.WithTimeout(ctxb, channelCloseTimeout) + defer cancel() + closeChannelAndAssert(ctxt, t, net, carol, chanPoint, false) +} + +// openChannelPsbt attempts to open a channel between srcNode and destNode with +// the passed channel funding parameters. If the passed context has a timeout, +// then if the timeout is reached before the channel pending notification is +// received, an error is returned. An error is returned if the expected step +// of funding the PSBT is not received from the source node. +func openChannelPsbt(ctx context.Context, srcNode, destNode *lntest.HarnessNode, + p lntest.OpenChannelParams) (lnrpc.Lightning_OpenChannelClient, []byte, + error) { + + // Wait until srcNode and destNode have the latest chain synced. + // Otherwise, we may run into a check within the funding manager that + // prevents any funding workflows from being kicked off if the chain + // isn't yet synced. + if err := srcNode.WaitForBlockchainSync(ctx); err != nil { + return nil, nil, fmt.Errorf("unable to sync srcNode chain: %v", + err) + } + if err := destNode.WaitForBlockchainSync(ctx); err != nil { + return nil, nil, fmt.Errorf("unable to sync destNode chain: %v", + err) + } + + // Send the request to open a channel to the source node now. This will + // open a long-lived stream where we'll receive status updates about the + // progress of the channel. + respStream, err := srcNode.OpenChannel(ctx, &lnrpc.OpenChannelRequest{ + NodePubkey: destNode.PubKey[:], + LocalFundingAmount: int64(p.Amt), + PushSat: int64(p.PushAmt), + Private: p.Private, + SpendUnconfirmed: p.SpendUnconfirmed, + MinHtlcMsat: int64(p.MinHtlc), + FundingShim: p.FundingShim, + }) + if err != nil { + return nil, nil, fmt.Errorf("unable to open channel between "+ + "source and dest: %v", err) + } + + // Consume the "PSBT funding ready" update. This waits until the node + // notifies us that the PSBT can now be funded. + resp, err := receiveChanUpdate(ctx, respStream) + if err != nil { + return nil, nil, fmt.Errorf("unable to consume channel update "+ + "message: %v", err) + } + upd, ok := resp.Update.(*lnrpc.OpenStatusUpdate_PsbtFund) + if !ok { + return nil, nil, fmt.Errorf("expected PSBT funding update, "+ + "instead got %v", resp) + } + return respStream, upd.PsbtFund.Psbt, nil +} + +// receiveChanUpdate waits until a message is received on the stream or the +// context is canceled. The context must have a timeout or must be canceled +// in case no message is received, otherwise this function will block forever. +func receiveChanUpdate(ctx context.Context, + stream lnrpc.Lightning_OpenChannelClient) (*lnrpc.OpenStatusUpdate, + error) { + + chanMsg := make(chan *lnrpc.OpenStatusUpdate) + errChan := make(chan error) + go func() { + // Consume one message. This will block until the message is + // recieved. + resp, err := stream.Recv() + if err != nil { + errChan <- err + return + } + chanMsg <- resp + }() + + select { + case <-ctx.Done(): + return nil, fmt.Errorf("timeout reached before chan pending " + + "update sent") + + case err := <-errChan: + return nil, err + + case updateMsg := <-chanMsg: + return updateMsg, nil + } +} + +// createPsbtFromSignedTx is a utility function to create a PSBT from an +// already-signed transaction, so we can test reconstructing, signing and +// extracting it. Returned are: an unsigned transaction serialization, a list +// of scriptSigs, one per input, and a list of witnesses, one per input. +func createPsbtFromSignedTx(tx *wire.MsgTx) (*psbt.Packet, [][]byte, + []wire.TxWitness, error) { + + scriptSigs := make([][]byte, 0, len(tx.TxIn)) + witnesses := make([]wire.TxWitness, 0, len(tx.TxIn)) + tx2 := tx.Copy() + + // Blank out signature info in inputs + for i, tin := range tx2.TxIn { + tin.SignatureScript = nil + scriptSigs = append(scriptSigs, tx.TxIn[i].SignatureScript) + tin.Witness = nil + witnesses = append(witnesses, tx.TxIn[i].Witness) + } + + // Outputs always contain: (value, scriptPubkey) so don't need + // amending. Now tx2 is tx with all signing data stripped out + unsignedPsbt, err := psbt.NewFromUnsignedTx(tx2) + if err != nil { + return nil, nil, nil, err + } + return unsignedPsbt, scriptSigs, witnesses, nil +}