lnwallet+fundingmgr: interrupt funding flow for PSBT

In case the funding manager detects that a funding flow is requested
to be executed with the help of a PsbtIntent, the normal channel
negotiation with the remote peer is interrupted, as soon as the
accept_channel message was received. With the remote peer's funding
multisig key and our local key, we can derive the funding output
script and its address. This is enough to start the PSBT funding
and signing process which the user will do externally to the daemon.
This commit is contained in:
Oliver Gugger committed 2020-03-31 09:17:24 +02:00
1 parent 126f79dbb1
commit 5a52420ab6
4 files changed
+419 -19

No files matched your search

+213 -9
View File
@@ -15,6 +15,7 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/psbt"
"github.com/btcsuite/btcutil/txsort"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/channeldb"
@@ -33,6 +34,28 @@ const (
msgBufferSize = 100
)
var (
// ErrPsbtFundingRequired is the error that is returned during the
// contribution handling process if the process should be paused for
// the construction of a PSBT outside of lnd's wallet.
ErrPsbtFundingRequired = errors.New("PSBT funding required")
)
// PsbtFundingRequired is a type that implements the error interface and
// contains the information needed to construct a PSBT.
type PsbtFundingRequired struct {
// Intent is the pending PSBT funding intent that needs to be funded
// if the wrapping error is returned.
Intent *chanfunding.PsbtIntent
}
// Error returns the underlying error.
//
// NOTE: This method is part of the error interface.
func (p *PsbtFundingRequired) Error() string {
return ErrPsbtFundingRequired.Error()
}
// InitFundingReserveMsg is the first message sent to initiate the workflow
// required to open a payment channel with a remote peer. The initial required
// parameters are configurable across channels. These parameters are to be
@@ -150,6 +173,16 @@ type addContributionMsg struct {
err chan error
}
// continueContributionMsg represents a message that signals that the
// interrupted funding process involving a PSBT can now be continued because the
// finalized transaction is now available.
type continueContributionMsg struct {
pendingFundingID uint64
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
err chan error
}
// addSingleContributionMsg represents a message executing the second phase of
// a single funder channel reservation workflow. This messages carries the
// counterparty's "contribution" to the payment channel. As this message is
@@ -395,6 +428,8 @@ out:
l.handleSingleContribution(msg)
case *addContributionMsg:
l.handleContributionMsg(msg)
case *continueContributionMsg:
l.handleChanPointReady(msg)
case *addSingleFunderSigsMsg:
l.handleSingleFunderSigs(msg)
case *addCounterPartySigsMsg:
@@ -462,17 +497,74 @@ func (l *LightningWallet) RegisterFundingIntent(expectedID [32]byte,
return nil
}
// PsbtFundingVerify looks up a previously registered funding intent by its
// pending channel ID and tries to advance the state machine by verifying the
// passed PSBT.
func (l *LightningWallet) PsbtFundingVerify(pid [32]byte,
packet *psbt.Packet) error {
l.intentMtx.Lock()
defer l.intentMtx.Unlock()
intent, ok := l.fundingIntents[pid]
if !ok {
return fmt.Errorf("no funding intent found for "+
"pendingChannelID(%x)", pid[:])
}
psbtIntent, ok := intent.(*chanfunding.PsbtIntent)
if !ok {
return fmt.Errorf("incompatible funding intent")
}
err := psbtIntent.Verify(packet)
if err != nil {
return fmt.Errorf("error verifying PSBT: %v", err)
}
return nil
}
// 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 {
l.intentMtx.Lock()
defer l.intentMtx.Unlock()
intent, ok := l.fundingIntents[pid]
if !ok {
return fmt.Errorf("no funding intent found for "+
"pendingChannelID(%x)", pid[:])
}
psbtIntent, ok := intent.(*chanfunding.PsbtIntent)
if !ok {
return fmt.Errorf("incompatible funding intent")
}
err := psbtIntent.Finalize(packet)
if err != nil {
return fmt.Errorf("error finalizing PSBT: %v", err)
}
return nil
}
// CancelFundingIntent allows a caller to cancel a previously registered
// funding intent. If no intent was found, then an error will be returned.
func (l *LightningWallet) CancelFundingIntent(pid [32]byte) error {
l.intentMtx.Lock()
defer l.intentMtx.Unlock()
if _, ok := l.fundingIntents[pid]; !ok {
intent, ok := l.fundingIntents[pid]
if !ok {
return fmt.Errorf("no funding intent found for "+
"pendingChannelID(%x)", pid[:])
}
// Give the intent a chance to clean up after itself, removing coin
// locks or similar reserved resources.
intent.Cancel()
delete(l.fundingIntents, pid)
return nil
@@ -556,10 +648,28 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
return
}
// Register the funding intent now in case we need to access it
// again later, as it's the case for the PSBT state machine for
// example.
err = l.RegisterFundingIntent(req.PendingChanID, fundingIntent)
if err != nil {
req.err <- err
req.resp <- nil
return
}
localFundingAmt = fundingIntent.LocalFundingAmt()
remoteFundingAmt = fundingIntent.RemoteFundingAmt()
}
// At this point there _has_ to be a funding intent, otherwise something
// went really wrong.
if fundingIntent == nil {
req.err <- fmt.Errorf("no funding intent present")
req.resp <- nil
return
}
// If this is a shim intent, then it may be attempting to use an
// existing set of keys for the funding workflow. In this case, we'll
// make a simple wrapper keychain.KeyRing that will proxy certain
@@ -592,9 +702,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
thawHeight,
)
if err != nil {
if fundingIntent != nil {
fundingIntent.Cancel()
}
fundingIntent.Cancel()
req.err <- err
req.resp <- nil
@@ -605,9 +713,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
reservation, fundingIntent, req.NodeAddr, req.NodeID, keyRing,
)
if err != nil {
if fundingIntent != nil {
fundingIntent.Cancel()
}
fundingIntent.Cancel()
req.err <- err
req.resp <- nil
@@ -852,6 +958,9 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
// which type of intent we obtained from our chanfunding.Assembler,
// we'll carry out a distinct set of steps.
switch fundingIntent := pendingReservation.fundingIntent.(type) {
// The transaction was created outside of the wallet and might already
// be published. Nothing left to do other than using the correct
// outpoint.
case *chanfunding.ShimIntent:
chanPoint, err = fundingIntent.ChanPoint()
if err != nil {
@@ -861,6 +970,38 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
pendingReservation.partialState.FundingOutpoint = *chanPoint
// The user has signaled that they want to use a PSBT to construct the
// funding transaction. Because we now have the multisig keys from both
// parties, we can create the multisig script that needs to be funded
// and then pause the process until the user supplies the PSBT
// containing the eventual funding transaction.
case *chanfunding.PsbtIntent:
if fundingIntent.PendingPsbt != nil {
req.err <- fmt.Errorf("PSBT funding already in" +
"progress")
return
}
// Now that we know our contribution, we can bind both the local
// and remote key which will be needed to calculate the multisig
// funding output in a next step.
pendingChanID := pendingReservation.pendingChanID
walletLog.Debugf("Advancing PSBT funding flow for "+
"pending_id(%x), binding keys local_key=%v, "+
"remote_key=%x", pendingChanID,
&ourContribution.MultiSigKey,
theirContribution.MultiSigKey.PubKey.SerializeCompressed())
fundingIntent.BindKeys(
&ourContribution.MultiSigKey,
theirContribution.MultiSigKey.PubKey,
)
// Exit early because we can't continue the funding flow yet.
req.err <- &PsbtFundingRequired{
Intent: fundingIntent,
}
return
case *chanfunding.FullIntent:
// Now that we know their public key, we can bind theirs as
// well as ours to the funding intent.
@@ -915,6 +1056,69 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
"generated: %v", chanPoint, spew.Sdump(fundingTx))
}
// If we landed here and didn't exit early, it means we already have
// the channel point ready. We can jump directly to the next step.
l.handleChanPointReady(&continueContributionMsg{
pendingFundingID: req.pendingFundingID,
err: req.err,
})
}
// handleChanPointReady continues the funding process once the channel point
// is known and the funding transaction can be completed.
func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
l.limboMtx.Lock()
pendingReservation, ok := l.fundingLimbo[req.pendingFundingID]
l.limboMtx.Unlock()
if !ok {
req.err <- fmt.Errorf("attempted to update non-existent " +
"funding state")
return
}
ourContribution := pendingReservation.ourContribution
theirContribution := pendingReservation.theirContribution
chanPoint := pendingReservation.partialState.FundingOutpoint
// If we're in the PSBT funding flow, we now should have everything that
// is needed to construct and publish the full funding transaction.
intent := pendingReservation.fundingIntent
if psbtIntent, ok := intent.(*chanfunding.PsbtIntent); ok {
// With our keys bound, we can now construct+sign the final
// funding transaction and also obtain the chanPoint that
// creates the channel.
fundingTx, err := psbtIntent.CompileFundingTx()
if err != nil {
req.err <- fmt.Errorf("unable to construct funding "+
"tx: %v", err)
return
}
chanPointPtr, err := psbtIntent.ChanPoint()
if err != nil {
req.err <- fmt.Errorf("unable to obtain chan "+
"point: %v", err)
return
}
// Finally, we'll populate the relevant information in our
// pendingReservation so the rest of the funding flow can
// continue as normal.
pendingReservation.fundingTx = fundingTx
pendingReservation.partialState.FundingOutpoint = *chanPointPtr
chanPoint = *chanPointPtr
pendingReservation.ourFundingInputScripts = make(
[]*input.Script, 0, len(ourContribution.Inputs),
)
for _, txIn := range fundingTx.TxIn {
pendingReservation.ourFundingInputScripts = append(
pendingReservation.ourFundingInputScripts,
&input.Script{
Witness: txIn.Witness,
SigScript: txIn.SignatureScript,
},
)
}
}
// Initialize an empty sha-chain for them, tracking the current pending
// revocation hash (we don't yet know the preimage so we can't add it
// to the chain).
@@ -930,7 +1134,7 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
// Create the txin to our commitment transaction; required to construct
// the commitment transactions.
fundingTxIn := wire.TxIn{
PreviousOutPoint: *chanPoint,
PreviousOutPoint: chanPoint,
}
// With the funding tx complete, create both commitment transactions.
@@ -991,7 +1195,7 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
chanPoint, spew.Sdump(theirCommitTx))
// Record newly available information within the open channel state.
chanState.FundingOutpoint = *chanPoint
chanState.FundingOutpoint = chanPoint
chanState.LocalCommitment.CommitTx = ourCommitTx
chanState.RemoteCommitment.CommitTx = theirCommitTx