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:
Oliver Gugger authored and GitHub committed 2021-10-04 12:37:51 +02:00
commit 51d19dad87
14 files changed
+2380 -1785

No files matched your search

+4 -4
View File
@@ -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 {
+32 -5
View File
@@ -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
+89 -27
View File
@@ -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
View File
@@ -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,
},
)
}
}
}