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 2021-10-04 12:37:51 +02:00 committed by GitHub
commit 51d19dad87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 2380 additions and 1785 deletions

View File

@ -640,3 +640,228 @@ lingering reservations/intents/pending channels are cleaned up.
**NOTE**: You must be connected to each of the nodes you want to open channels
to before you run the command.
### Example Node.JS script
To demonstrate how the PSBT funding API can be used with JavaScript, we add a
simple example script that imitates the behavior of `lncli` but **does not
publish** the final transaction itself. This allows the app creator to publish
the transaction whenever everything is ready.
> multi-channel-funding.js
```js
const fs = require('fs');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const Buffer = require('safe-buffer').Buffer;
const randomBytes = require('random-bytes').sync;
const prompt = require('prompt');
const LND_DIR = '/home/myuser/.lnd';
const LND_HOST = 'localhost:10009';
const NETWORK = 'regtest';
const LNRPC_PROTO_DIR = '/home/myuser/projects/go/lnd/lnrpc';
const grpcOptions = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
includeDirs: [LNRPC_PROTO_DIR],
};
const packageDefinition = protoLoader.loadSync(`${LNRPC_PROTO_DIR}/rpc.proto`, grpcOptions);
const lnrpc = grpc.loadPackageDefinition(packageDefinition).lnrpc;
process.env.GRPC_SSL_CIPHER_SUITES = 'HIGH+ECDSA';
const adminMac = fs.readFileSync(`${LND_DIR}/data/chain/bitcoin/${NETWORK}/admin.macaroon`);
const metadata = new grpc.Metadata();
metadata.add('macaroon', adminMac.toString('hex'));
const macaroonCreds = grpc.credentials.createFromMetadataGenerator((_args, callback) => {
callback(null, metadata);
});
const lndCert = fs.readFileSync(`${LND_DIR}/tls.cert`);
const sslCreds = grpc.credentials.createSsl(lndCert);
const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds);
const client = new lnrpc.Lightning(LND_HOST, credentials);
const params = process.argv.slice(2);
if (params.length % 2 !== 0) {
console.log('Usage: node multi-channel-funding.js pubkey amount [pubkey amount]...')
}
const channels = [];
for (let i = 0; i < params.length; i += 2) {
channels.push({
pubKey: Buffer.from(params[i], 'hex'),
amount: parseInt(params[i + 1], 10),
pendingChanID: randomBytes(32),
outputAddr: '',
finalized: false,
chanPending: null,
cleanedUp: false,
});
}
channels.forEach(c => {
const openChannelMsg = {
node_pubkey: c.pubKey,
local_funding_amount: c.amount,
funding_shim: {
psbt_shim: {
pending_chan_id: c.pendingChanID,
no_publish: true,
}
}
};
const openChannelCall = client.OpenChannel(openChannelMsg);
openChannelCall.on('data', function (update) {
if (update.psbt_fund && update.psbt_fund.funding_address) {
console.log('Got funding addr for PSBT: ' + update.psbt_fund.funding_address);
c.outputAddr = update.psbt_fund.funding_address;
maybeFundPSBT();
}
if (update.chan_pending) {
c.chanPending = update.chan_pending;
const txidStr = update.chan_pending.txid.reverse().toString('hex');
console.log(`
Channels are now pending!
Expected TXID of published final transaction: ${txidStr}
`);
process.exit(0);
}
});
openChannelCall.on('error', function (e) {
console.log('Error on open channel call: ' + e);
tryCleanup();
});
});
function tryCleanup() {
function maybeExit() {
for (let i = 0; i < channels.length; i++) {
if (!channels[i].cleanedUp) {
// Not all channels are cleaned up yet.
return;
}
}
}
channels.forEach(c => {
if (c.cleanedUp) {
return;
}
if (c.chanPending === null) {
console.log("Cleaning up channel, shim cancel")
// The channel never made it into the pending state, let's try to
// remove the funding shim. This is best effort. Depending on the
// state of the channel this might fail so we don't log any errors
// here.
client.FundingStateStep({
shim_cancel: {
pending_chan_id: c.pendingChanID,
}
}, () => {
c.cleanedUp = true;
maybeExit();
});
} else {
// The channel is pending but since we aborted will never make it
// to be confirmed. We need to tell lnd to abandon this channel
// otherwise it will show in the pending channels for forever.
console.log("Cleaning up channel, abandon channel")
client.AbandonChannel({
channel_point: {
funding_txid: {
funding_txid_bytes: c.chanPending.txid,
},
output_index: c.chanPending.output_index,
},
i_know_what_i_am_doing: true,
}, () => {
c.cleanedUp = true;
maybeExit();
});
}
});
}
function maybeFundPSBT() {
const outputsBitcoind = [];
const outputsLnd = {};
for (let i = 0; i < channels.length; i++) {
const c = channels[i];
if (c.outputAddr === '') {
// Not all channels did get a funding address yet.
return;
}
outputsBitcoind.push({
[c.outputAddr]: c.amount / 100000000,
});
outputsLnd[c.outputAddr] = c.amount;
}
console.log(`
Channels ready for funding transaction.
Please create a funded PSBT now.
Examples:
bitcoind:
bitcoin-cli walletcreatefundedpsbt '[]' '${JSON.stringify(outputsBitcoind)}' 0 '{"fee_rate": 15}'
lnd:
lncli wallet psbt fund --outputs='${JSON.stringify(outputsLnd)}' --sat_per_vbyte=15
`);
prompt.get([{name: 'funded_psbt'}], (err, result) => {
if (err) {
console.log(err);
tryCleanup();
return;
}
channels.forEach(c => {
const verifyMsg = {
psbt_verify: {
funded_psbt: Buffer.from(result.funded_psbt, 'base64'),
pending_chan_id: c.pendingChanID,
skip_finalize: true
}
};
client.FundingStateStep(verifyMsg, (err, res) => {
if (err) {
console.log(err);
tryCleanup();
return;
}
if (res) {
c.finalized = true;
maybePublishPSBT();
}
});
});
});
}
function maybePublishPSBT() {
for (let i = 0; i < channels.length; i++) {
const c = channels[i];
if (!channels[i].finalized) {
// Not all channels are verified/finalized yet.
return;
}
}
console.log(`
PSBT verification successful!
You can now sign and publish the transaction.
Make sure the TXID does not change!
`);
}
```

