lnd/chanrestore.go
Boris Nagaev f485e079b7
chanbackup: add backup version for TapscriptRoot
Previous to this change taproot assets channels and simple taproot channels were
considered the same in the context of chanbackup package, since they stored the
same data. In the following commits we are adding the data needed to produce a
signed commitment transaction from a SCB file and in order to do that we need to
add more fields and a custom channel gets one additional field (TapscriptRoot)
compared to a simple taproot channel. So now we have to distinguish these kinds
of channels in chanbackup package.

See PR https://github.com/lightningnetwork/lnd/pull/8183 for more details.
2024-10-11 10:00:42 -03:00

359 lines
12 KiB
Go

package lnd
import (
"fmt"
"math"
"net"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/contractcourt"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/shachain"
)
const (
// mainnetSCBLaunchBlock is the approximate block height of the bitcoin
// mainnet chain of the date when SCBs first were released in lnd
// (v0.6.0-beta). The block date is 4/15/2019, 10:54 PM UTC.
mainnetSCBLaunchBlock = 571800
// testnetSCBLaunchBlock is the approximate block height of the bitcoin
// testnet3 chain of the date when SCBs first were released in lnd
// (v0.6.0-beta). The block date is 4/16/2019, 08:04 AM UTC.
testnetSCBLaunchBlock = 1489300
)
// chanDBRestorer is an implementation of the chanbackup.ChannelRestorer
// interface that is able to properly map a Single backup, into a
// channeldb.ChannelShell which is required to fully restore a channel. We also
// need the secret key chain in order obtain the prior shachain root so we can
// verify the DLP protocol as initiated by the remote node.
type chanDBRestorer struct {
db *channeldb.ChannelStateDB
secretKeys keychain.SecretKeyRing
chainArb *contractcourt.ChainArbitrator
}
// openChannelShell maps the static channel back up into an open channel
// "shell". We say shell as this doesn't include all the information required
// to continue to use the channel, only the minimal amount of information to
// insert this shell channel back into the database.
func (c *chanDBRestorer) openChannelShell(backup chanbackup.Single) (
*channeldb.ChannelShell, error) {
var err error
// Each of the keys in our local channel config only have their
// locators populate, so we'll re-derive the raw key now as we'll need
// it in order to carry out the DLP protocol.
backup.LocalChanCfg.MultiSigKey, err = c.secretKeys.DeriveKey(
backup.LocalChanCfg.MultiSigKey.KeyLocator,
)
if err != nil {
return nil, fmt.Errorf("unable to derive multi sig key: %w",
err)
}
backup.LocalChanCfg.RevocationBasePoint, err = c.secretKeys.DeriveKey(
backup.LocalChanCfg.RevocationBasePoint.KeyLocator,
)
if err != nil {
return nil, fmt.Errorf("unable to derive revocation key: %w",
err)
}
backup.LocalChanCfg.PaymentBasePoint, err = c.secretKeys.DeriveKey(
backup.LocalChanCfg.PaymentBasePoint.KeyLocator,
)
if err != nil {
return nil, fmt.Errorf("unable to derive payment key: %w", err)
}
backup.LocalChanCfg.DelayBasePoint, err = c.secretKeys.DeriveKey(
backup.LocalChanCfg.DelayBasePoint.KeyLocator,
)
if err != nil {
return nil, fmt.Errorf("unable to derive delay key: %w", err)
}
backup.LocalChanCfg.HtlcBasePoint, err = c.secretKeys.DeriveKey(
backup.LocalChanCfg.HtlcBasePoint.KeyLocator,
)
if err != nil {
return nil, fmt.Errorf("unable to derive htlc key: %w", err)
}
// The shachain root that seeds RevocationProducer for this channel.
// It currently has two possible formats.
var revRoot *chainhash.Hash
// If the PubKey field is non-nil, then this shachain root is using the
// legacy non-ECDH scheme.
if backup.ShaChainRootDesc.PubKey != nil {
ltndLog.Debugf("Using legacy revocation producer format for "+
"channel point %v", backup.FundingOutpoint)
// Obtain the private key for the shachain root from the
// encoded public key.
privKey, err := c.secretKeys.DerivePrivKey(
backup.ShaChainRootDesc,
)
if err != nil {
return nil, fmt.Errorf("could not derive private key "+
"for legacy channel revocation root format: "+
"%v", err)
}
revRoot, err = chainhash.NewHash(privKey.Serialize())
if err != nil {
return nil, err
}
} else {
ltndLog.Debugf("Using new ECDH revocation producer format "+
"for channel point %v", backup.FundingOutpoint)
// This is the scheme in which the shachain root is derived via
// an ECDH operation on the private key of ShaChainRootDesc and
// our public multisig key.
ecdh, err := c.secretKeys.ECDH(
backup.ShaChainRootDesc,
backup.LocalChanCfg.MultiSigKey.PubKey,
)
if err != nil {
return nil, fmt.Errorf("unable to derive shachain "+
"root: %v", err)
}
ch := chainhash.Hash(ecdh)
revRoot = &ch
}
shaChainProducer := shachain.NewRevocationProducer(*revRoot)
var chanType channeldb.ChannelType
switch backup.Version {
case chanbackup.DefaultSingleVersion:
chanType = channeldb.SingleFunderBit
case chanbackup.TweaklessCommitVersion:
chanType = channeldb.SingleFunderTweaklessBit
case chanbackup.AnchorsCommitVersion:
chanType = channeldb.AnchorOutputsBit
chanType |= channeldb.SingleFunderTweaklessBit
case chanbackup.AnchorsZeroFeeHtlcTxCommitVersion:
chanType = channeldb.ZeroHtlcTxFeeBit
chanType |= channeldb.AnchorOutputsBit
chanType |= channeldb.SingleFunderTweaklessBit
case chanbackup.ScriptEnforcedLeaseVersion:
chanType = channeldb.LeaseExpirationBit
chanType |= channeldb.ZeroHtlcTxFeeBit
chanType |= channeldb.AnchorOutputsBit
chanType |= channeldb.SingleFunderTweaklessBit
case chanbackup.SimpleTaprootVersion:
chanType = channeldb.ZeroHtlcTxFeeBit
chanType |= channeldb.AnchorOutputsBit
chanType |= channeldb.SingleFunderTweaklessBit
chanType |= channeldb.SimpleTaprootFeatureBit
case chanbackup.TapscriptRootVersion:
chanType = channeldb.ZeroHtlcTxFeeBit
chanType |= channeldb.AnchorOutputsBit
chanType |= channeldb.SingleFunderTweaklessBit
chanType |= channeldb.SimpleTaprootFeatureBit
chanType |= channeldb.TapscriptRootBit
default:
return nil, fmt.Errorf("unknown Single version: %w", err)
}
ltndLog.Infof("SCB Recovery: created channel shell for ChannelPoint"+
"(%v), chan_type=%v", backup.FundingOutpoint, chanType)
chanShell := channeldb.ChannelShell{
NodeAddrs: backup.Addresses,
Chan: &channeldb.OpenChannel{
ChanType: chanType,
ChainHash: backup.ChainHash,
IsInitiator: backup.IsInitiator,
Capacity: backup.Capacity,
FundingOutpoint: backup.FundingOutpoint,
ShortChannelID: backup.ShortChannelID,
IdentityPub: backup.RemoteNodePub,
IsPending: false,
LocalChanCfg: backup.LocalChanCfg,
RemoteChanCfg: backup.RemoteChanCfg,
RemoteCurrentRevocation: backup.RemoteNodePub,
RevocationStore: shachain.NewRevocationStore(),
RevocationProducer: shaChainProducer,
ThawHeight: backup.LeaseExpiry,
},
}
return &chanShell, nil
}
// RestoreChansFromSingles attempts to map the set of single channel backups to
// channel shells that will be stored persistently. Once these shells have been
// stored on disk, we'll be able to connect to the channel peer an execute the
// data loss recovery protocol.
//
// NOTE: Part of the chanbackup.ChannelRestorer interface.
func (c *chanDBRestorer) RestoreChansFromSingles(backups ...chanbackup.Single) error {
channelShells := make([]*channeldb.ChannelShell, 0, len(backups))
firstChanHeight := uint32(math.MaxUint32)
for _, backup := range backups {
chanShell, err := c.openChannelShell(backup)
if err != nil {
return err
}
// Find the block height of the earliest channel in this backup.
chanHeight := chanShell.Chan.ShortChanID().BlockHeight
if chanHeight != 0 && chanHeight < firstChanHeight {
firstChanHeight = chanHeight
}
channelShells = append(channelShells, chanShell)
}
// In case there were only unconfirmed channels, we will have to scan
// the chain beginning from the launch date of SCBs.
if firstChanHeight == math.MaxUint32 {
chainHash := channelShells[0].Chan.ChainHash
switch {
case chainHash.IsEqual(chaincfg.MainNetParams.GenesisHash):
firstChanHeight = mainnetSCBLaunchBlock
case chainHash.IsEqual(chaincfg.TestNet3Params.GenesisHash):
firstChanHeight = testnetSCBLaunchBlock
default:
// Worst case: We have no height hint and start at
// block 1. Should only happen for SCBs in regtest
// and simnet.
firstChanHeight = 1
}
}
// If there were channels in the backup that were not confirmed at the
// time of the backup creation, they won't have a block height in the
// ShortChanID which would lead to an error in the chain watcher.
// We want to at least set the funding broadcast height that the chain
// watcher can use instead. We have two possible fallback values for
// the broadcast height that we are going to try here.
for _, chanShell := range channelShells {
channel := chanShell.Chan
switch {
// Fallback case 1: This is an unconfirmed channel from an old
// backup file where we didn't have any workaround in place and
// the short channel ID is 0:0:0. Best we can do here is set the
// funding broadcast height to a reasonable value that we
// determined earlier.
case channel.ShortChanID().BlockHeight == 0:
channel.SetBroadcastHeight(firstChanHeight)
// Fallback case 2: It is extremely unlikely at this point that
// a channel we are trying to restore has a coinbase funding TX.
// Therefore we can be quite certain that if the TxIndex is
// zero but the block height wasn't, it was an unconfirmed
// channel where we used the BlockHeight to encode the funding
// TX broadcast height. To not end up with an invalid short
// channel ID that looks valid, we restore the "original"
// unconfirmed one here.
case channel.ShortChannelID.TxIndex == 0:
broadcastHeight := channel.ShortChannelID.BlockHeight
channel.SetBroadcastHeight(broadcastHeight)
channel.ShortChannelID.BlockHeight = 0
}
}
ltndLog.Infof("Inserting %v SCB channel shells into DB",
len(channelShells))
// Now that we have all the backups mapped into a series of Singles,
// we'll insert them all into the database.
if err := c.db.RestoreChannelShells(channelShells...); err != nil {
return err
}
ltndLog.Infof("Informing chain watchers of new restored channels")
// Finally, we'll need to inform the chain arbitrator of these new
// channels so we'll properly watch for their ultimate closure on chain
// and sweep them via the DLP.
for _, restoredChannel := range channelShells {
err := c.chainArb.WatchNewChannel(restoredChannel.Chan)
if err != nil {
return err
}
}
return nil
}
// A compile-time constraint to ensure chanDBRestorer implements
// chanbackup.ChannelRestorer.
var _ chanbackup.ChannelRestorer = (*chanDBRestorer)(nil)
// ConnectPeer attempts to connect to the target node at the set of available
// addresses. Once this method returns with a non-nil error, the connector
// should attempt to persistently connect to the target peer in the background
// as a persistent attempt.
//
// NOTE: Part of the chanbackup.PeerConnector interface.
func (s *server) ConnectPeer(nodePub *btcec.PublicKey, addrs []net.Addr) error {
// Before we connect to the remote peer, we'll remove any connections
// to ensure the new connection is created after this new link/channel
// is known.
if err := s.DisconnectPeer(nodePub); err != nil {
ltndLog.Infof("Peer(%v) is already connected, proceeding "+
"with chan restore", nodePub.SerializeCompressed())
}
// For each of the known addresses, we'll attempt to launch a
// persistent connection to the (pub, addr) pair. In the event that any
// of them connect, all the other stale requests will be canceled.
for _, addr := range addrs {
netAddr := &lnwire.NetAddress{
IdentityKey: nodePub,
Address: addr,
}
ltndLog.Infof("Attempting to connect to %v for SCB restore "+
"DLP", netAddr)
// Attempt to connect to the peer using this full address. If
// we're unable to connect to them, then we'll try the next
// address in place of it.
err := s.ConnectToPeer(netAddr, true, s.cfg.ConnectionTimeout)
// If we're already connected to this peer, then we don't
// consider this an error, so we'll exit here.
if _, ok := err.(*errPeerAlreadyConnected); ok {
return nil
} else if err != nil {
// Otherwise, something else happened, so we'll try the
// next address.
ltndLog.Errorf("unable to connect to %v to "+
"complete SCB restore: %v", netAddr, err)
continue
}
// If we connected no problem, then we can exit early as our
// job here is done.
return nil
}
return fmt.Errorf("unable to connect to peer %x for SCB restore",
nodePub.SerializeCompressed())
}