Skip to content

Commit

Permalink
Merge pull request lightningnetwork#5363 from guggero/psbt-no-final-tx
Browse files Browse the repository at this point in the history
Allow skipping `PsbtFinalize` step during channel funding to support external broadcast
  • Loading branch information
guggero authored Oct 4, 2021
2 parents 692ea25 + 3e7df3d commit 51d19da
Show file tree
Hide file tree
Showing 14 changed files with 2,379 additions and 1,784 deletions.
225 changes: 225 additions & 0 deletions docs/psbt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
`);
}
```
10 changes: 10 additions & 0 deletions docs/release-notes/release-notes-0.14.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions funding/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion funding/batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Loading

0 comments on commit 51d19da

Please sign in to comment.