multi: allow skipping the PSBT finalize step

The FundingPsbtFinalize step is a safety measure that assures the final
signed funding transaction has the same TXID as was registered during
the funding flow and was used for the commitment transactions.
This step is cumbersome to use if the whole funding process is completed
external to lnd. We allow the finalize step to be skipped for such
cases. The API user/script will need to make sure things are verified
(and possibly cleaned up) properly.
This commit is contained in:
Oliver Gugger 2021-06-07 11:16:38 +02:00
parent d4136002c1
commit 1608faf199
No known key found for this signature in database
GPG Key ID: 8E4256593F177720
9 changed files with 1882 additions and 1760 deletions

View File

@ -126,7 +126,7 @@ type Wallet interface {
// 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.
PsbtFundingVerify([32]byte, *psbt.Packet) error
PsbtFundingVerify([32]byte, *psbt.Packet, bool) error
// PsbtFundingFinalize looks up a previously registered funding intent
// by its pending channel ID and tries to advance the state machine by
@ -355,7 +355,7 @@ func (b *Batcher) BatchFund(ctx context.Context,
// each of the channels.
for _, channel := range b.channels {
err = b.cfg.Wallet.PsbtFundingVerify(
channel.pendingChanID, unsignedPacket,
channel.pendingChanID, unsignedPacket, false,
)
if err != nil {
return nil, fmt.Errorf("error verifying PSBT: %v", err)

View File

@ -233,7 +233,7 @@ func (h *testHarness) ReleaseOutput(_ context.Context,
return &walletrpc.ReleaseOutputResponse{}, nil
}
func (h *testHarness) PsbtFundingVerify([32]byte, *psbt.Packet) error {
func (h *testHarness) PsbtFundingVerify([32]byte, *psbt.Packet, bool) error {
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -2132,6 +2132,20 @@ message FundingPsbtVerify {
// The pending channel ID of the channel to get the PSBT for.
bytes pending_chan_id = 2;
/*
Can only be used if the no_publish flag was set to true in the OpenChannel
call meaning that the caller is solely responsible for publishing the final
funding transaction. If skip_finalize is set to true then lnd will not wait
for a FundingPsbtFinalize state step and instead assumes that a transaction
with the same TXID as the passed in PSBT will eventually confirm.
IT IS ABSOLUTELY IMPERATIVE that the TXID of the transaction that is
eventually published does have the _same TXID_ as the verified PSBT. That
means no inputs or outputs can change, only signatures can be added. If the
TXID changes between this call and the publish step then the channel will
never be created and the funds will be in limbo.
*/
bool skip_finalize = 3;
}
message FundingPsbtFinalize {

View File

@ -4175,6 +4175,10 @@
"type": "string",
"format": "byte",
"description": "The pending channel ID of the channel to get the PSBT for."
},
"skip_finalize": {
"type": "boolean",
"description": "Can only be used if the no_publish flag was set to true in the OpenChannel\ncall meaning that the caller is solely responsible for publishing the final\nfunding transaction. If skip_finalize is set to true then lnd will not wait\nfor a FundingPsbtFinalize state step and instead assumes that a transaction\nwith the same TXID as the passed in PSBT will eventually confirm.\nIT IS ABSOLUTELY IMPERATIVE that the TXID of the transaction that is\neventually published does have the _same TXID_ as the verified PSBT. That\nmeans no inputs or outputs can change, only signatures can be added. If the\nTXID changes between this call and the publish step then the channel will\nnever be created and the funds will be in limbo."
}
}
},

View File

@ -213,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")
}
@ -264,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
}

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)
}

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)
}

View File

@ -7135,7 +7135,7 @@ func (r *rpcServer) FundingStateStep(ctx context.Context,
}
err = r.server.cc.Wallet.PsbtFundingVerify(
pendingChanID, packet,
pendingChanID, packet, in.GetPsbtVerify().SkipFinalize,
)
if err != nil {
return nil, err