diff --git a/breacharbiter.go b/breacharbiter.go index 6924682f..865e67ce 100644 --- a/breacharbiter.go +++ b/breacharbiter.go @@ -20,6 +20,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb/kvdb" "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/labels" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) @@ -566,7 +567,8 @@ justiceTxBroadcast: // We'll now attempt to broadcast the transaction which finalized the // channel's retribution against the cheating counter party. - err = b.cfg.PublishTransaction(finalTx, "") + label := labels.MakeLabel(labels.LabelTypeJusticeTransaction, nil) + err = b.cfg.PublishTransaction(finalTx, label) if err != nil { brarLog.Errorf("Unable to broadcast justice tx: %v", err) diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index dacbc1f5..dceee6c8 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -15,6 +15,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb/kvdb" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/labels" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" @@ -715,7 +716,10 @@ func (c *ChainArbitrator) rebroadcast(channel *channeldb.OpenChannel, log.Infof("Re-publishing %s close tx(%v) for channel %v", kind, closeTx.TxHash(), chanPoint) - err = c.cfg.PublishTx(closeTx, "") + label := labels.MakeLabel( + labels.LabelTypeChannelClose, &channel.ShortChannelID, + ) + err = c.cfg.PublishTx(closeTx, label) if err != nil && err != lnwallet.ErrDoubleSpend { log.Warnf("Unable to broadcast %s close tx(%v): %v", kind, closeTx.TxHash(), err) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index b67d2c7c..2d5009e8 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -16,6 +16,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/kvdb" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/labels" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" @@ -874,7 +875,11 @@ func (c *ChannelArbitrator) stateStep( // At this point, we'll now broadcast the commitment // transaction itself. - if err := c.cfg.PublishTx(closeTx, ""); err != nil { + label := labels.MakeLabel( + labels.LabelTypeChannelClose, &c.cfg.ShortChanID, + ) + + if err := c.cfg.PublishTx(closeTx, label); err != nil { log.Errorf("ChannelArbitrator(%v): unable to broadcast "+ "close tx: %v", c.cfg.ChanPoint, err) if err != lnwallet.ErrDoubleSpend { diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 7b262091..1a99cc3b 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -10,6 +10,7 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/labels" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/sweep" ) @@ -157,7 +158,10 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { // Regardless of whether an existing transaction was found or newly // constructed, we'll broadcast the sweep transaction to the // network. - err := h.PublishTx(h.sweepTx, "") + label := labels.MakeLabel( + labels.LabelTypeChannelClose, &h.ShortChanID, + ) + err := h.PublishTx(h.sweepTx, label) if err != nil { log.Infof("%T(%x): unable to publish tx: %v", h, h.htlc.RHash[:], err) @@ -206,7 +210,10 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { // the claiming process. // // TODO(roasbeef): after changing sighashes send to tx bundler - err := h.PublishTx(h.htlcResolution.SignedSuccessTx, "") + label := labels.MakeLabel( + labels.LabelTypeChannelClose, &h.ShortChanID, + ) + err := h.PublishTx(h.htlcResolution.SignedSuccessTx, label) if err != nil { return nil, err } diff --git a/fundingmanager.go b/fundingmanager.go index aa3887ed..826df8ab 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -22,6 +22,7 @@ import ( "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/labels" "github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwallet" @@ -243,6 +244,10 @@ type fundingConfig struct { // transaction to the network. PublishTransaction func(*wire.MsgTx, string) error + // UpdateLabel updates the label that a transaction has in our wallet, + // overwriting any existing labels. + UpdateLabel func(chainhash.Hash, string) error + // FeeEstimator calculates appropriate fee rates based on historical // transaction information. FeeEstimator chainfee.Estimator @@ -576,8 +581,15 @@ func (f *fundingManager) start() error { channel.FundingOutpoint, fundingTxBuf.Bytes()) + // Set a nil short channel ID at this stage + // because we do not know it until our funding + // tx confirms. + label := labels.MakeLabel( + labels.LabelTypeChannelOpen, nil, + ) + err = f.cfg.PublishTransaction( - channel.FundingTxn, "", + channel.FundingTxn, label, ) if err != nil { fndgLog.Errorf("Unable to rebroadcast "+ @@ -2032,7 +2044,13 @@ func (f *fundingManager) handleFundingSigned(fmsg *fundingSignedMsg) { fndgLog.Infof("Broadcasting funding tx for ChannelPoint(%v): %x", completeChan.FundingOutpoint, fundingTxBuf.Bytes()) - err = f.cfg.PublishTransaction(fundingTx, "") + // Set a nil short channel ID at this stage because we do not + // know it until our funding tx confirms. + label := labels.MakeLabel( + labels.LabelTypeChannelOpen, nil, + ) + + err = f.cfg.PublishTransaction(fundingTx, label) if err != nil { fndgLog.Errorf("Unable to broadcast funding tx %x for "+ "ChannelPoint(%v): %v", fundingTxBuf.Bytes(), @@ -2372,6 +2390,25 @@ func (f *fundingManager) handleFundingConfirmation( fndgLog.Errorf("unable to report short chan id: %v", err) } + // If we opened the channel, and lnd's wallet published our funding tx + // (which is not the case for some channels) then we update our + // transaction label with our short channel ID, which is known now that + // our funding transaction has confirmed. We do not label transactions + // we did not publish, because our wallet has no knowledge of them. + if completeChan.IsInitiator && completeChan.ChanType.HasFundingTx() { + shortChanID := completeChan.ShortChanID() + label := labels.MakeLabel( + labels.LabelTypeChannelOpen, &shortChanID, + ) + + err = f.cfg.UpdateLabel( + completeChan.FundingOutpoint.Hash, label, + ) + if err != nil { + fndgLog.Errorf("unable to update label: %v", err) + } + } + // Close the discoverySignal channel, indicating to a separate // goroutine that the channel now is marked as open in the database // and that it is acceptable to process funding locked messages diff --git a/fundingmanager_test.go b/fundingmanager_test.go index 9896563d..c0539959 100644 --- a/fundingmanager_test.go +++ b/fundingmanager_test.go @@ -421,6 +421,9 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey, publTxChan <- txn return nil }, + UpdateLabel: func(chainhash.Hash, string) error { + return nil + }, ZombieSweeperInterval: 1 * time.Hour, ReservationTimeout: 1 * time.Nanosecond, MaxPendingChannels: lncfg.DefaultMaxPendingChannels, @@ -524,6 +527,9 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) { publishChan <- txn return nil }, + UpdateLabel: func(chainhash.Hash, string) error { + return nil + }, ZombieSweeperInterval: oldCfg.ZombieSweeperInterval, ReservationTimeout: oldCfg.ReservationTimeout, OpenChannelPredicate: chainedAcceptor, diff --git a/labels/labels.go b/labels/labels.go index 3bbe5fea..c35faaf7 100644 --- a/labels/labels.go +++ b/labels/labels.go @@ -1,12 +1,24 @@ // Package labels contains labels used to label transactions broadcast by lnd. // These labels are used across packages, so they are declared in a separate // package to avoid dependency issues. +// +// Labels for transactions broadcast by lnd have two set fields followed by an +// optional set labelled data values, all separated by colons. +// - Label version: an integer that indicates the version lnd used +// - Label type: the type of transaction we are labelling +// - {field name}-{value}: a named field followed by its value, these items are +// optional, and there may be more than field present. +// +// For version 0 we have the following optional data fields defined: +// - shortchanid: the short channel ID that a transaction is associated with, +// with its value set to the uint64 short channel id. package labels import ( "fmt" "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightningnetwork/lnd/lnwire" ) // External labels a transaction as user initiated via the api. This @@ -31,3 +43,50 @@ func ValidateAPI(label string) (string, error) { return label, nil } + +// LabelVersion versions our labels so they can be easily update to contain +// new data while still easily string matched. +type LabelVersion uint8 + +// LabelVersionZero is the label version for labels that contain label type and +// channel ID (where available). +const LabelVersionZero LabelVersion = iota + +// LabelType indicates the type of label we are creating. It is a string rather +// than an int for easy string matching and human-readability. +type LabelType string + +const ( + // LabelTypeChannelOpen is used to label channel opens. + LabelTypeChannelOpen LabelType = "openchannel" + + // LabelTypeChannelClose is used to label channel closes. + LabelTypeChannelClose LabelType = "closechannel" + + // LabelTypeJusticeTransaction is used to label justice transactions. + LabelTypeJusticeTransaction LabelType = "justicetx" + + // LabelTypeSweepTransaction is used to label sweeps. + LabelTypeSweepTransaction LabelType = "sweep" +) + +// LabelField is used to tag a value within a label. +type LabelField string + +const ( + // ShortChanID is used to tag short channel id values in our labels. + ShortChanID LabelField = "shortchanid" +) + +// MakeLabel creates a label with the provided type and short channel id. If +// our short channel ID is not known, we simply return version:label_type. If +// we do have a short channel ID set, the label will also contain its value: +// shortchanid-{int64 chan ID}. +func MakeLabel(labelType LabelType, channelID *lnwire.ShortChannelID) string { + if channelID == nil { + return fmt.Sprintf("%v:%v", LabelVersionZero, labelType) + } + + return fmt.Sprintf("%v:%v:%v-%v", LabelVersionZero, labelType, + ShortChanID, channelID.ToUint64()) +} diff --git a/lntest/harness.go b/lntest/harness.go index aa20c3b9..f999f897 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -1204,9 +1204,14 @@ func (n *NetworkHarness) WaitForChannelClose(ctx context.Context, } // AssertChannelExists asserts that an active channel identified by the -// specified channel point exists from the point-of-view of the node. +// specified channel point exists from the point-of-view of the node. It takes +// an optional set of check functions which can be used to make further +// assertions using channel's values. These functions are responsible for +// failing the test themselves if they do not pass. +// nolint: interfacer func (n *NetworkHarness) AssertChannelExists(ctx context.Context, - node *HarnessNode, chanPoint *wire.OutPoint) error { + node *HarnessNode, chanPoint *wire.OutPoint, + checks ...func(*lnrpc.Channel)) error { req := &lnrpc.ListChannelsRequest{} @@ -1218,12 +1223,20 @@ func (n *NetworkHarness) AssertChannelExists(ctx context.Context, for _, channel := range resp.Channels { if channel.ChannelPoint == chanPoint.String() { - if channel.Active { - return nil + // First check whether our channel is active, + // failing early if it is not. + if !channel.Active { + return fmt.Errorf("channel %s inactive", + chanPoint) } - return fmt.Errorf("channel %s inactive", - chanPoint) + // Apply any additional checks that we would + // like to verify. + for _, check := range checks { + check(channel) + } + + return nil } } diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index ee51f47b..023bb3fc 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -35,6 +35,7 @@ import ( "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/labels" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" @@ -2905,6 +2906,7 @@ func testChannelFundingPersistence(net *lntest.NetworkHarness, t *harnessTest) { t.Fatalf("unable to convert funding txid into chainhash.Hash:"+ " %v", err) } + fundingTxStr := fundingTxID.String() // Mine a block, then wait for Alice's node to notify us that the // channel has been opened. The funding transaction should be found @@ -2912,6 +2914,10 @@ func testChannelFundingPersistence(net *lntest.NetworkHarness, t *harnessTest) { block := mineBlocks(t, net, 1, 1)[0] assertTxInBlock(t, block, fundingTxID) + // Get the height that our transaction confirmed at. + _, height, err := net.Miner.Node.GetBestBlock() + require.NoError(t.t, err, "could not get best block") + // Restart both nodes to test that the appropriate state has been // persisted and that both nodes recover gracefully. if err := net.RestartNode(net.Alice, nil); err != nil { @@ -2934,6 +2940,16 @@ func testChannelFundingPersistence(net *lntest.NetworkHarness, t *harnessTest) { t.Fatalf("unable to mine blocks: %v", err) } + // Assert that our wallet has our opening transaction with a label + // that does not have a channel ID set yet, because we have not + // reached our required confirmations. + tx := findTxAtHeight(ctxt, t, height, fundingTxStr, net, net.Alice) + + // At this stage, we expect the transaction to be labelled, but not with + // our channel ID because our transaction has not yet confirmed. + label := labels.MakeLabel(labels.LabelTypeChannelOpen, nil) + require.Equal(t.t, label, tx.Label, "open channel label wrong") + // Both nodes should still show a single channel as pending. time.Sleep(time.Second * 1) ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) @@ -2957,9 +2973,27 @@ func testChannelFundingPersistence(net *lntest.NetworkHarness, t *harnessTest) { Index: pendingUpdate.OutputIndex, } + // Re-lookup our transaction in the block that it confirmed in. + tx = findTxAtHeight(ctxt, t, height, fundingTxStr, net, net.Alice) + + // Create an additional check for our channel assertion that will + // check that our label is as expected. + check := func(channel *lnrpc.Channel) { + shortChanID := lnwire.NewShortChanIDFromInt( + channel.ChanId, + ) + + label := labels.MakeLabel( + labels.LabelTypeChannelOpen, &shortChanID, + ) + require.Equal(t.t, label, tx.Label, + "open channel label not updated") + } + // Check both nodes to ensure that the channel is ready for operation. ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - if err := net.AssertChannelExists(ctxt, net.Alice, &outPoint); err != nil { + err = net.AssertChannelExists(ctxt, net.Alice, &outPoint, check) + if err != nil { t.Fatalf("unable to assert channel existence: %v", err) } ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) @@ -2980,6 +3014,30 @@ func testChannelFundingPersistence(net *lntest.NetworkHarness, t *harnessTest) { closeChannelAndAssert(ctxt, t, net, net.Alice, chanPoint, false) } +// findTxAtHeight gets all of the transactions that a node's wallet has a record +// of at the target height, and finds and returns the tx with the target txid, +// failing if it is not found. +func findTxAtHeight(ctx context.Context, t *harnessTest, height int32, + target string, net *lntest.NetworkHarness, + node *lntest.HarnessNode) *lnrpc.Transaction { + + txns, err := node.LightningClient.GetTransactions( + ctx, &lnrpc.GetTransactionsRequest{ + StartHeight: height, + EndHeight: height, + }, + ) + require.NoError(t.t, err, "could not get transactions") + + for _, tx := range txns.Transactions { + if tx.TxHash == target { + return tx + } + } + + return nil +} + // testChannelBalance creates a new channel between Alice and Bob, then // checks channel balance to be equal amount specified while creation of channel. func testChannelBalance(net *lntest.NetworkHarness, t *harnessTest) { diff --git a/lnwallet/chancloser/chancloser.go b/lnwallet/chancloser/chancloser.go index f1380ef7..0b073bf7 100644 --- a/lnwallet/chancloser/chancloser.go +++ b/lnwallet/chancloser/chancloser.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/htlcswitch" + "github.com/lightningnetwork/lnd/labels" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" @@ -551,7 +552,14 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, return spew.Sdump(closeTx) }), ) - if err := c.cfg.BroadcastTx(closeTx, ""); err != nil { + + // Create a close channel label. + chanID := c.cfg.Channel.ShortChanID() + closeLabel := labels.MakeLabel( + labels.LabelTypeChannelClose, &chanID, + ) + + if err := c.cfg.BroadcastTx(closeTx, closeLabel); err != nil { return nil, false, err } diff --git a/server.go b/server.go index fe711cec..07e7e3e8 100644 --- a/server.go +++ b/server.go @@ -971,8 +971,11 @@ func newServer(cfg *Config, listenAddrs []net.Addr, chanDB *channeldb.DB, IDKey: nodeKeyECDH.PubKey(), Wallet: cc.wallet, PublishTransaction: cc.wallet.PublishTransaction, - Notifier: cc.chainNotifier, - FeeEstimator: cc.feeEstimator, + UpdateLabel: func(hash chainhash.Hash, label string) error { + return cc.wallet.LabelTransaction(hash, label, true) + }, + Notifier: cc.chainNotifier, + FeeEstimator: cc.feeEstimator, SignMessage: func(pubKey *btcec.PublicKey, msg []byte) (input.Signature, error) { diff --git a/utxonursery.go b/utxonursery.go index a1a20794..217aa2da 100644 --- a/utxonursery.go +++ b/utxonursery.go @@ -11,10 +11,10 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/davecgh/go-spew/spew" - "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/labels" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/sweep" ) @@ -867,7 +867,8 @@ func (u *utxoNursery) sweepCribOutput(classHeight uint32, baby *babyOutput) erro // We'll now broadcast the HTLC transaction, then wait for it to be // confirmed before transitioning it to kindergarten. - err := u.cfg.PublishTransaction(baby.timeoutTx, "") + label := labels.MakeLabel(labels.LabelTypeSweepTransaction, nil) + err := u.cfg.PublishTransaction(baby.timeoutTx, label) if err != nil && err != lnwallet.ErrDoubleSpend { utxnLog.Errorf("Unable to broadcast baby tx: "+ "%v, %v", err, spew.Sdump(baby.timeoutTx)) diff --git a/watchtower/lookout/punisher.go b/watchtower/lookout/punisher.go index 98d2b6c6..0c8f8bb8 100644 --- a/watchtower/lookout/punisher.go +++ b/watchtower/lookout/punisher.go @@ -2,6 +2,7 @@ package lookout import ( "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/labels" ) // PunisherConfig houses the resources required by the Punisher. @@ -42,7 +43,8 @@ func (p *BreachPunisher) Punish(desc *JusticeDescriptor, quit <-chan struct{}) e log.Infof("Publishing justice transaction for client=%s with txid=%s", desc.SessionInfo.ID, justiceTxn.TxHash()) - err = p.cfg.PublishTx(justiceTxn, "") + label := labels.MakeLabel(labels.LabelTypeJusticeTransaction, nil) + err = p.cfg.PublishTx(justiceTxn, label) if err != nil { log.Errorf("Unable to publish justice txn for client=%s"+ "with breach-txid=%s: %v",