From b635b83abdc30d9366c331b3dd8cc589ff97feaa Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Sat, 10 Mar 2018 17:28:00 -0800 Subject: [PATCH 1/6] fundingmanager: rebroadcast iniatior's funding txn on restart This commit alters the behavior of the fundingmanager to rebroadcast the funding transaction of all pending-open channels upon restart. This is applied only to single-funder channels for which we are the initiator, and helps ensure that funding txns do not get stuck in the face of failures or restarts. --- fundingmanager.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/fundingmanager.go b/fundingmanager.go index 7c17fb3a..9e29de16 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -161,6 +161,10 @@ type fundingConfig struct { // funds from on-chain transaction outputs into Lightning channels. Wallet *lnwallet.LightningWallet + // PublishTransaction facilitates the process of broadcasting a + // transaction to the network. + PublishTransaction func(*wire.MsgTx) error + // FeeEstimator calculates appropriate fee rates based on historical // transaction information. FeeEstimator lnwallet.FeeEstimator @@ -422,6 +426,22 @@ func (f *fundingManager) Start() error { f.localDiscoverySignals[chanID] = make(chan struct{}) + // Rebroadcast the funding transaction for any pending channel + // that we initiated. If this operation fails due to a reported + // double spend, we treat this as an indicator that we have + // already broadcast this transaction. Otherwise, we simply log + // the error as there isn't anything we can currently do to + // recover. + if channel.ChanType == channeldb.SingleFunder && + channel.IsInitiator { + + err := f.cfg.PublishTransaction(channel.FundingTxn) + if err != nil && err != lnwallet.ErrDoubleSpend { + fndgLog.Warnf("unable to rebroadcast funding "+ + "txn: %v", err) + } + } + confChan := make(chan *lnwire.ShortChannelID) timeoutChan := make(chan struct{}) From db88c821695ccae7c4f5a2a4c85fe813b17eb213 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Sat, 10 Mar 2018 17:28:11 -0800 Subject: [PATCH 2/6] channeldb/channel: add serialization for single funder funding txn --- channeldb/channel.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/channeldb/channel.go b/channeldb/channel.go index 5b8b4b22..6d58f8ab 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -403,6 +403,14 @@ type OpenChannel struct { // failures and reforward HTLCs that were not fully processed. Packager FwdPackager + // FundingTxn is the transaction containing this channel's funding + // outpoint. Upon restarts, this txn will be rebroadcast if the channel + // is found to be pending. + // + // NOTE: This value will only be populated for single-funder channels + // for which we are the initiator. + FundingTxn *wire.MsgTx + // TODO(roasbeef): eww Db *DB @@ -1797,6 +1805,13 @@ func putChanInfo(chanBucket *bolt.Bucket, channel *OpenChannel) error { return err } + // For single funder channels that we initiated, write the funding txn. + if channel.ChanType == SingleFunder && channel.IsInitiator { + if err := writeElement(&w, channel.FundingTxn); err != nil { + return err + } + } + writeChanConfig := func(b io.Writer, c *ChannelConfig) error { return writeElements(b, c.DustLimit, c.MaxPendingAmount, c.ChanReserve, c.MinHTLC, @@ -1898,6 +1913,13 @@ func fetchChanInfo(chanBucket *bolt.Bucket, channel *OpenChannel) error { return err } + // For single funder channels that we initiated, read the funding txn. + if channel.ChanType == SingleFunder && channel.IsInitiator { + if err := readElement(r, &channel.FundingTxn); err != nil { + return err + } + } + readChanConfig := func(b io.Reader, c *ChannelConfig) error { return readElements(b, &c.DustLimit, &c.MaxPendingAmount, &c.ChanReserve, From 2e1dcd316c0d5596f9441440e802516c1379237f Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Sat, 10 Mar 2018 17:28:17 -0800 Subject: [PATCH 3/6] lnwallet/wallet: store funding txn during reservation --- lnwallet/wallet.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 0ad253fd..61f09c2f 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -1073,10 +1073,12 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs res.partialState.LocalChanCfg = res.ourContribution.toChanConfig() res.partialState.RemoteChanCfg = res.theirContribution.toChanConfig() + // We'll also record the finalized funding txn, which will allow us to + // rebroadcast on startup in case we fail. + res.partialState.FundingTxn = fundingTx + // Add the complete funding transaction to the DB, in its open bucket // which will be used for the lifetime of this channel. - // TODO(roasbeef): - // * attempt to retransmit funding transactions on re-start nodeAddr := res.nodeAddr err = res.partialState.SyncPending(nodeAddr, uint32(bestHeight)) if err != nil { From e754db44d214b52ab485bb29e6e607be378d770d Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Sat, 10 Mar 2018 17:28:04 -0800 Subject: [PATCH 4/6] lnd: configure fmgr with PublishTransaction --- lnd.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lnd.go b/lnd.go index 9842b77d..b2685f90 100644 --- a/lnd.go +++ b/lnd.go @@ -275,10 +275,11 @@ func lndMain() error { return err } fundingMgr, err := newFundingManager(fundingConfig{ - IDKey: idPrivKey.PubKey(), - Wallet: activeChainControl.wallet, - Notifier: activeChainControl.chainNotifier, - FeeEstimator: activeChainControl.feeEstimator, + IDKey: idPrivKey.PubKey(), + Wallet: activeChainControl.wallet, + PublishTransaction: activeChainControl.wallet.PublishTransaction, + Notifier: activeChainControl.chainNotifier, + FeeEstimator: activeChainControl.feeEstimator, SignMessage: func(pubKey *btcec.PublicKey, msg []byte) (*btcec.Signature, error) { From 3512cfe83646efbf4b93767a667f478e99133eb7 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Sat, 10 Mar 2018 17:09:03 -0800 Subject: [PATCH 5/6] fundingmanager_test: assert initator funding txn rebcast --- fundingmanager_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/fundingmanager_test.go b/fundingmanager_test.go index 451287ca..3fbc27b4 100644 --- a/fundingmanager_test.go +++ b/fundingmanager_test.go @@ -337,6 +337,7 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) { aliceMsgChan := make(chan lnwire.Message) aliceAnnounceChan := make(chan lnwire.Message) shutdownChan := make(chan struct{}) + publishChan := make(chan *wire.MsgTx, 10) oldCfg := alice.fundingMgr.cfg @@ -376,6 +377,10 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) { TempChanIDSeed: oldCfg.TempChanIDSeed, ArbiterChan: alice.arbiterChan, FindChannel: oldCfg.FindChannel, + PublishTransaction: func(txn *wire.MsgTx) error { + publishChan <- txn + return nil + }, }) if err != nil { t.Fatalf("failed recreating aliceFundingManager: %v", err) @@ -384,6 +389,7 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) { alice.fundingMgr = f alice.msgChan = aliceMsgChan alice.announceChan = aliceAnnounceChan + alice.publTxChan = publishChan alice.shutdownChannel = shutdownChan if err = f.Start(); err != nil { @@ -1228,6 +1234,13 @@ func TestFundingManagerFundingNotTimeoutInitiator(t *testing.T) { recreateAliceFundingManager(t, alice) + // We should receive the rebroadcasted funding txn. + select { + case <-alice.publTxChan: + case <-time.After(time.Second * 5): + t.Fatalf("alice did not publish funding tx") + } + // Increase the height to 1 minus the maxWaitNumBlocksFundingConf height alice.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{ Height: fundingBroadcastHeight + maxWaitNumBlocksFundingConf - 1, From 5df8b52daef229a69c26341452b7d7f8630395c7 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Sat, 10 Mar 2018 17:27:51 -0800 Subject: [PATCH 6/6] multi: set initiator funding txn --- breacharbiter_test.go | 1 + channeldb/channel_test.go | 1 + htlcswitch/test_utils.go | 35 +++++++++++++++++++++++++++++++++++ lnwallet/channel_test.go | 35 +++++++++++++++++++++++++++++++++++ test_utils.go | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+) diff --git a/breacharbiter_test.go b/breacharbiter_test.go index 31a08fd7..31d8002a 100644 --- a/breacharbiter_test.go +++ b/breacharbiter_test.go @@ -1412,6 +1412,7 @@ func createInitChannels(revocationWindow int) (*lnwallet.LightningChannel, *lnwa RemoteCommitment: aliceCommit, Db: dbAlice, Packager: channeldb.NewChannelPackager(shortChanID), + FundingTxn: testTx, } bobChannelState := &channeldb.OpenChannel{ LocalChanCfg: bobCfg, diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index ba93410f..4cf14c78 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -220,6 +220,7 @@ func createTestChannelState(cdb *DB) (*OpenChannel, error) { RevocationStore: store, Db: cdb, Packager: NewChannelPackager(chanID), + FundingTxn: testTx, }, nil } diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index d5a6f93c..9ba0a676 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -54,6 +54,40 @@ var ( "5445068131219452686511677818569431", 10) _, _ = testSig.S.SetString("1880105606924982582529128710493133386286603"+ "3135609736119018462340006816851118", 10) + + // testTx is used as the default funding txn for single-funder channels. + testTx = &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0xffffffff, + }, + SignatureScript: []byte{0x04, 0x31, 0xdc, 0x00, 0x1b, 0x01, 0x62}, + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 5000000000, + PkScript: []byte{ + 0x41, // OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, + 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, + 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, + 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, + 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, + 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, + 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, + 0xa6, // 65-byte signature + 0xac, // OP_CHECKSIG + }, + }, + }, + LockTime: 5, + } ) var idSeqNum uint64 @@ -294,6 +328,7 @@ func createTestChannel(alicePrivKey, bobPrivKey []byte, ShortChanID: chanID, Db: dbAlice, Packager: channeldb.NewChannelPackager(chanID), + FundingTxn: testTx, } bobChannelState := &channeldb.OpenChannel{ diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index cbf73ad6..2f8ccad1 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -57,6 +57,40 @@ var ( // The number of confirmations required to consider any created channel // open. numReqConfs = uint16(1) + + // A serializable txn for testing funding txn. + testTx = &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0xffffffff, + }, + SignatureScript: []byte{0x04, 0x31, 0xdc, 0x00, 0x1b, 0x01, 0x62}, + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 5000000000, + PkScript: []byte{ + 0x41, // OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, + 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, + 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, + 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, + 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, + 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, + 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, + 0xa6, // 65-byte signature + 0xac, // OP_CHECKSIG + }, + }, + }, + LockTime: 5, + } ) // initRevocationWindows simulates a new channel being opened within the p2p @@ -308,6 +342,7 @@ func createTestChannels(revocationWindow int) (*LightningChannel, RemoteCommitment: aliceCommit, Db: dbAlice, Packager: channeldb.NewChannelPackager(shortChanID), + FundingTxn: testTx, } bobChannelState := &channeldb.OpenChannel{ LocalChanCfg: bobCfg, diff --git a/test_utils.go b/test_utils.go index 37a83068..bd5ee4b2 100644 --- a/test_utils.go +++ b/test_utils.go @@ -49,6 +49,40 @@ var ( // Just use some arbitrary bytes as delivery script. dummyDeliveryScript = alicesPrivKey[:] + + // testTx is used as the default funding txn for single-funder channels. + testTx = &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0xffffffff, + }, + SignatureScript: []byte{0x04, 0x31, 0xdc, 0x00, 0x1b, 0x01, 0x62}, + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 5000000000, + PkScript: []byte{ + 0x41, // OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, + 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, + 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, + 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, + 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, + 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, + 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, + 0xa6, // 65-byte signature + 0xac, // OP_CHECKSIG + }, + }, + }, + LockTime: 5, + } ) // createTestPeer creates a channel between two nodes, and returns a peer for @@ -219,6 +253,7 @@ func createTestPeer(notifier chainntnfs.ChainNotifier, RemoteCommitment: aliceCommit, Db: dbAlice, Packager: channeldb.NewChannelPackager(shortChanID), + FundingTxn: testTx, } bobChannelState := &channeldb.OpenChannel{ LocalChanCfg: bobCfg,