Merge pull request #4601 from guggero/psbt-final-tx-raw

PSBT funding: allow final TX to be specified as raw wire format transaction
This commit is contained in:
Oliver Gugger 2020-09-15 10:07:52 +02:00 committed by GitHub
commit 904055cf4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 996 additions and 815 deletions

View file

@ -1,6 +1,7 @@
package main
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
@ -11,6 +12,7 @@ import (
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
@ -43,9 +45,9 @@ Base64 encoded PSBT: `
userMsgSign = `
PSBT verified by lnd, please continue the funding flow by signing the PSBT by
all required parties/devices. Once the transaction is fully signed, paste it
again here.
again here either in base64 PSBT or hex encoded raw wire TX format.
Base64 encoded signed PSBT: `
Signed base64 encoded PSBT or hex encoded raw wire TX: `
)
// TODO(roasbeef): change default number of confirmations
@ -505,7 +507,7 @@ func openChannelPsbt(ctx *cli.Context, client lnrpc.LightningClient,
return fmt.Errorf("reading from console "+
"failed: %v", err)
}
psbt, err := base64.StdEncoding.DecodeString(
fundedPsbt, err := base64.StdEncoding.DecodeString(
strings.TrimSpace(psbtBase64),
)
if err != nil {
@ -515,7 +517,7 @@ func openChannelPsbt(ctx *cli.Context, client lnrpc.LightningClient,
verifyMsg := &lnrpc.FundingTransitionMsg{
Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{
PsbtVerify: &lnrpc.FundingPsbtVerify{
FundedPsbt: psbt,
FundedPsbt: fundedPsbt,
PendingChanId: pendingChanID[:],
},
},
@ -531,7 +533,7 @@ func openChannelPsbt(ctx *cli.Context, client lnrpc.LightningClient,
fmt.Print(userMsgSign)
// Read the signed PSBT and send it to lnd.
psbtBase64, err = readLine(quit)
finalTxStr, err := readLine(quit)
if err == io.EOF {
return nil
}
@ -539,22 +541,16 @@ func openChannelPsbt(ctx *cli.Context, client lnrpc.LightningClient,
return fmt.Errorf("reading from console "+
"failed: %v", err)
}
psbt, err = base64.StdEncoding.DecodeString(
strings.TrimSpace(psbtBase64),
finalizeMsg, err := finalizeMsgFromString(
finalTxStr, pendingChanID[:],
)
if err != nil {
return fmt.Errorf("base64 decode failed: %v",
err)
return err
}
finalizeMsg := &lnrpc.FundingTransitionMsg{
Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{
PsbtFinalize: &lnrpc.FundingPsbtFinalize{
SignedPsbt: psbt,
PendingChanId: pendingChanID[:],
},
},
transitionMsg := &lnrpc.FundingTransitionMsg{
Trigger: finalizeMsg,
}
err = sendFundingState(ctxc, ctx, finalizeMsg)
err = sendFundingState(ctxc, ctx, transitionMsg)
if err != nil {
return fmt.Errorf("finalizing PSBT funding "+
"flow failed: %v", err)
@ -686,3 +682,41 @@ func sendFundingState(cancelCtx context.Context, cliCtx *cli.Context,
_, err := client.FundingStateStep(cancelCtx, msg)
return err
}
// finalizeMsgFromString creates the final message for the PsbtFinalize step
// from either a hex encoded raw wire transaction or a base64 encoded PSBT
// packet.
func finalizeMsgFromString(tx string,
pendingChanID []byte) (*lnrpc.FundingTransitionMsg_PsbtFinalize, error) {
rawTx, err := hex.DecodeString(strings.TrimSpace(tx))
if err == nil {
// Hex decoding succeeded so we assume we have a raw wire format
// transaction. Let's submit that instead of a PSBT packet.
tx := &wire.MsgTx{}
err := tx.Deserialize(bytes.NewReader(rawTx))
if err != nil {
return nil, fmt.Errorf("deserializing as raw wire "+
"transaction failed: %v", err)
}
return &lnrpc.FundingTransitionMsg_PsbtFinalize{
PsbtFinalize: &lnrpc.FundingPsbtFinalize{
FinalRawTx: rawTx,
PendingChanId: pendingChanID,
},
}, nil
}
// If the string isn't a hex encoded transaction, we assume it must be
// a base64 encoded PSBT packet.
psbtBytes, err := base64.StdEncoding.DecodeString(strings.TrimSpace(tx))
if err != nil {
return nil, fmt.Errorf("base64 decode failed: %v", err)
}
return &lnrpc.FundingTransitionMsg_PsbtFinalize{
PsbtFinalize: &lnrpc.FundingPsbtFinalize{
SignedPsbt: psbtBytes,
PendingChanId: pendingChanID,
},
}, nil
}

4
go.mod
View file

@ -8,8 +8,8 @@ require (
github.com/btcsuite/btcd v0.20.1-beta.0.20200730232343-1db1b6f8217f
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/btcsuite/btcutil v1.0.2
github.com/btcsuite/btcutil/psbt v1.0.2
github.com/btcsuite/btcwallet v0.11.1-0.20200814001439-1d31f4ea6fc5
github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0
github.com/btcsuite/btcwallet v0.11.1-0.20200904022754-2c5947a45222
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0
github.com/btcsuite/btcwallet/wallet/txrules v1.0.0
github.com/btcsuite/btcwallet/walletdb v1.3.3

8
go.sum
View file

@ -35,10 +35,10 @@ github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts=
github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
github.com/btcsuite/btcutil/psbt v1.0.2 h1:gCVY3KxdoEVU7Q6TjusPO+GANIwVgr9yTLqM+a6CZr8=
github.com/btcsuite/btcutil/psbt v1.0.2/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ=
github.com/btcsuite/btcwallet v0.11.1-0.20200814001439-1d31f4ea6fc5 h1:1We7EuizBnX/17Q6O2dkeToyehxzUHo62Wv1c0ncr7c=
github.com/btcsuite/btcwallet v0.11.1-0.20200814001439-1d31f4ea6fc5/go.mod h1:YkEbJaCyN6yncq5gEp2xG0OKDwus2QxGCEXTNF27w5I=
github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0 h1:3Zumkyl6PWyHuVJ04me0xeD9CnPOhNgeGpapFbzy7O4=
github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ=
github.com/btcsuite/btcwallet v0.11.1-0.20200904022754-2c5947a45222 h1:rh1FQAhh+BeR29twIFDM0RLOFpDK62tsABtUkWctTXw=
github.com/btcsuite/btcwallet v0.11.1-0.20200904022754-2c5947a45222/go.mod h1:owv9oZqM0HnUW+ByF7VqOgfs2eb0ooiePW/+Tl/i/Nk=
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 h1:KGHMW5sd7yDdDMkCZ/JpP0KltolFsQcB973brBnfj4c=
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU=
github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 h1:2VsfS0sBedcM5KmDzRMT3+b6xobqWveZGvjb+jFez5w=

File diff suppressed because it is too large Load diff

View file

@ -1859,12 +1859,19 @@ message FundingPsbtFinalize {
/*
The funded PSBT that contains all witness data to send the exact channel
capacity amount to the PK script returned in the open channel message in a
previous step.
previous step. Cannot be set at the same time as final_raw_tx.
*/
bytes signed_psbt = 1;
// The pending channel ID of the channel to get the PSBT for.
bytes pending_chan_id = 2;
/*
As an alternative to the signed PSBT with all witness data, the final raw
wire format transaction can also be specified directly. Cannot be set at the
same time as signed_psbt.
*/
bytes final_raw_tx = 3;
}
message FundingTransitionMsg {

View file

@ -3537,12 +3537,17 @@
"signed_psbt": {
"type": "string",
"format": "byte",
"description": "The funded PSBT that contains all witness data to send the exact channel\ncapacity amount to the PK script returned in the open channel message in a\nprevious step."
"description": "The funded PSBT that contains all witness data to send the exact channel\ncapacity amount to the PK script returned in the open channel message in a\nprevious step. Cannot be set at the same time as final_raw_tx."
},
"pending_chan_id": {
"type": "string",
"format": "byte",
"description": "The pending channel ID of the channel to get the PSBT for."
},
"final_raw_tx": {
"type": "string",
"format": "byte",
"description": "As an alternative to the signed PSBT with all witness data, the final raw\nwire format transaction can also be specified directly. Cannot be set at the\nsame time as signed_psbt."
}
}
},

View file

@ -14,6 +14,7 @@ import (
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/stretchr/testify/require"
)
// testPsbtChanFunding makes sure a channel can be opened between carol and dave
@ -119,14 +120,14 @@ func testPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest) {
// 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.
allOuts := append(packet.UnsignedTx.TxOut, packet2.UnsignedTx.TxOut...)
tx, err := net.Miner.CreateTransaction(allOuts, 5, true)
finalTx, err := net.Miner.CreateTransaction(allOuts, 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)
unsignedPsbt, scripts, witnesses, err := createPsbtFromSignedTx(finalTx)
if err != nil {
t.Fatalf("unable to convert funding transaction into PSBT: %v",
err)
@ -185,7 +186,7 @@ func testPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest) {
// 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 {
for idx := range finalTx.TxIn {
if len(witnesses[idx]) > 0 {
t.Fatalf("unexpected witness inputs in wallet TX")
}
@ -239,12 +240,16 @@ func testPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest) {
t.Fatalf("unexpected txes in mempool: %v", mempool)
}
// Let's progress the second channel now.
// Let's progress the second channel now. This time we'll use the raw
// wire format transaction directly.
buf.Reset()
err = finalTx.Serialize(&buf)
require.NoError(t.t, err)
_, err = carol.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{
Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{
PsbtFinalize: &lnrpc.FundingPsbtFinalize{
PendingChanId: pendingChanID2[:],
SignedPsbt: buf.Bytes(),
FinalRawTx: buf.Bytes(),
},
},
})
@ -275,7 +280,7 @@ func testPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest) {
// 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()
txHash := finalTx.TxHash()
block := mineBlocks(t, net, 6, 1)[0]
assertTxInBlock(t, block, &txHash)
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)

View file

@ -125,6 +125,11 @@ type PsbtIntent struct {
// Only witness data should be added after the verification process.
PendingPsbt *psbt.Packet
// FinalTX is the final, signed and ready to be published wire format
// transaction. This is only set after the PsbtFinalize step was
// completed successfully.
FinalTX *wire.MsgTx
// PsbtReady is an error channel the funding manager will listen for
// a signal about the PSBT being ready to continue the funding flow. In
// the normal, happy flow, this channel is only ever closed. If a
@ -270,12 +275,29 @@ func (i *PsbtIntent) Finalize(packet *psbt.Packet) error {
if err != nil {
return fmt.Errorf("error finalizing PSBT: %v", err)
}
_, err = psbt.Extract(packet)
rawTx, err := psbt.Extract(packet)
if err != nil {
return fmt.Errorf("unable to extract funding TX: %v", err)
}
// Do a basic check that this is still the same PSBT that we verified in
return i.FinalizeRawTX(rawTx)
}
// FinalizeRawTX makes sure the final raw transaction that is given to the
// intent is fully valid and signed but still contains the same UTXOs and
// outputs as the pending transaction we previously verified. If everything
// checks out, the funding manager is informed that the channel can now be
// opened and the funding transaction be broadcast.
func (i *PsbtIntent) FinalizeRawTX(rawTx *wire.MsgTx) error {
if rawTx == nil {
return fmt.Errorf("raw transaction is nil")
}
if i.State != PsbtVerified {
return fmt.Errorf("invalid state. got %v expected %v", i.State,
PsbtVerified)
}
// Do a basic check that this is still the same TX that we verified in
// the previous step. This is to protect the user from unwanted
// modifications. We only check the outputs and previous outpoints of
// the inputs of the wire transaction because the fields in the PSBT
@ -283,23 +305,29 @@ func (i *PsbtIntent) Finalize(packet *psbt.Packet) error {
if i.PendingPsbt == nil {
return fmt.Errorf("PSBT was not verified first")
}
err = verifyOutputsEqual(
packet.UnsignedTx.TxOut, i.PendingPsbt.UnsignedTx.TxOut,
)
err := verifyOutputsEqual(rawTx.TxOut, i.PendingPsbt.UnsignedTx.TxOut)
if err != nil {
return fmt.Errorf("outputs differ from verified PSBT: %v", err)
}
err = verifyInputPrevOutpointsEqual(
packet.UnsignedTx.TxIn, i.PendingPsbt.UnsignedTx.TxIn,
rawTx.TxIn, i.PendingPsbt.UnsignedTx.TxIn,
)
if err != nil {
return fmt.Errorf("inputs differ from verified PSBT: %v", err)
}
// As far as we can tell, this PSBT is ok to be used as a funding
// We also check that we have a signed TX. This is only necessary if the
// FinalizeRawTX is called directly with a wire format TX instead of
// extracting the TX from a PSBT.
err = verifyInputsSigned(rawTx.TxIn)
if err != nil {
return fmt.Errorf("inputs not signed: %v", err)
}
// As far as we can tell, this TX is ok to be used as a funding
// transaction.
i.PendingPsbt = packet
i.State = PsbtFinalized
i.FinalTX = rawTx
// Signal the funding manager that it can now finally continue with its
// funding flow as the PSBT is now ready to be converted into a real
@ -319,32 +347,22 @@ func (i *PsbtIntent) CompileFundingTx() (*wire.MsgTx, error) {
i.State, PsbtFinalized)
}
// Make sure the PSBT can be finalized and extracted.
err := psbt.MaybeFinalizeAll(i.PendingPsbt)
if err != nil {
return nil, fmt.Errorf("error finalizing PSBT: %v", err)
}
fundingTx, err := psbt.Extract(i.PendingPsbt)
if err != nil {
return nil, fmt.Errorf("unable to extract funding TX: %v", err)
}
// Identify our funding outpoint now that we know everything's ready.
_, txOut, err := i.FundingOutput()
if err != nil {
return nil, fmt.Errorf("cannot get funding output: %v", err)
}
ok, idx := input.FindScriptOutputIndex(fundingTx, txOut.PkScript)
ok, idx := input.FindScriptOutputIndex(i.FinalTX, txOut.PkScript)
if !ok {
return nil, fmt.Errorf("funding output not found in PSBT")
}
i.chanPoint = &wire.OutPoint{
Hash: fundingTx.TxHash(),
Hash: i.FinalTX.TxHash(),
Index: idx,
}
i.State = PsbtFundingTxCompiled
return fundingTx, nil
return i.FinalTX, nil
}
// RemoteCanceled informs the listener of the PSBT ready channel that the
@ -529,3 +547,18 @@ func verifyInputPrevOutpointsEqual(ins1, ins2 []*wire.TxIn) error {
}
return nil
}
// verifyInputsSigned verifies that the given list of inputs is non-empty and
// that all the inputs either contain a script signature or a witness stack.
func verifyInputsSigned(ins []*wire.TxIn) error {
if len(ins) == 0 {
return fmt.Errorf("no inputs in transaction")
}
for idx, in := range ins {
if len(in.SignatureScript) == 0 && len(in.Witness) == 0 {
return fmt.Errorf("input %d has no signature data "+
"attached", idx)
}
}
return nil
}

View file

@ -18,6 +18,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/stretchr/testify/require"
)
var (
@ -496,6 +497,44 @@ func TestPsbtFinalize(t *testing.T) {
return i.Finalize(p)
},
},
{
name: "raw tx - nil transaction",
expectedErr: "raw transaction is nil",
doFinalize: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
return i.FinalizeRawTX(nil)
},
},
{
name: "raw tx - no witness data in raw tx",
expectedErr: "inputs not signed: input 0 has no " +
"signature data attached",
doFinalize: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
rawTx, err := psbt.Extract(p)
require.NoError(t, err)
rawTx.TxIn[0].Witness = nil
return i.FinalizeRawTX(rawTx)
},
},
{
name: "happy path",
expectedErr: "",
doFinalize: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
err := i.Finalize(p)
require.NoError(t, err)
require.Equal(t, PsbtFinalized, i.State)
require.NotNil(t, i.FinalTX)
return nil
},
},
}
// Create a simple assembler and ask it to provision a channel to get

View file

@ -536,8 +536,8 @@ func (l *LightningWallet) PsbtFundingVerify(pid [32]byte,
// PsbtFundingFinalize looks up a previously registered funding intent by its
// pending channel ID and tries to advance the state machine by finalizing the
// passed PSBT.
func (l *LightningWallet) PsbtFundingFinalize(pid [32]byte,
packet *psbt.Packet) error {
func (l *LightningWallet) PsbtFundingFinalize(pid [32]byte, packet *psbt.Packet,
rawTx *wire.MsgTx) error {
l.intentMtx.Lock()
defer l.intentMtx.Unlock()
@ -551,9 +551,23 @@ func (l *LightningWallet) PsbtFundingFinalize(pid [32]byte,
if !ok {
return fmt.Errorf("incompatible funding intent")
}
err := psbtIntent.Finalize(packet)
if err != nil {
return fmt.Errorf("error finalizing PSBT: %v", err)
// Either the PSBT or the raw TX must be set.
switch {
case packet != nil && rawTx == nil:
err := psbtIntent.Finalize(packet)
if err != nil {
return fmt.Errorf("error finalizing PSBT: %v", err)
}
case rawTx != nil && packet == nil:
err := psbtIntent.FinalizeRawTX(rawTx)
if err != nil {
return fmt.Errorf("error finalizing raw TX: %v", err)
}
default:
return fmt.Errorf("either a PSBT or raw TX must be specified")
}
return nil

View file

@ -6738,19 +6738,50 @@ func (r *rpcServer) FundingStateStep(ctx context.Context,
// the final PSBT to the previously verified one and if nothing
// unexpected was changed, continue the channel opening process.
case in.GetPsbtFinalize() != nil:
msg := in.GetPsbtFinalize()
rpcsLog.Debugf("Finalizing PSBT for pending_id=%x",
in.GetPsbtFinalize().PendingChanId)
msg.PendingChanId)
copy(pendingChanID[:], in.GetPsbtFinalize().PendingChanId)
packet, err := psbt.NewFromRawBytes(
bytes.NewReader(in.GetPsbtFinalize().SignedPsbt), false,
var (
packet *psbt.Packet
rawTx *wire.MsgTx
err error
)
if err != nil {
return nil, fmt.Errorf("error parsing psbt: %v", err)
// Either the signed PSBT or the raw transaction need to be set
// but not both at the same time.
switch {
case len(msg.SignedPsbt) > 0 && len(msg.FinalRawTx) > 0:
return nil, fmt.Errorf("cannot set both signed PSBT " +
"and final raw TX at the same time")
case len(msg.SignedPsbt) > 0:
packet, err = psbt.NewFromRawBytes(
bytes.NewReader(in.GetPsbtFinalize().SignedPsbt),
false,
)
if err != nil {
return nil, fmt.Errorf("error parsing psbt: %v",
err)
}
case len(msg.FinalRawTx) > 0:
rawTx = &wire.MsgTx{}
err = rawTx.Deserialize(bytes.NewReader(msg.FinalRawTx))
if err != nil {
return nil, fmt.Errorf("error parsing final "+
"raw TX: %v", err)
}
default:
return nil, fmt.Errorf("PSBT or raw transaction to " +
"finalize missing")
}
err = r.server.cc.wallet.PsbtFundingFinalize(
pendingChanID, packet,
pendingChanID, packet, rawTx,
)
if err != nil {
return nil, err