View File

@ -120,6 +120,16 @@ proposed channel type is used.
* [Adds NOT_FOUND status code for LookupInvoice](https://github.com/lightningnetwork/lnd/pull/5768)
* 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](https://github.com/lightningnetwork/lnd/pull/5363) for such cases.
The API user/script will need to make sure things are verified (and possibly
cleaned up) properly. An example script was added to the [PSBT
documentation](../psbt.md) to show the simplified process.
### Batched channel funding
[Multiple channels can now be opened in a single

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

@ -245,6 +245,223 @@ func testPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest) {
closeChannelAndAssert(t, net, carol, chanPoint, false)
}
// testPsbtChanFundingExternal makes sure a channel can be opened between carol
// and dave by using a Partially Signed Bitcoin Transaction that funds the
// channel multisig funding output and is fully funded by an external third
// party.
func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) {
ctxb := context.Background()
const chanSize = funding.MaxBtcFundingAmount
// First, we'll create two new nodes that we'll use to open channels
// between for this test. Both these nodes have an empty wallet as Alice
// will be funding the channel.
carol := net.NewNode(t.t, "carol", nil)
defer shutdownAndAssert(net, t, carol)
dave := net.NewNode(t.t, "dave", nil)
defer shutdownAndAssert(net, t, dave)
// Before we start the test, we'll ensure both sides are connected so
// the funding flow can be properly executed.
net.EnsureConnected(t.t, carol, dave)
net.EnsureConnected(t.t, carol, net.Alice)
// At this point, we can begin our PSBT channel funding workflow. We'll
// start by generating a pending channel ID externally that will be used
// to track this new funding type.
var pendingChanID [32]byte
_, err := rand.Read(pendingChanID[:])
require.NoError(t.t, err)
// We'll also test batch funding of two channels so we need another ID.
var pendingChanID2 [32]byte
_, err = rand.Read(pendingChanID2[:])
require.NoError(t.t, err)
// Now that we have the pending channel ID, Carol will open the channel
// by specifying a PSBT shim. We use the NoPublish flag here to avoid
// publishing the whole batch TX too early.
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
chanUpdates, tempPsbt, err := openChannelPsbt(
ctxt, carol, dave, lntest.OpenChannelParams{
Amt: chanSize,
FundingShim: &lnrpc.FundingShim{
Shim: &lnrpc.FundingShim_PsbtShim{
PsbtShim: &lnrpc.PsbtShim{
PendingChanId: pendingChanID[:],
NoPublish: true,
},
},
},
},
)
require.NoError(t.t, err)
// Let's add a second channel to the batch. This time between Carol and
// Alice. We will publish the batch TX once this channel funding is
// complete.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
chanUpdates2, psbtBytes2, err := openChannelPsbt(
ctxt, carol, net.Alice, lntest.OpenChannelParams{
Amt: chanSize,
FundingShim: &lnrpc.FundingShim{
Shim: &lnrpc.FundingShim_PsbtShim{
PsbtShim: &lnrpc.PsbtShim{
PendingChanId: pendingChanID2[:],
NoPublish: true,
BasePsbt: tempPsbt,
},
},
},
},
)
require.NoError(t.t, err)
// We'll now ask Alice's wallet to fund the PSBT for us. This will
// return a packet with inputs and outputs set but without any witness
// data. This is exactly what we need for the next step.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
fundReq := &walletrpc.FundPsbtRequest{
Template: &walletrpc.FundPsbtRequest_Psbt{
Psbt: psbtBytes2,
},
Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{
SatPerVbyte: 2,
},
}
fundResp, err := net.Alice.WalletKitClient.FundPsbt(ctxt, fundReq)
require.NoError(t.t, err)
// We have a PSBT that has no witness data yet, which is exactly what we
// need for the next step: Verify the PSBT with the funding intents.
// We tell the PSBT intent to skip the finalize step because we know the
// final transaction will not be broadcast by Carol herself but by
// Alice. And we assume that Alice is a third party that is not in
// direct communication with Carol and won't send the signed TX to her
// before broadcasting it. So we cannot call the finalize step but
// instead just tell lnd to wait for a TX to be published/confirmed.
_, err = carol.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{
Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{
PsbtVerify: &lnrpc.FundingPsbtVerify{
PendingChanId: pendingChanID[:],
FundedPsbt: fundResp.FundedPsbt,
SkipFinalize: true,
},
},
})
require.NoError(t.t, err)
_, err = carol.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{
Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{
PsbtVerify: &lnrpc.FundingPsbtVerify{
PendingChanId: pendingChanID2[:],
FundedPsbt: fundResp.FundedPsbt,
SkipFinalize: true,
},
},
})
require.NoError(t.t, err)
// Consume the "channel pending" update. This waits until the funding
// transaction was fully compiled for both channels.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
updateResp, err := receiveChanUpdate(ctxt, chanUpdates)
require.NoError(t.t, err)
upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending)
require.True(t.t, ok)
chanPoint := &lnrpc.ChannelPoint{
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
FundingTxidBytes: upd.ChanPending.Txid,
},
OutputIndex: upd.ChanPending.OutputIndex,
}
updateResp2, err := receiveChanUpdate(ctxt, chanUpdates2)
require.NoError(t.t, err)
upd2, ok := updateResp2.Update.(*lnrpc.OpenStatusUpdate_ChanPending)
require.True(t.t, ok)
chanPoint2 := &lnrpc.ChannelPoint{
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
FundingTxidBytes: upd2.ChanPending.Txid,
},
OutputIndex: upd2.ChanPending.OutputIndex,
}
numPending, err := numOpenChannelsPending(ctxt, carol)
require.NoError(t.t, err)
require.Equal(t.t, 2, numPending)
// Now we'll ask Alice's wallet to sign the PSBT so we can finish the
// funding flow.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
finalizeReq := &walletrpc.FinalizePsbtRequest{
FundedPsbt: fundResp.FundedPsbt,
}
finalizeRes, err := net.Alice.WalletKitClient.FinalizePsbt(
ctxt, finalizeReq,
)
require.NoError(t.t, err)
// No transaction should have been published yet.
mempool, err := net.Miner.Client.GetRawMempool()
require.NoError(t.t, err)
require.Equal(t.t, 0, len(mempool))
// Great, now let's publish the final raw transaction.
var finalTx wire.MsgTx
err = finalTx.Deserialize(bytes.NewReader(finalizeRes.RawFinalTx))
require.NoError(t.t, err)
txHash := finalTx.TxHash()
_, err = net.Miner.Client.SendRawTransaction(&finalTx, false)
require.NoError(t.t, err)
// Now we can mine a block to get the transaction confirmed, then wait
// for the new channel to be propagated through the network.
block := mineBlocks(t, net, 6, 1)[0]
assertTxInBlock(t, block, &txHash)
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint)
require.NoError(t.t, err)
err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint2)
require.NoError(t.t, err)
// With the channel open, ensure that it is counted towards Carol's
// total channel balance.
balReq := &lnrpc.ChannelBalanceRequest{}
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
balRes, err := carol.ChannelBalance(ctxt, balReq)
require.NoError(t.t, err)
require.NotEqual(t.t, int64(0), balRes.LocalBalance.Sat)
// Next, to make sure the channel functions as normal, we'll make some
// payments within the channel.
payAmt := btcutil.Amount(100000)
invoice := &lnrpc.Invoice{
Memo: "new chans",
Value: int64(payAmt),
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
resp, err := dave.AddInvoice(ctxt, invoice)
require.NoError(t.t, err)
err = completePaymentRequests(
carol, carol.RouterClient, []string{resp.PaymentRequest}, true,
)
require.NoError(t.t, err)
// To conclude, we'll close the newly created channel between Carol and
// Dave. This function will also block until the channels are closed and
// will additionally assert the relevant channel closing post
// conditions.
closeChannelAndAssert(t, net, carol, chanPoint, false)
closeChannelAndAssert(t, net, carol, chanPoint2, false)
}
// openChannelPsbt attempts to open a channel between srcNode and destNode with
// the passed channel funding parameters. If the passed context has a timeout,
// then if the timeout is reached before the channel pending notification is

View File

@ -275,6 +275,10 @@ var allTestCases = []*testCase{
name: "psbt channel funding",
test: testPsbtChanFunding,
},
{
name: "psbt channel funding external",
test: testPsbtChanFundingExternal,
},
{
name: "batch channel funding",
test: testBatchChanFunding,

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 {

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

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

View File

@ -7133,7 +7133,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