Merge pull request #5363 from guggero/psbt-no-final-tx
Allow skipping `PsbtFinalize` step during channel funding to support external broadcast
This commit is contained in:
14 files changed
+2380
-1785
No files matched your search
@@ -63,16 +63,16 @@ func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) {
|
||||
func (s *ShimIntent) Cancel() {
|
||||
}
|
||||
|
||||
// RemoteFundingAmt is the amount the remote party put into the channel.
|
||||
// LocalFundingAmt is the amount we put into the channel. This may differ from
|
||||
// the local amount requested, as depending on coin selection, we may bleed
|
||||
// from of that LocalAmt into fees to minimize change.
|
||||
//
|
||||
// NOTE: This method satisfies the chanfunding.Intent interface.
|
||||
func (s *ShimIntent) LocalFundingAmt() btcutil.Amount {
|
||||
return s.localFundingAmt
|
||||
}
|
||||
|
||||
// LocalFundingAmt is the amount we put into the channel. This may differ from
|
||||
// the local amount requested, as depending on coin selection, we may bleed
|
||||
// from of that LocalAmt into fees to minimize change.
|
||||
// RemoteFundingAmt is the amount the remote party put into the channel.
|
||||
//
|
||||
// NOTE: This method satisfies the chanfunding.Intent interface.
|
||||
func (s *ShimIntent) RemoteFundingAmt() btcutil.Amount {
|
||||
|
||||
@@ -139,6 +139,11 @@ type PsbtIntent struct {
|
||||
// NOTE: This channel must always be buffered.
|
||||
PsbtReady chan error
|
||||
|
||||
// shouldPublish specifies if the intent assumes its assembler should
|
||||
// publish the transaction once the channel funding has completed. If
|
||||
// this is set to false then the finalize step can be skipped.
|
||||
shouldPublish bool
|
||||
|
||||
// signalPsbtReady is a Once guard to make sure the PsbtReady channel is
|
||||
// only closed exactly once.
|
||||
signalPsbtReady sync.Once
|
||||
@@ -208,7 +213,7 @@ func (i *PsbtIntent) FundingParams() (btcutil.Address, int64, *psbt.Packet,
|
||||
// Verify makes sure the PSBT that is given to the intent has an output that
|
||||
// sends to the channel funding multisig address with the correct amount. A
|
||||
// simple check that at least a single input has been specified is performed.
|
||||
func (i *PsbtIntent) Verify(packet *psbt.Packet) error {
|
||||
func (i *PsbtIntent) Verify(packet *psbt.Packet, skipFinalize bool) error {
|
||||
if packet == nil {
|
||||
return fmt.Errorf("PSBT is nil")
|
||||
}
|
||||
@@ -259,7 +264,22 @@ func (i *PsbtIntent) Verify(packet *psbt.Packet) error {
|
||||
"malleability: %v", err)
|
||||
}
|
||||
|
||||
// In case we aren't going to publish any transaction, we now have
|
||||
// everything we need and can skip the Finalize step.
|
||||
i.PendingPsbt = packet
|
||||
if !i.shouldPublish && skipFinalize {
|
||||
i.FinalTX = packet.UnsignedTx
|
||||
i.State = PsbtFinalized
|
||||
|
||||
// Signal the funding manager that it can now continue with its
|
||||
// funding flow as the PSBT is now complete .
|
||||
i.signalPsbtReady.Do(func() {
|
||||
close(i.PsbtReady)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
i.State = PsbtVerified
|
||||
return nil
|
||||
}
|
||||
@@ -449,6 +469,12 @@ func (i *PsbtIntent) Outputs() []*wire.TxOut {
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldPublishFundingTX returns true if the intent assumes that its assembler
|
||||
// should publish the funding TX once the funding negotiation is complete.
|
||||
func (i *PsbtIntent) ShouldPublishFundingTX() bool {
|
||||
return i.shouldPublish
|
||||
}
|
||||
|
||||
// PsbtAssembler is a type of chanfunding.Assembler wherein the funding
|
||||
// transaction is constructed outside of lnd by using partially signed bitcoin
|
||||
// transactions (PSBT).
|
||||
@@ -500,10 +526,11 @@ func (p *PsbtAssembler) ProvisionChannel(req *Request) (Intent, error) {
|
||||
ShimIntent: ShimIntent{
|
||||
localFundingAmt: p.fundingAmt,
|
||||
},
|
||||
State: PsbtShimRegistered,
|
||||
BasePsbt: p.basePsbt,
|
||||
PsbtReady: make(chan error, 1),
|
||||
netParams: p.netParams,
|
||||
State: PsbtShimRegistered,
|
||||
BasePsbt: p.basePsbt,
|
||||
PsbtReady: make(chan error, 1),
|
||||
shouldPublish: p.shouldPublish,
|
||||
netParams: p.netParams,
|
||||
}
|
||||
|
||||
// A simple sanity check to ensure the provisioned request matches the
|
||||
|
||||
@@ -119,7 +119,7 @@ func TestPsbtIntent(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify the dummy PSBT with the intent.
|
||||
err = psbtIntent.Verify(pendingPsbt)
|
||||
err = psbtIntent.Verify(pendingPsbt, false)
|
||||
if err != nil {
|
||||
t.Fatalf("error verifying pending PSBT: %v", err)
|
||||
}
|
||||
@@ -271,61 +271,68 @@ func TestPsbtVerify(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
expectedErr string
|
||||
doVerify func(int64, *psbt.Packet, *PsbtIntent) error
|
||||
name string
|
||||
expectedErr string
|
||||
shouldPublish bool
|
||||
doVerify func(int64, *psbt.Packet, *PsbtIntent) error
|
||||
}{
|
||||
{
|
||||
name: "nil packet",
|
||||
expectedErr: "PSBT is nil",
|
||||
name: "nil packet",
|
||||
expectedErr: "PSBT is nil",
|
||||
shouldPublish: true,
|
||||
doVerify: func(amt int64, p *psbt.Packet,
|
||||
i *PsbtIntent) error {
|
||||
|
||||
return i.Verify(nil)
|
||||
return i.Verify(nil, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong state",
|
||||
name: "wrong state",
|
||||
shouldPublish: true,
|
||||
expectedErr: "invalid state. got user_canceled " +
|
||||
"expected output_known",
|
||||
doVerify: func(amt int64, p *psbt.Packet,
|
||||
i *PsbtIntent) error {
|
||||
|
||||
i.State = PsbtInitiatorCanceled
|
||||
return i.Verify(p)
|
||||
return i.Verify(p, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "output not found, value wrong",
|
||||
expectedErr: "funding output not found in PSBT",
|
||||
name: "output not found, value wrong",
|
||||
shouldPublish: true,
|
||||
expectedErr: "funding output not found in PSBT",
|
||||
doVerify: func(amt int64, p *psbt.Packet,
|
||||
i *PsbtIntent) error {
|
||||
|
||||
p.UnsignedTx.TxOut[0].Value = 123
|
||||
return i.Verify(p)
|
||||
return i.Verify(p, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "output not found, pk script wrong",
|
||||
expectedErr: "funding output not found in PSBT",
|
||||
name: "output not found, pk script wrong",
|
||||
shouldPublish: true,
|
||||
expectedErr: "funding output not found in PSBT",
|
||||
doVerify: func(amt int64, p *psbt.Packet,
|
||||
i *PsbtIntent) error {
|
||||
|
||||
p.UnsignedTx.TxOut[0].PkScript = []byte{1, 2, 3}
|
||||
return i.Verify(p)
|
||||
return i.Verify(p, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no inputs",
|
||||
expectedErr: "PSBT has no inputs",
|
||||
name: "no inputs",
|
||||
shouldPublish: true,
|
||||
expectedErr: "PSBT has no inputs",
|
||||
doVerify: func(amt int64, p *psbt.Packet,
|
||||
i *PsbtIntent) error {
|
||||
|
||||
return i.Verify(p)
|
||||
return i.Verify(p, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "input(s) too small",
|
||||
name: "input(s) too small",
|
||||
shouldPublish: true,
|
||||
expectedErr: "input amount sum must be larger than " +
|
||||
"output amount sum",
|
||||
doVerify: func(amt int64, p *psbt.Packet,
|
||||
@@ -337,11 +344,12 @@ func TestPsbtVerify(t *testing.T) {
|
||||
Value: int64(chanCapacity),
|
||||
},
|
||||
}}
|
||||
return i.Verify(p)
|
||||
return i.Verify(p, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing witness-utxo field",
|
||||
name: "missing witness-utxo field",
|
||||
shouldPublish: true,
|
||||
expectedErr: "cannot use TX for channel funding, not " +
|
||||
"all inputs are SegWit spends, risk of " +
|
||||
"malleability: input 1 is non-SegWit spend " +
|
||||
@@ -370,13 +378,66 @@ func TestPsbtVerify(t *testing.T) {
|
||||
txOut,
|
||||
},
|
||||
},
|
||||
}}
|
||||
return i.Verify(p)
|
||||
},
|
||||
}
|
||||
return i.Verify(p, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "input correct",
|
||||
expectedErr: "",
|
||||
name: "skip verify",
|
||||
shouldPublish: false,
|
||||
expectedErr: "",
|
||||
doVerify: func(amt int64, p *psbt.Packet,
|
||||
i *PsbtIntent) error {
|
||||
|
||||
txOut := &wire.TxOut{
|
||||
Value: int64(chanCapacity/2) + 1,
|
||||
}
|
||||
p.UnsignedTx.TxIn = []*wire.TxIn{
|
||||
{},
|
||||
{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Index: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
p.Inputs = []psbt.PInput{
|
||||
{
|
||||
WitnessUtxo: txOut,
|
||||
},
|
||||
{
|
||||
WitnessUtxo: txOut,
|
||||
},
|
||||
}
|
||||
|
||||
if err := i.Verify(p, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if i.FinalTX != p.UnsignedTx {
|
||||
return fmt.Errorf("expected final TX " +
|
||||
"to be set")
|
||||
}
|
||||
if i.State != PsbtFinalized {
|
||||
return fmt.Errorf("expected state to " +
|
||||
"be finalized")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-i.PsbtReady:
|
||||
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
return fmt.Errorf("expected PSBT " +
|
||||
"ready to be signaled")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "input correct",
|
||||
shouldPublish: true,
|
||||
expectedErr: "",
|
||||
doVerify: func(amt int64, p *psbt.Packet,
|
||||
i *PsbtIntent) error {
|
||||
|
||||
@@ -398,7 +459,7 @@ func TestPsbtVerify(t *testing.T) {
|
||||
{
|
||||
WitnessUtxo: txOut,
|
||||
}}
|
||||
return i.Verify(p)
|
||||
return i.Verify(p, false)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -425,6 +486,7 @@ func TestPsbtVerify(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Reset the state from a previous test and create a new
|
||||
// pending PSBT that we can manipulate.
|
||||
psbtIntent.shouldPublish = tc.shouldPublish
|
||||
psbtIntent.State = PsbtOutputKnown
|
||||
_, amt, pendingPsbt, err := psbtIntent.FundingParams()
|
||||
if err != nil {
|
||||
@@ -613,7 +675,7 @@ func TestPsbtFinalize(t *testing.T) {
|
||||
},
|
||||
FinalScriptWitness: []byte{0x01, 0x00},
|
||||
}}
|
||||
err = psbtIntent.Verify(pendingPsbt)
|
||||
err = psbtIntent.Verify(pendingPsbt, false)
|
||||
if err != nil {
|
||||
t.Fatalf("error verifying PSBT: %v", err)
|
||||
}
|
||||
|
||||
+30
-19
@@ -573,7 +573,7 @@ func (l *LightningWallet) RegisterFundingIntent(expectedID [32]byte,
|
||||
// pending channel ID and tries to advance the state machine by verifying the
|
||||
// passed PSBT.
|
||||
func (l *LightningWallet) PsbtFundingVerify(pendingChanID [32]byte,
|
||||
packet *psbt.Packet) error {
|
||||
packet *psbt.Packet, skipFinalize bool) error {
|
||||
|
||||
l.intentMtx.Lock()
|
||||
defer l.intentMtx.Unlock()
|
||||
@@ -587,7 +587,13 @@ func (l *LightningWallet) PsbtFundingVerify(pendingChanID [32]byte,
|
||||
if !ok {
|
||||
return fmt.Errorf("incompatible funding intent")
|
||||
}
|
||||
err := psbtIntent.Verify(packet)
|
||||
|
||||
if skipFinalize && psbtIntent.ShouldPublishFundingTX() {
|
||||
return fmt.Errorf("cannot set skip_finalize for channel that " +
|
||||
"did not set no_publish")
|
||||
}
|
||||
|
||||
err := psbtIntent.Verify(packet, skipFinalize)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error verifying PSBT: %v", err)
|
||||
}
|
||||
@@ -1454,9 +1460,11 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
|
||||
// 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.
|
||||
// With our keys bound, we can now construct and possibly sign
|
||||
// the final funding transaction and also obtain the chanPoint
|
||||
// that creates the channel. We _have_ to call CompileFundingTx
|
||||
// even if we don't publish ourselves as that sets the actual
|
||||
// funding outpoint in stone for this channel.
|
||||
fundingTx, err := psbtIntent.CompileFundingTx()
|
||||
if err != nil {
|
||||
req.err <- fmt.Errorf("unable to construct funding "+
|
||||
@@ -1470,23 +1478,26 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
|
||||
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,
|
||||
},
|
||||
|
||||
// Finally, we'll populate the relevant information in our
|
||||
// pendingReservation so the rest of the funding flow can
|
||||
// continue as normal in case we are going to publish ourselves.
|
||||
if psbtIntent.ShouldPublishFundingTX() {
|
||||
pendingReservation.fundingTx = fundingTx
|
||||
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,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user