mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 01:43:16 +01:00
d95c1f93f3
In this commit, we add a new AuxLeafStore which can be used to dynamically fetch the latest aux leaves for a given state. This is useful for custom channel types that will store some extra information in the form of a custom blob, then will use that information to derive the new leaf tapscript leaves that may be attached to reach state.
2734 lines
89 KiB
Go
2734 lines
89 KiB
Go
package lnwallet
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/btcsuite/btcd/blockchain"
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
|
"github.com/btcsuite/btcd/btcutil/txsort"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcwallet/wallet"
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/fn"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/keychain"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chanvalidate"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/shachain"
|
|
)
|
|
|
|
const (
|
|
// The size of the buffered queue of requests to the wallet from the
|
|
// outside word.
|
|
msgBufferSize = 100
|
|
|
|
// AnchorChanReservedValue is the amount we'll keep around in the
|
|
// wallet in case we have to fee bump anchor channels on force close.
|
|
// TODO(halseth): update constant to target a specific commit size at
|
|
// set fee rate.
|
|
AnchorChanReservedValue = btcutil.Amount(10_000)
|
|
|
|
// MaxAnchorChanReservedValue is the maximum value we'll reserve for
|
|
// anchor channel fee bumping. We cap it at 10 times the per-channel
|
|
// amount such that nodes with a high number of channels don't have to
|
|
// keep around a very large amount for the unlikely scenario that they
|
|
// all close at the same time.
|
|
MaxAnchorChanReservedValue = 10 * AnchorChanReservedValue
|
|
)
|
|
|
|
var (
|
|
// ErrPsbtFundingRequired is the error that is returned during the
|
|
// contribution handling process if the process should be paused for
|
|
// the construction of a PSBT outside of lnd's wallet.
|
|
ErrPsbtFundingRequired = errors.New("PSBT funding required")
|
|
|
|
// ErrReservedValueInvalidated is returned if we try to publish a
|
|
// transaction that would take the walletbalance below what we require
|
|
// to keep around to fee bump our open anchor channels.
|
|
ErrReservedValueInvalidated = errors.New("reserved wallet balance " +
|
|
"invalidated: transaction would leave insufficient funds for " +
|
|
"fee bumping anchor channel closings (see debug log for details)")
|
|
|
|
// ErrEmptyPendingChanID is returned when an empty value is used for
|
|
// the pending channel ID.
|
|
ErrEmptyPendingChanID = errors.New("pending channel ID is empty")
|
|
|
|
// ErrDuplicatePendingChanID is returned when an existing pending
|
|
// channel ID is registered again.
|
|
ErrDuplicatePendingChanID = errors.New("duplicate pending channel ID")
|
|
)
|
|
|
|
// PsbtFundingRequired is a type that implements the error interface and
|
|
// contains the information needed to construct a PSBT.
|
|
type PsbtFundingRequired struct {
|
|
// Intent is the pending PSBT funding intent that needs to be funded
|
|
// if the wrapping error is returned.
|
|
Intent *chanfunding.PsbtIntent
|
|
}
|
|
|
|
// Error returns the underlying error.
|
|
//
|
|
// NOTE: This method is part of the error interface.
|
|
func (p *PsbtFundingRequired) Error() string {
|
|
return ErrPsbtFundingRequired.Error()
|
|
}
|
|
|
|
// InitFundingReserveMsg is the first message sent to initiate the workflow
|
|
// required to open a payment channel with a remote peer. The initial required
|
|
// parameters are configurable across channels. These parameters are to be
|
|
// chosen depending on the fee climate within the network, and time value of
|
|
// funds to be locked up within the channel. Upon success a ChannelReservation
|
|
// will be created in order to track the lifetime of this pending channel.
|
|
// Outputs selected will be 'locked', making them unavailable, for any other
|
|
// pending reservations. Therefore, all channels in reservation limbo will be
|
|
// periodically timed out after an idle period in order to avoid "exhaustion"
|
|
// attacks.
|
|
type InitFundingReserveMsg struct {
|
|
// ChainHash denotes that chain to be used to ultimately open the
|
|
// target channel.
|
|
ChainHash *chainhash.Hash
|
|
|
|
// PendingChanID is the pending channel ID for this funding flow as
|
|
// used in the wire protocol.
|
|
PendingChanID [32]byte
|
|
|
|
// NodeID is the ID of the remote node we would like to open a channel
|
|
// with.
|
|
NodeID *btcec.PublicKey
|
|
|
|
// NodeAddr is the address port that we used to either establish or
|
|
// accept the connection which led to the negotiation of this funding
|
|
// workflow.
|
|
NodeAddr net.Addr
|
|
|
|
// SubtractFees should be set if we intend to spend exactly
|
|
// LocalFundingAmt when opening the channel, subtracting the fees from
|
|
// the funding output. This can be used for instance to use all our
|
|
// remaining funds to open the channel, since it will take fees into
|
|
// account.
|
|
SubtractFees bool
|
|
|
|
// LocalFundingAmt is the amount of funds requested from us for this
|
|
// channel.
|
|
LocalFundingAmt btcutil.Amount
|
|
|
|
// RemoteFundingAmnt is the amount of funds the remote will contribute
|
|
// to this channel.
|
|
RemoteFundingAmt btcutil.Amount
|
|
|
|
// FundUpToMaxAmt defines if channel funding should try to add as many
|
|
// funds to the channel opening as possible up to this amount. If used,
|
|
// then MinFundAmt is treated as the minimum amount of funds that must
|
|
// be available to open the channel. If set to zero it is ignored.
|
|
FundUpToMaxAmt btcutil.Amount
|
|
|
|
// MinFundAmt denotes the minimum channel capacity that has to be
|
|
// allocated iff the FundUpToMaxAmt is set.
|
|
MinFundAmt btcutil.Amount
|
|
|
|
// Outpoints is a list of client-selected outpoints that should be used
|
|
// for funding a channel. If LocalFundingAmt is specified then this
|
|
// amount is allocated from the sum of outpoints towards funding. If the
|
|
// FundUpToMaxAmt is specified the entirety of selected funds is
|
|
// allocated towards channel funding.
|
|
Outpoints []wire.OutPoint
|
|
|
|
// RemoteChanReserve is the channel reserve we required for the remote
|
|
// peer.
|
|
RemoteChanReserve btcutil.Amount
|
|
|
|
// CommitFeePerKw is the starting accepted satoshis/Kw fee for the set
|
|
// of initial commitment transactions. In order to ensure timely
|
|
// confirmation, it is recommended that this fee should be generous,
|
|
// paying some multiple of the accepted base fee rate of the network.
|
|
CommitFeePerKw chainfee.SatPerKWeight
|
|
|
|
// FundingFeePerKw is the fee rate in sat/kw to use for the initial
|
|
// funding transaction.
|
|
FundingFeePerKw chainfee.SatPerKWeight
|
|
|
|
// PushMSat is the number of milli-satoshis that should be pushed over
|
|
// the responder as part of the initial channel creation.
|
|
PushMSat lnwire.MilliSatoshi
|
|
|
|
// Flags are the channel flags specified by the initiator in the
|
|
// open_channel message.
|
|
Flags lnwire.FundingFlag
|
|
|
|
// MinConfs indicates the minimum number of confirmations that each
|
|
// output selected to fund the channel should satisfy.
|
|
MinConfs int32
|
|
|
|
// CommitType indicates what type of commitment type the channel should
|
|
// be using, like tweakless or anchors.
|
|
CommitType CommitmentType
|
|
|
|
// ChanFunder is an optional channel funder that allows the caller to
|
|
// control exactly how the channel funding is carried out. If not
|
|
// specified, then the default chanfunding.WalletAssembler will be
|
|
// used.
|
|
ChanFunder chanfunding.Assembler
|
|
|
|
// AllowUtxoForFunding enables the channel funding workflow to restrict
|
|
// the selection of utxos when selecting the inputs for the channel
|
|
// opening. This does ONLY apply for the internal wallet backed channel
|
|
// opening case.
|
|
//
|
|
// NOTE: This is very useful when opening channels with unconfirmed
|
|
// inputs to make sure stable non-replaceable inputs are used.
|
|
AllowUtxoForFunding func(Utxo) bool
|
|
|
|
// ZeroConf is a boolean that is true if a zero-conf channel was
|
|
// negotiated.
|
|
ZeroConf bool
|
|
|
|
// OptionScidAlias is a boolean that is true if an option-scid-alias
|
|
// channel type was explicitly negotiated.
|
|
OptionScidAlias bool
|
|
|
|
// ScidAliasFeature is true if the option-scid-alias feature bit was
|
|
// negotiated.
|
|
ScidAliasFeature bool
|
|
|
|
// Memo is any arbitrary information we wish to store locally about the
|
|
// channel that will be useful to our future selves.
|
|
Memo []byte
|
|
|
|
// TapscriptRoot is the root of the tapscript tree that will be used to
|
|
// create the funding output. This is an optional field that should
|
|
// only be set for taproot channels.
|
|
TapscriptRoot fn.Option[chainhash.Hash]
|
|
|
|
// err is a channel in which all errors will be sent across. Will be
|
|
// nil if this initial set is successful.
|
|
//
|
|
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
|
err chan error
|
|
|
|
// resp is channel in which a ChannelReservation with our contributions
|
|
// filled in will be sent across this channel in the case of a
|
|
// successfully reservation initiation. In the case of an error, this
|
|
// will read a nil pointer.
|
|
//
|
|
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
|
resp chan *ChannelReservation
|
|
}
|
|
|
|
// fundingReserveCancelMsg is a message reserved for cancelling an existing
|
|
// channel reservation identified by its reservation ID. Cancelling a reservation
|
|
// frees its locked outputs up, for inclusion within further reservations.
|
|
type fundingReserveCancelMsg struct {
|
|
pendingFundingID uint64
|
|
|
|
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
|
err chan error // Buffered
|
|
}
|
|
|
|
// addContributionMsg represents a message executing the second phase of the
|
|
// channel reservation workflow. This message carries the counterparty's
|
|
// "contribution" to the payment channel. In the case that this message is
|
|
// processed without generating any errors, then channel reservation will then
|
|
// be able to construct the funding tx, both commitment transactions, and
|
|
// finally generate signatures for all our inputs to the funding transaction,
|
|
// and for the remote node's version of the commitment transaction.
|
|
type addContributionMsg struct {
|
|
pendingFundingID uint64
|
|
|
|
// TODO(roasbeef): Should also carry SPV proofs in we're in SPV mode
|
|
contribution *ChannelContribution
|
|
|
|
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
|
err chan error
|
|
}
|
|
|
|
// continueContributionMsg represents a message that signals that the
|
|
// interrupted funding process involving a PSBT can now be continued because the
|
|
// finalized transaction is now available.
|
|
type continueContributionMsg struct {
|
|
pendingFundingID uint64
|
|
|
|
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
|
err chan error
|
|
}
|
|
|
|
// addSingleContributionMsg represents a message executing the second phase of
|
|
// a single funder channel reservation workflow. This messages carries the
|
|
// counterparty's "contribution" to the payment channel. As this message is
|
|
// sent when on the responding side to a single funder workflow, no further
|
|
// action apart from storing the provided contribution is carried out.
|
|
type addSingleContributionMsg struct {
|
|
pendingFundingID uint64
|
|
|
|
contribution *ChannelContribution
|
|
|
|
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
|
err chan error
|
|
}
|
|
|
|
// addCounterPartySigsMsg represents the final message required to complete,
|
|
// and 'open' a payment channel. This message carries the counterparty's
|
|
// signatures for each of their inputs to the funding transaction, and also a
|
|
// signature allowing us to spend our version of the commitment transaction.
|
|
// If we're able to verify all the signatures are valid, the funding transaction
|
|
// will be broadcast to the network. After the funding transaction gains a
|
|
// configurable number of confirmations, the channel is officially considered
|
|
// 'open'.
|
|
type addCounterPartySigsMsg struct {
|
|
pendingFundingID uint64
|
|
|
|
// Should be order of sorted inputs that are theirs. Sorting is done
|
|
// in accordance to BIP-69:
|
|
// https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki.
|
|
theirFundingInputScripts []*input.Script
|
|
|
|
// This should be 1/2 of the signatures needed to successfully spend our
|
|
// version of the commitment transaction.
|
|
theirCommitmentSig input.Signature
|
|
|
|
// This channel is used to return the completed channel after the wallet
|
|
// has completed all of its stages in the funding process.
|
|
completeChan chan *channeldb.OpenChannel
|
|
|
|
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
|
err chan error
|
|
}
|
|
|
|
// addSingleFunderSigsMsg represents the next-to-last message required to
|
|
// complete a single-funder channel workflow. Once the initiator is able to
|
|
// construct the funding transaction, they send both the outpoint and a
|
|
// signature for our version of the commitment transaction. Once this message
|
|
// is processed we (the responder) are able to construct both commitment
|
|
// transactions, signing the remote party's version.
|
|
type addSingleFunderSigsMsg struct {
|
|
pendingFundingID uint64
|
|
|
|
// fundingOutpoint is the outpoint of the completed funding
|
|
// transaction as assembled by the workflow initiator.
|
|
fundingOutpoint *wire.OutPoint
|
|
|
|
// theirCommitmentSig are the 1/2 of the signatures needed to
|
|
// successfully spend our version of the commitment transaction.
|
|
theirCommitmentSig input.Signature
|
|
|
|
// This channel is used to return the completed channel after the wallet
|
|
// has completed all of its stages in the funding process.
|
|
completeChan chan *channeldb.OpenChannel
|
|
|
|
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
|
err chan error
|
|
}
|
|
|
|
// CheckReservedValueTxReq is the request struct used to call
|
|
// CheckReservedValueTx with. It contains the transaction to check as well as
|
|
// an optional explicitly defined index to denote a change output that is not
|
|
// watched by the wallet.
|
|
type CheckReservedValueTxReq struct {
|
|
// Tx is the transaction to check the outputs for.
|
|
Tx *wire.MsgTx
|
|
|
|
// ChangeIndex denotes an optional output index that can be explicitly
|
|
// set for a change that is not being watched by the wallet and would
|
|
// otherwise not be recognized as a change output.
|
|
ChangeIndex *int
|
|
}
|
|
|
|
// LightningWallet is a domain specific, yet general Bitcoin wallet capable of
|
|
// executing workflow required to interact with the Lightning Network. It is
|
|
// domain specific in the sense that it understands all the fancy scripts used
|
|
// within the Lightning Network, channel lifetimes, etc. However, it embeds a
|
|
// general purpose Bitcoin wallet within it. Therefore, it is also able to
|
|
// serve as a regular Bitcoin wallet which uses HD keys. The wallet is highly
|
|
// concurrent internally. All communication, and requests towards the wallet
|
|
// are dispatched as messages over channels, ensuring thread safety across all
|
|
// operations. Interaction has been designed independent of any peer-to-peer
|
|
// communication protocol, allowing the wallet to be self-contained and
|
|
// embeddable within future projects interacting with the Lightning Network.
|
|
//
|
|
// NOTE: At the moment the wallet requires a btcd full node, as it's dependent
|
|
// on btcd's websockets notifications as event triggers during the lifetime of a
|
|
// channel. However, once the chainntnfs package is complete, the wallet will
|
|
// be compatible with multiple RPC/notification services such as Electrum,
|
|
// Bitcoin Core + ZeroMQ, etc. Eventually, the wallet won't require a full-node
|
|
// at all, as SPV support is integrated into btcwallet.
|
|
type LightningWallet struct {
|
|
started int32 // To be used atomically.
|
|
shutdown int32 // To be used atomically.
|
|
|
|
nextFundingID uint64 // To be used atomically.
|
|
|
|
// Cfg is the configuration struct that will be used by the wallet to
|
|
// access the necessary interfaces and default it needs to carry on its
|
|
// duties.
|
|
Cfg Config
|
|
|
|
// WalletController is the core wallet, all non Lightning Network
|
|
// specific interaction is proxied to the internal wallet.
|
|
WalletController
|
|
|
|
// SecretKeyRing is the interface we'll use to derive any keys related
|
|
// to our purpose within the network including: multi-sig keys, node
|
|
// keys, revocation keys, etc.
|
|
keychain.SecretKeyRing
|
|
|
|
// This mutex MUST be held when performing coin selection in order to
|
|
// avoid inadvertently creating multiple funding transaction which
|
|
// double spend inputs across each other.
|
|
coinSelectMtx sync.RWMutex
|
|
|
|
// All messages to the wallet are to be sent across this channel.
|
|
msgChan chan interface{}
|
|
|
|
// Incomplete payment channels are stored in the map below. An intent
|
|
// to create a payment channel is tracked as a "reservation" within
|
|
// limbo. Once the final signatures have been exchanged, a reservation
|
|
// is removed from limbo. Each reservation is tracked by a unique
|
|
// monotonically integer. All requests concerning the channel MUST
|
|
// carry a valid, active funding ID.
|
|
fundingLimbo map[uint64]*ChannelReservation
|
|
|
|
// reservationIDs maps a pending channel ID to the reservation ID used
|
|
// as key in the fundingLimbo map. Used to easily look up a channel
|
|
// reservation given a pending channel ID.
|
|
reservationIDs map[[32]byte]uint64
|
|
limboMtx sync.RWMutex
|
|
|
|
// lockedOutPoints is a set of the currently locked outpoint. This
|
|
// information is kept in order to provide an easy way to unlock all
|
|
// the currently locked outpoints.
|
|
lockedOutPoints map[wire.OutPoint]struct{}
|
|
|
|
// fundingIntents houses all the "interception" registered by a caller
|
|
// using the RegisterFundingIntent method.
|
|
intentMtx sync.RWMutex
|
|
fundingIntents map[[32]byte]chanfunding.Intent
|
|
|
|
quit chan struct{}
|
|
|
|
wg sync.WaitGroup
|
|
|
|
// TODO(roasbeef): handle wallet lock/unlock
|
|
}
|
|
|
|
// NewLightningWallet creates/opens and initializes a LightningWallet instance.
|
|
// If the wallet has never been created (according to the passed dataDir), first-time
|
|
// setup is executed.
|
|
func NewLightningWallet(Cfg Config) (*LightningWallet, error) {
|
|
|
|
return &LightningWallet{
|
|
Cfg: Cfg,
|
|
SecretKeyRing: Cfg.SecretKeyRing,
|
|
WalletController: Cfg.WalletController,
|
|
msgChan: make(chan interface{}, msgBufferSize),
|
|
nextFundingID: 0,
|
|
fundingLimbo: make(map[uint64]*ChannelReservation),
|
|
reservationIDs: make(map[[32]byte]uint64),
|
|
lockedOutPoints: make(map[wire.OutPoint]struct{}),
|
|
fundingIntents: make(map[[32]byte]chanfunding.Intent),
|
|
quit: make(chan struct{}),
|
|
}, nil
|
|
}
|
|
|
|
// Startup establishes a connection to the RPC source, and spins up all
|
|
// goroutines required to handle incoming messages.
|
|
func (l *LightningWallet) Startup() error {
|
|
// Already started?
|
|
if atomic.AddInt32(&l.started, 1) != 1 {
|
|
return nil
|
|
}
|
|
|
|
// Start the underlying wallet controller.
|
|
if err := l.Start(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if l.Cfg.Rebroadcaster != nil {
|
|
go func() {
|
|
if err := l.Cfg.Rebroadcaster.Start(); err != nil {
|
|
walletLog.Errorf("unable to start "+
|
|
"rebroadcaster: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
l.wg.Add(1)
|
|
// TODO(roasbeef): multiple request handlers?
|
|
go l.requestHandler()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Shutdown gracefully stops the wallet, and all active goroutines.
|
|
func (l *LightningWallet) Shutdown() error {
|
|
if atomic.AddInt32(&l.shutdown, 1) != 1 {
|
|
return nil
|
|
}
|
|
|
|
// Signal the underlying wallet controller to shutdown, waiting until
|
|
// all active goroutines have been shutdown.
|
|
if err := l.Stop(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if l.Cfg.Rebroadcaster != nil && l.Cfg.Rebroadcaster.Started() {
|
|
l.Cfg.Rebroadcaster.Stop()
|
|
}
|
|
|
|
close(l.quit)
|
|
l.wg.Wait()
|
|
return nil
|
|
}
|
|
|
|
// PublishTransaction wraps the wallet controller tx publish method with an
|
|
// extra rebroadcaster layer if the sub-system is configured.
|
|
func (l *LightningWallet) PublishTransaction(tx *wire.MsgTx,
|
|
label string) error {
|
|
|
|
sendTxToWallet := func() error {
|
|
return l.WalletController.PublishTransaction(tx, label)
|
|
}
|
|
|
|
// If we don't have rebroadcaster then we can exit early (and send only
|
|
// to the wallet).
|
|
if l.Cfg.Rebroadcaster == nil || !l.Cfg.Rebroadcaster.Started() {
|
|
return sendTxToWallet()
|
|
}
|
|
|
|
// We pass this into the rebroadcaster first, so the initial attempt
|
|
// will succeed if the transaction isn't yet in the mempool. However we
|
|
// ignore the error here as this might be resent on start up and the
|
|
// transaction already exists.
|
|
_ = l.Cfg.Rebroadcaster.Broadcast(tx)
|
|
|
|
// Then we pass things into the wallet as normal, which'll add the
|
|
// transaction label on disk.
|
|
if err := sendTxToWallet(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO(roasbeef): want diff height actually? no context though
|
|
_, bestHeight, err := l.Cfg.ChainIO.GetBestBlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txHash := tx.TxHash()
|
|
go func() {
|
|
const numConfs = 6
|
|
|
|
txConf, err := l.Cfg.Notifier.RegisterConfirmationsNtfn(
|
|
&txHash, tx.TxOut[0].PkScript, numConfs,
|
|
uint32(bestHeight),
|
|
)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case <-txConf.Confirmed:
|
|
// TODO(roasbeef): also want to remove from
|
|
// rebroadcaster if conflict happens...deeper wallet
|
|
// integration?
|
|
l.Cfg.Rebroadcaster.MarkAsConfirmed(tx.TxHash())
|
|
|
|
case <-l.quit:
|
|
return
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// ConfirmedBalance returns the current confirmed balance of a wallet account.
|
|
// This methods wraps the internal WalletController method so we're able to
|
|
// properly hold the coin select mutex while we compute the balance.
|
|
func (l *LightningWallet) ConfirmedBalance(confs int32,
|
|
account string) (btcutil.Amount, error) {
|
|
|
|
l.coinSelectMtx.Lock()
|
|
defer l.coinSelectMtx.Unlock()
|
|
|
|
return l.WalletController.ConfirmedBalance(confs, account)
|
|
}
|
|
|
|
// ListUnspentWitnessFromDefaultAccount returns all unspent outputs from the
|
|
// default wallet account which are version 0 witness programs. The 'minConfs'
|
|
// and 'maxConfs' parameters indicate the minimum and maximum number of
|
|
// confirmations an output needs in order to be returned by this method. Passing
|
|
// -1 as 'minConfs' indicates that even unconfirmed outputs should be returned.
|
|
// Using MaxInt32 as 'maxConfs' implies returning all outputs with at least
|
|
// 'minConfs'.
|
|
//
|
|
// NOTE: This method requires the global coin selection lock to be held.
|
|
func (l *LightningWallet) ListUnspentWitnessFromDefaultAccount(
|
|
minConfs, maxConfs int32) ([]*Utxo, error) {
|
|
|
|
return l.WalletController.ListUnspentWitness(
|
|
minConfs, maxConfs, DefaultAccountName,
|
|
)
|
|
}
|
|
|
|
// LockedOutpoints returns a list of all currently locked outpoint.
|
|
func (l *LightningWallet) LockedOutpoints() []*wire.OutPoint {
|
|
outPoints := make([]*wire.OutPoint, 0, len(l.lockedOutPoints))
|
|
for outPoint := range l.lockedOutPoints {
|
|
outPoint := outPoint
|
|
|
|
outPoints = append(outPoints, &outPoint)
|
|
}
|
|
|
|
return outPoints
|
|
}
|
|
|
|
// ResetReservations reset the volatile wallet state which tracks all currently
|
|
// active reservations.
|
|
func (l *LightningWallet) ResetReservations() {
|
|
l.nextFundingID = 0
|
|
l.fundingLimbo = make(map[uint64]*ChannelReservation)
|
|
l.reservationIDs = make(map[[32]byte]uint64)
|
|
|
|
for outpoint := range l.lockedOutPoints {
|
|
_ = l.ReleaseOutput(chanfunding.LndInternalLockID, outpoint)
|
|
}
|
|
l.lockedOutPoints = make(map[wire.OutPoint]struct{})
|
|
}
|
|
|
|
// ActiveReservations returns a slice of all the currently active
|
|
// (non-canceled) reservations.
|
|
func (l *LightningWallet) ActiveReservations() []*ChannelReservation {
|
|
reservations := make([]*ChannelReservation, 0, len(l.fundingLimbo))
|
|
for _, reservation := range l.fundingLimbo {
|
|
reservations = append(reservations, reservation)
|
|
}
|
|
|
|
return reservations
|
|
}
|
|
|
|
// requestHandler is the primary goroutine(s) responsible for handling, and
|
|
// dispatching replies to all messages.
|
|
func (l *LightningWallet) requestHandler() {
|
|
defer l.wg.Done()
|
|
|
|
out:
|
|
for {
|
|
select {
|
|
case m := <-l.msgChan:
|
|
switch msg := m.(type) {
|
|
case *InitFundingReserveMsg:
|
|
l.handleFundingReserveRequest(msg)
|
|
case *fundingReserveCancelMsg:
|
|
l.handleFundingCancelRequest(msg)
|
|
case *addSingleContributionMsg:
|
|
l.handleSingleContribution(msg)
|
|
case *addContributionMsg:
|
|
l.handleContributionMsg(msg)
|
|
case *continueContributionMsg:
|
|
l.handleChanPointReady(msg)
|
|
case *addSingleFunderSigsMsg:
|
|
l.handleSingleFunderSigs(msg)
|
|
case *addCounterPartySigsMsg:
|
|
l.handleFundingCounterPartySigs(msg)
|
|
}
|
|
case <-l.quit:
|
|
// TODO: do some clean up
|
|
break out
|
|
}
|
|
}
|
|
}
|
|
|
|
// InitChannelReservation kicks off the 3-step workflow required to successfully
|
|
// open a payment channel with a remote node. As part of the funding
|
|
// reservation, the inputs selected for the funding transaction are 'locked'.
|
|
// This ensures that multiple channel reservations aren't double spending the
|
|
// same inputs in the funding transaction. If reservation initialization is
|
|
// successful, a ChannelReservation containing our completed contribution is
|
|
// returned. Our contribution contains all the items necessary to allow the
|
|
// counterparty to build the funding transaction, and both versions of the
|
|
// commitment transaction. Otherwise, an error occurred and a nil pointer along
|
|
// with an error are returned.
|
|
//
|
|
// Once a ChannelReservation has been obtained, two additional steps must be
|
|
// processed before a payment channel can be considered 'open'. The second step
|
|
// validates, and processes the counterparty's channel contribution. The third,
|
|
// and final step verifies all signatures for the inputs of the funding
|
|
// transaction, and that the signature we record for our version of the
|
|
// commitment transaction is valid.
|
|
func (l *LightningWallet) InitChannelReservation(
|
|
req *InitFundingReserveMsg) (*ChannelReservation, error) {
|
|
|
|
req.resp = make(chan *ChannelReservation, 1)
|
|
req.err = make(chan error, 1)
|
|
|
|
select {
|
|
case l.msgChan <- req:
|
|
case <-l.quit:
|
|
return nil, errors.New("wallet shutting down")
|
|
}
|
|
|
|
return <-req.resp, <-req.err
|
|
}
|
|
|
|
// RegisterFundingIntent allows a caller to signal to the wallet that if a
|
|
// pending channel ID of expectedID is found, then it can skip constructing a
|
|
// new chanfunding.Assembler, and instead use the specified chanfunding.Intent.
|
|
// As an example, this lets some of the parameters for funding transaction to
|
|
// be negotiated outside the regular funding protocol.
|
|
func (l *LightningWallet) RegisterFundingIntent(expectedID [32]byte,
|
|
shimIntent chanfunding.Intent) error {
|
|
|
|
l.intentMtx.Lock()
|
|
defer l.intentMtx.Unlock()
|
|
|
|
// Sanity check the pending channel ID is not empty.
|
|
var zeroID [32]byte
|
|
if expectedID == zeroID {
|
|
return ErrEmptyPendingChanID
|
|
}
|
|
|
|
if _, ok := l.fundingIntents[expectedID]; ok {
|
|
return fmt.Errorf("%w: already has intent registered: %v",
|
|
ErrDuplicatePendingChanID, expectedID[:])
|
|
}
|
|
|
|
l.fundingIntents[expectedID] = shimIntent
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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.
|
|
func (l *LightningWallet) PsbtFundingVerify(pendingChanID [32]byte,
|
|
packet *psbt.Packet, skipFinalize bool) error {
|
|
|
|
l.intentMtx.Lock()
|
|
defer l.intentMtx.Unlock()
|
|
|
|
intent, ok := l.fundingIntents[pendingChanID]
|
|
if !ok {
|
|
return fmt.Errorf("no funding intent found for "+
|
|
"pendingChannelID(%x)", pendingChanID[:])
|
|
}
|
|
psbtIntent, ok := intent.(*chanfunding.PsbtIntent)
|
|
if !ok {
|
|
return fmt.Errorf("incompatible funding intent")
|
|
}
|
|
|
|
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: %w", err)
|
|
}
|
|
|
|
// Get the channel reservation for that corresponds to this pending
|
|
// channel ID.
|
|
l.limboMtx.Lock()
|
|
pid, ok := l.reservationIDs[pendingChanID]
|
|
if !ok {
|
|
l.limboMtx.Unlock()
|
|
return fmt.Errorf("no channel reservation found for "+
|
|
"pendingChannelID(%x)", pendingChanID[:])
|
|
}
|
|
|
|
pendingReservation, ok := l.fundingLimbo[pid]
|
|
l.limboMtx.Unlock()
|
|
|
|
if !ok {
|
|
return fmt.Errorf("no channel reservation found for "+
|
|
"reservation ID %v", pid)
|
|
}
|
|
|
|
// Now the PSBT has been populated and verified, we can again check
|
|
// whether the value reserved for anchor fee bumping is respected.
|
|
isPublic := pendingReservation.partialState.ChannelFlags&lnwire.FFAnnounceChannel != 0
|
|
hasAnchors := pendingReservation.partialState.ChanType.HasAnchors()
|
|
return l.enforceNewReservedValue(intent, isPublic, hasAnchors)
|
|
}
|
|
|
|
// PsbtFundingFinalize looks up a previously registered funding intent by its
|
|
// pending channel ID and tries to advance the state machine by finalizing the
|
|
// passed PSBT.
|
|
func (l *LightningWallet) PsbtFundingFinalize(pid [32]byte, packet *psbt.Packet,
|
|
rawTx *wire.MsgTx) error {
|
|
|
|
l.intentMtx.Lock()
|
|
defer l.intentMtx.Unlock()
|
|
|
|
intent, ok := l.fundingIntents[pid]
|
|
if !ok {
|
|
return fmt.Errorf("no funding intent found for "+
|
|
"pendingChannelID(%x)", pid[:])
|
|
}
|
|
psbtIntent, ok := intent.(*chanfunding.PsbtIntent)
|
|
if !ok {
|
|
return fmt.Errorf("incompatible funding intent")
|
|
}
|
|
|
|
// Either the PSBT or the raw TX must be set.
|
|
switch {
|
|
case packet != nil && rawTx == nil:
|
|
err := psbtIntent.Finalize(packet)
|
|
if err != nil {
|
|
return fmt.Errorf("error finalizing PSBT: %w", err)
|
|
}
|
|
|
|
case rawTx != nil && packet == nil:
|
|
err := psbtIntent.FinalizeRawTX(rawTx)
|
|
if err != nil {
|
|
return fmt.Errorf("error finalizing raw TX: %w", err)
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("either a PSBT or raw TX must be specified")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CancelFundingIntent allows a caller to cancel a previously registered
|
|
// funding intent. If no intent was found, then an error will be returned.
|
|
func (l *LightningWallet) CancelFundingIntent(pid [32]byte) error {
|
|
l.intentMtx.Lock()
|
|
defer l.intentMtx.Unlock()
|
|
|
|
intent, ok := l.fundingIntents[pid]
|
|
if !ok {
|
|
return fmt.Errorf("no funding intent found for "+
|
|
"pendingChannelID(%x)", pid[:])
|
|
}
|
|
|
|
// Give the intent a chance to clean up after itself, removing coin
|
|
// locks or similar reserved resources.
|
|
intent.Cancel()
|
|
|
|
delete(l.fundingIntents, pid)
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleFundingReserveRequest processes a message intending to create, and
|
|
// validate a funding reservation request.
|
|
func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg) {
|
|
|
|
noFundsCommitted := req.LocalFundingAmt == 0 &&
|
|
req.RemoteFundingAmt == 0 && req.FundUpToMaxAmt == 0
|
|
|
|
// It isn't possible to create a channel with zero funds committed.
|
|
if noFundsCommitted {
|
|
err := ErrZeroCapacity()
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
|
|
// If the funding request is for a different chain than the one the
|
|
// wallet is aware of, then we'll reject the request.
|
|
if !bytes.Equal(l.Cfg.NetParams.GenesisHash[:], req.ChainHash[:]) {
|
|
err := ErrChainMismatch(
|
|
l.Cfg.NetParams.GenesisHash, req.ChainHash,
|
|
)
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
|
|
// We need to avoid enforcing reserved value in the middle of PSBT
|
|
// funding because some of the following steps may add UTXOs funding
|
|
// the on-chain wallet.
|
|
// The enforcement still happens at the last step - in PsbtFundingVerify
|
|
enforceNewReservedValue := true
|
|
|
|
// If no chanFunder was provided, then we'll assume the default
|
|
// assembler, which is backed by the wallet's internal coin selection.
|
|
if req.ChanFunder == nil {
|
|
// We use the P2WSH dust limit since it is larger than the
|
|
// P2WPKH dust limit and to avoid threading through two
|
|
// different dust limits.
|
|
cfg := chanfunding.WalletConfig{
|
|
CoinSource: NewCoinSource(
|
|
l, req.AllowUtxoForFunding,
|
|
),
|
|
CoinSelectLocker: l,
|
|
CoinLeaser: l,
|
|
Signer: l.Cfg.Signer,
|
|
DustLimit: DustLimitForSize(
|
|
input.P2WSHSize,
|
|
),
|
|
CoinSelectionStrategy: l.Cfg.CoinSelectionStrategy,
|
|
}
|
|
req.ChanFunder = chanfunding.NewWalletAssembler(cfg)
|
|
} else {
|
|
_, isPsbtFunder := req.ChanFunder.(*chanfunding.PsbtAssembler)
|
|
enforceNewReservedValue = !isPsbtFunder
|
|
}
|
|
|
|
localFundingAmt := req.LocalFundingAmt
|
|
remoteFundingAmt := req.RemoteFundingAmt
|
|
hasAnchors := req.CommitType.HasAnchors()
|
|
|
|
var (
|
|
fundingIntent chanfunding.Intent
|
|
err error
|
|
)
|
|
|
|
// If we've just received an inbound funding request that we have a
|
|
// registered shim intent to, then we'll obtain the backing intent now.
|
|
// In this case, we're doing a special funding workflow that allows
|
|
// more advanced constructions such as channel factories to be
|
|
// instantiated.
|
|
l.intentMtx.Lock()
|
|
fundingIntent, ok := l.fundingIntents[req.PendingChanID]
|
|
l.intentMtx.Unlock()
|
|
|
|
// Otherwise, this is a normal funding flow, so we'll use the chan
|
|
// funder in the attached request to provision the inputs/outputs
|
|
// that'll ultimately be used to construct the funding transaction.
|
|
if !ok {
|
|
var err error
|
|
var numAnchorChans int
|
|
|
|
// Get the number of anchor channels to determine if there is a
|
|
// reserved value that must be respected when funding up to the
|
|
// maximum amount. Since private channels (most likely) won't be
|
|
// used for routing other than the last hop, they bear a smaller
|
|
// risk that we must force close them in order to resolve a HTLC
|
|
// up/downstream. Hence we exclude them from the count of anchor
|
|
// channels in order to attribute the respective anchor amount
|
|
// to the channel capacity.
|
|
if req.FundUpToMaxAmt > 0 && req.MinFundAmt > 0 {
|
|
numAnchorChans, err = l.CurrentNumAnchorChans()
|
|
if err != nil {
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
|
|
isPublic := req.Flags&lnwire.FFAnnounceChannel != 0
|
|
if hasAnchors && isPublic {
|
|
numAnchorChans++
|
|
}
|
|
}
|
|
|
|
// Coin selection is done on the basis of sat/kw, so we'll use
|
|
// the fee rate passed in to perform coin selection.
|
|
fundingReq := &chanfunding.Request{
|
|
RemoteAmt: req.RemoteFundingAmt,
|
|
LocalAmt: req.LocalFundingAmt,
|
|
FundUpToMaxAmt: req.FundUpToMaxAmt,
|
|
MinFundAmt: req.MinFundAmt,
|
|
RemoteChanReserve: req.RemoteChanReserve,
|
|
PushAmt: lnwire.MilliSatoshi.ToSatoshis(
|
|
req.PushMSat,
|
|
),
|
|
WalletReserve: l.RequiredReserve(
|
|
uint32(numAnchorChans),
|
|
),
|
|
Outpoints: req.Outpoints,
|
|
MinConfs: req.MinConfs,
|
|
SubtractFees: req.SubtractFees,
|
|
FeeRate: req.FundingFeePerKw,
|
|
ChangeAddr: func() (btcutil.Address, error) {
|
|
return l.NewAddress(
|
|
TaprootPubkey, true, DefaultAccountName,
|
|
)
|
|
},
|
|
Musig2: req.CommitType == CommitmentTypeSimpleTaproot,
|
|
}
|
|
fundingIntent, err = req.ChanFunder.ProvisionChannel(
|
|
fundingReq,
|
|
)
|
|
if err != nil {
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
|
|
// Register the funding intent now in case we need to access it
|
|
// again later, as it's the case for the PSBT state machine for
|
|
// example.
|
|
err = l.RegisterFundingIntent(req.PendingChanID, fundingIntent)
|
|
if err != nil {
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
|
|
walletLog.Debugf("Registered funding intent for "+
|
|
"PendingChanID: %x", req.PendingChanID)
|
|
|
|
localFundingAmt = fundingIntent.LocalFundingAmt()
|
|
remoteFundingAmt = fundingIntent.RemoteFundingAmt()
|
|
}
|
|
|
|
// At this point there _has_ to be a funding intent, otherwise something
|
|
// went really wrong.
|
|
if fundingIntent == nil {
|
|
req.err <- fmt.Errorf("no funding intent present")
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
|
|
// If this is a shim intent, then it may be attempting to use an
|
|
// existing set of keys for the funding workflow. In this case, we'll
|
|
// make a simple wrapper keychain.KeyRing that will proxy certain
|
|
// derivation calls to future callers.
|
|
var (
|
|
keyRing keychain.KeyRing = l.SecretKeyRing
|
|
thawHeight uint32
|
|
)
|
|
if shimIntent, ok := fundingIntent.(*chanfunding.ShimIntent); ok {
|
|
keyRing = &shimKeyRing{
|
|
KeyRing: keyRing,
|
|
ShimIntent: shimIntent,
|
|
}
|
|
|
|
// As this was a registered shim intent, we'll obtain the thaw
|
|
// height of the intent, if present at all. If this is
|
|
// non-zero, then we'll mark this as the proper channel type.
|
|
thawHeight = shimIntent.ThawHeight()
|
|
}
|
|
|
|
// Now that we have a funding intent, we'll check whether funding a
|
|
// channel using it would violate our reserved value for anchor channel
|
|
// fee bumping.
|
|
//
|
|
// Check the reserved value using the inputs and outputs given by the
|
|
// intent. Note that for the PSBT intent type we don't yet have the
|
|
// funding tx ready, so this will always pass. We'll do another check
|
|
// when the PSBT has been verified.
|
|
isPublic := req.Flags&lnwire.FFAnnounceChannel != 0
|
|
if enforceNewReservedValue {
|
|
err = l.enforceNewReservedValue(fundingIntent, isPublic, hasAnchors)
|
|
if err != nil {
|
|
fundingIntent.Cancel()
|
|
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
}
|
|
|
|
// The total channel capacity will be the size of the funding output we
|
|
// created plus the remote contribution.
|
|
capacity := localFundingAmt + remoteFundingAmt
|
|
|
|
id := atomic.AddUint64(&l.nextFundingID, 1)
|
|
reservation, err := NewChannelReservation(
|
|
capacity, localFundingAmt, l, id, l.Cfg.NetParams.GenesisHash,
|
|
thawHeight, req,
|
|
)
|
|
if err != nil {
|
|
fundingIntent.Cancel()
|
|
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
|
|
err = l.initOurContribution(
|
|
reservation, fundingIntent, req.NodeAddr, req.NodeID, keyRing,
|
|
)
|
|
if err != nil {
|
|
fundingIntent.Cancel()
|
|
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
|
|
// Create a limbo and record entry for this newly pending funding
|
|
// request.
|
|
l.limboMtx.Lock()
|
|
l.fundingLimbo[id] = reservation
|
|
l.reservationIDs[req.PendingChanID] = id
|
|
l.limboMtx.Unlock()
|
|
|
|
// Funding reservation request successfully handled. The funding inputs
|
|
// will be marked as unavailable until the reservation is either
|
|
// completed, or canceled.
|
|
req.resp <- reservation
|
|
req.err <- nil
|
|
|
|
walletLog.Debugf("Successfully handled funding reservation with "+
|
|
"pendingChanID: %x, reservationID: %v",
|
|
reservation.pendingChanID, reservation.reservationID)
|
|
}
|
|
|
|
// enforceReservedValue enforces that the wallet, upon a new channel being
|
|
// opened, meets the minimum amount of funds required for each advertised anchor
|
|
// channel.
|
|
//
|
|
// We only enforce the reserve if we are contributing funds to the channel. This
|
|
// is done to still allow incoming channels even though we have no UTXOs
|
|
// available, as in bootstrapping phases.
|
|
func (l *LightningWallet) enforceNewReservedValue(fundingIntent chanfunding.Intent,
|
|
isPublic, hasAnchors bool) error {
|
|
|
|
// Only enforce the reserve when an advertised channel is being opened
|
|
// in which we are contributing funds to. This ensures we never dip
|
|
// below the reserve.
|
|
if !isPublic || fundingIntent.LocalFundingAmt() == 0 {
|
|
return nil
|
|
}
|
|
|
|
numAnchors, err := l.CurrentNumAnchorChans()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add the to-be-opened channel.
|
|
if hasAnchors {
|
|
numAnchors++
|
|
}
|
|
|
|
return l.WithCoinSelectLock(func() error {
|
|
_, err := l.CheckReservedValue(
|
|
fundingIntent.Inputs(), fundingIntent.Outputs(),
|
|
numAnchors,
|
|
)
|
|
return err
|
|
})
|
|
}
|
|
|
|
// CurrentNumAnchorChans returns the current number of non-private anchor
|
|
// channels the wallet should be ready to fee bump if needed.
|
|
func (l *LightningWallet) CurrentNumAnchorChans() (int, error) {
|
|
// Count all anchor channels that are open or pending
|
|
// open, or waiting close.
|
|
chans, err := l.Cfg.Database.FetchAllChannels()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var numAnchors int
|
|
cntChannel := func(c *channeldb.OpenChannel) {
|
|
// We skip private channels, as we assume they won't be used
|
|
// for routing.
|
|
if c.ChannelFlags&lnwire.FFAnnounceChannel == 0 {
|
|
return
|
|
}
|
|
|
|
// Count anchor channels.
|
|
if c.ChanType.HasAnchors() {
|
|
numAnchors++
|
|
}
|
|
}
|
|
|
|
for _, c := range chans {
|
|
cntChannel(c)
|
|
}
|
|
|
|
// We also count pending close channels.
|
|
pendingClosed, err := l.Cfg.Database.FetchClosedChannels(
|
|
true,
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
for _, c := range pendingClosed {
|
|
c, err := l.Cfg.Database.FetchHistoricalChannel(
|
|
&c.ChanPoint,
|
|
)
|
|
if err != nil {
|
|
// We don't have a guarantee that all channels re found
|
|
// in the historical channels bucket, so we continue.
|
|
walletLog.Warnf("Unable to fetch historical "+
|
|
"channel: %v", err)
|
|
continue
|
|
}
|
|
|
|
cntChannel(c)
|
|
}
|
|
|
|
return numAnchors, nil
|
|
}
|
|
|
|
// CheckReservedValue checks whether publishing a transaction with the given
|
|
// inputs and outputs would violate the value we reserve in the wallet for
|
|
// bumping the fee of anchor channels. The numAnchorChans argument should be
|
|
// set the number of open anchor channels controlled by the wallet after
|
|
// the transaction has been published.
|
|
//
|
|
// If the reserved value is violated, the returned error will be
|
|
// ErrReservedValueInvalidated. The method will also return the current
|
|
// reserved value, both in case of success and in case of
|
|
// ErrReservedValueInvalidated.
|
|
//
|
|
// NOTE: This method should only be run with the CoinSelectLock held.
|
|
func (l *LightningWallet) CheckReservedValue(in []wire.OutPoint,
|
|
out []*wire.TxOut, numAnchorChans int) (btcutil.Amount, error) {
|
|
|
|
// Get all unspent coins in the wallet. We only care about those part of
|
|
// the wallet's default account as we know we can readily sign for those
|
|
// at any time.
|
|
witnessOutputs, err := l.ListUnspentWitnessFromDefaultAccount(
|
|
0, math.MaxInt32,
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
ourInput := make(map[wire.OutPoint]struct{})
|
|
for _, op := range in {
|
|
ourInput[op] = struct{}{}
|
|
}
|
|
|
|
// When crafting a transaction with inputs from the wallet, these coins
|
|
// will usually be locked in the process, and not be returned when
|
|
// listing unspents. In this case they have already been deducted from
|
|
// the wallet balance. In case they haven't been properly locked, we
|
|
// check whether they are still listed among our unspents and deduct
|
|
// them.
|
|
var walletBalance btcutil.Amount
|
|
for _, in := range witnessOutputs {
|
|
// Spending an unlocked wallet UTXO, don't add it to the
|
|
// balance.
|
|
if _, ok := ourInput[in.OutPoint]; ok {
|
|
continue
|
|
}
|
|
|
|
walletBalance += in.Value
|
|
}
|
|
|
|
// Now we go through the outputs of the transaction, if any of the
|
|
// outputs are paying into the wallet (likely a change output), we add
|
|
// it to our final balance.
|
|
for _, txOut := range out {
|
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
|
txOut.PkScript, &l.Cfg.NetParams,
|
|
)
|
|
if err != nil {
|
|
// Non-standard outputs can safely be skipped because
|
|
// they're not supported by the wallet.
|
|
continue
|
|
}
|
|
|
|
for _, addr := range addrs {
|
|
if !l.IsOurAddress(addr) {
|
|
continue
|
|
}
|
|
|
|
walletBalance += btcutil.Amount(txOut.Value)
|
|
|
|
// We break since we don't want to double count the output.
|
|
break
|
|
}
|
|
}
|
|
|
|
// We reserve a given amount for each anchor channel.
|
|
reserved := l.RequiredReserve(uint32(numAnchorChans))
|
|
|
|
if walletBalance < reserved {
|
|
walletLog.Debugf("Reserved value=%v above final "+
|
|
"walletbalance=%v with %d anchor channels open",
|
|
reserved, walletBalance, numAnchorChans)
|
|
return reserved, ErrReservedValueInvalidated
|
|
}
|
|
|
|
return reserved, nil
|
|
}
|
|
|
|
// CheckReservedValueTx calls CheckReservedValue with the inputs and outputs
|
|
// from the given tx, with the number of anchor channels currently open in the
|
|
// database.
|
|
//
|
|
// NOTE: This method should only be run with the CoinSelectLock held.
|
|
func (l *LightningWallet) CheckReservedValueTx(req CheckReservedValueTxReq) (
|
|
btcutil.Amount, error) {
|
|
|
|
numAnchors, err := l.CurrentNumAnchorChans()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var inputs []wire.OutPoint
|
|
for _, txIn := range req.Tx.TxIn {
|
|
inputs = append(inputs, txIn.PreviousOutPoint)
|
|
}
|
|
|
|
reservedVal, err := l.CheckReservedValue(
|
|
inputs, req.Tx.TxOut, numAnchors,
|
|
)
|
|
switch {
|
|
// If the error returned from CheckReservedValue is
|
|
// ErrReservedValueInvalidated, then it did nonetheless return
|
|
// the required reserved value and we check for the optional
|
|
// change index.
|
|
case errors.Is(err, ErrReservedValueInvalidated):
|
|
// Without a change index provided there is nothing more to
|
|
// check and the error is returned.
|
|
if req.ChangeIndex == nil {
|
|
return reservedVal, err
|
|
}
|
|
|
|
// If a change index was provided we make only sure that it
|
|
// would leave sufficient funds for the reserved balance value.
|
|
//
|
|
// Note: This is used if a change output index is explicitly set
|
|
// but that may not be watched by the wallet and therefore is
|
|
// not picked up by the call to CheckReservedValue above.
|
|
chIdx := *req.ChangeIndex
|
|
if chIdx < 0 || chIdx >= len(req.Tx.TxOut) ||
|
|
req.Tx.TxOut[chIdx].Value < int64(reservedVal) {
|
|
|
|
return reservedVal, err
|
|
}
|
|
|
|
case err != nil:
|
|
return reservedVal, err
|
|
}
|
|
|
|
return reservedVal, nil
|
|
}
|
|
|
|
// initOurContribution initializes the given ChannelReservation with our coins
|
|
// and change reserved for the channel, and derives the keys to use for this
|
|
// channel.
|
|
func (l *LightningWallet) initOurContribution(reservation *ChannelReservation,
|
|
fundingIntent chanfunding.Intent, nodeAddr net.Addr,
|
|
nodeID *btcec.PublicKey, keyRing keychain.KeyRing) error {
|
|
|
|
// Grab the mutex on the ChannelReservation to ensure thread-safety
|
|
reservation.Lock()
|
|
defer reservation.Unlock()
|
|
|
|
// At this point, if we have a funding intent, we'll use it to populate
|
|
// the existing reservation state entries for our coin selection.
|
|
if fundingIntent != nil {
|
|
if intent, ok := fundingIntent.(*chanfunding.FullIntent); ok {
|
|
for _, coin := range intent.InputCoins {
|
|
reservation.ourContribution.Inputs = append(
|
|
reservation.ourContribution.Inputs,
|
|
&wire.TxIn{
|
|
PreviousOutPoint: coin.OutPoint,
|
|
},
|
|
)
|
|
}
|
|
reservation.ourContribution.ChangeOutputs = intent.ChangeOutputs
|
|
}
|
|
|
|
reservation.fundingIntent = fundingIntent
|
|
}
|
|
|
|
reservation.nodeAddr = nodeAddr
|
|
reservation.partialState.IdentityPub = nodeID
|
|
|
|
var err error
|
|
reservation.ourContribution.MultiSigKey, err = keyRing.DeriveNextKey(
|
|
keychain.KeyFamilyMultiSig,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reservation.ourContribution.RevocationBasePoint, err = keyRing.DeriveNextKey(
|
|
keychain.KeyFamilyRevocationBase,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reservation.ourContribution.HtlcBasePoint, err = keyRing.DeriveNextKey(
|
|
keychain.KeyFamilyHtlcBase,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reservation.ourContribution.PaymentBasePoint, err = keyRing.DeriveNextKey(
|
|
keychain.KeyFamilyPaymentBase,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reservation.ourContribution.DelayBasePoint, err = keyRing.DeriveNextKey(
|
|
keychain.KeyFamilyDelayBase,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// With the above keys created, we'll also need to initialize our
|
|
// revocation tree state, and from that generate the per-commitment
|
|
// point.
|
|
producer, taprootNonceProducer, err := l.nextRevocationProducer(
|
|
reservation, keyRing,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
firstPreimage, err := producer.AtIndex(0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reservation.ourContribution.FirstCommitmentPoint = input.ComputeCommitmentPoint(
|
|
firstPreimage[:],
|
|
)
|
|
|
|
reservation.partialState.RevocationProducer = producer
|
|
reservation.ourContribution.CommitmentParams.DustLimit =
|
|
DustLimitUnknownWitness()
|
|
|
|
// If taproot channels are active, then we'll generate our verification
|
|
// nonce here. We'll use this nonce to verify the signature for our
|
|
// local commitment transaction. If we need to force close, then this
|
|
// is also what'll be used to sign that transaction.
|
|
if reservation.partialState.ChanType.IsTaproot() {
|
|
firstNoncePreimage, err := taprootNonceProducer.AtIndex(0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// As we'd like the local nonce we send over to be generated
|
|
// deterministically, we'll provide a custom reader that
|
|
// actually just uses our sha-chain pre-image as the primary
|
|
// randomness source.
|
|
shaChainRand := musig2.WithCustomRand(
|
|
bytes.NewBuffer(firstNoncePreimage[:]),
|
|
)
|
|
pubKeyOpt := musig2.WithPublicKey(
|
|
reservation.ourContribution.MultiSigKey.PubKey,
|
|
)
|
|
reservation.ourContribution.LocalNonce, err = musig2.GenNonces(
|
|
pubKeyOpt, shaChainRand,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleFundingReserveCancel cancels an existing channel reservation. As part
|
|
// of the cancellation, outputs previously selected as inputs for the funding
|
|
// transaction via coin selection are freed allowing future reservations to
|
|
// include them.
|
|
func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMsg) {
|
|
// TODO(roasbeef): holding lock too long
|
|
l.limboMtx.Lock()
|
|
defer l.limboMtx.Unlock()
|
|
|
|
pendingReservation, ok := l.fundingLimbo[req.pendingFundingID]
|
|
if !ok {
|
|
// TODO(roasbeef): make new error, "unknown funding state" or something
|
|
req.err <- fmt.Errorf("attempted to cancel non-existent funding state")
|
|
return
|
|
}
|
|
|
|
// Grab the mutex on the ChannelReservation to ensure thread-safety
|
|
pendingReservation.Lock()
|
|
defer pendingReservation.Unlock()
|
|
|
|
// Mark all previously locked outpoints as usable for future funding
|
|
// requests.
|
|
for _, unusedInput := range pendingReservation.ourContribution.Inputs {
|
|
delete(l.lockedOutPoints, unusedInput.PreviousOutPoint)
|
|
_ = l.ReleaseOutput(
|
|
chanfunding.LndInternalLockID,
|
|
unusedInput.PreviousOutPoint,
|
|
)
|
|
}
|
|
|
|
// TODO(roasbeef): is it even worth it to keep track of unused keys?
|
|
|
|
// TODO(roasbeef): Is it possible to mark the unused change also as
|
|
// available?
|
|
|
|
delete(l.fundingLimbo, req.pendingFundingID)
|
|
|
|
pid := pendingReservation.pendingChanID
|
|
delete(l.reservationIDs, pid)
|
|
|
|
l.intentMtx.Lock()
|
|
if intent, ok := l.fundingIntents[pid]; ok {
|
|
intent.Cancel()
|
|
|
|
delete(l.fundingIntents, pendingReservation.pendingChanID)
|
|
}
|
|
l.intentMtx.Unlock()
|
|
|
|
req.err <- nil
|
|
}
|
|
|
|
// createCommitOpts is a struct that holds the options for creating a new
|
|
// commitment transaction.
|
|
type createCommitOpts struct {
|
|
auxLeaves fn.Option[CommitAuxLeaves]
|
|
}
|
|
|
|
// defaultCommitOpts returns a new createCommitOpts with default values.
|
|
func defaultCommitOpts() createCommitOpts {
|
|
return createCommitOpts{}
|
|
}
|
|
|
|
// CreateCommitOpt is a functional option that can be used to modify the way a
|
|
// new commitment transaction is created.
|
|
type CreateCommitOpt func(*createCommitOpts)
|
|
|
|
// CreateCommitmentTxns is a helper function that creates the initial
|
|
// commitment transaction for both parties. This function is used during the
|
|
// initial funding workflow as both sides must generate a signature for the
|
|
// remote party's commitment transaction, and verify the signature for their
|
|
// version of the commitment transaction.
|
|
func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount,
|
|
ourChanCfg, theirChanCfg *channeldb.ChannelConfig,
|
|
localCommitPoint, remoteCommitPoint *btcec.PublicKey,
|
|
fundingTxIn wire.TxIn, chanType channeldb.ChannelType, initiator bool,
|
|
leaseExpiry uint32, opts ...CreateCommitOpt) (*wire.MsgTx, *wire.MsgTx,
|
|
error) {
|
|
|
|
options := defaultCommitOpts()
|
|
for _, optFunc := range opts {
|
|
optFunc(&options)
|
|
}
|
|
|
|
localCommitmentKeys := DeriveCommitmentKeys(
|
|
localCommitPoint, lntypes.Local, chanType, ourChanCfg,
|
|
theirChanCfg,
|
|
)
|
|
remoteCommitmentKeys := DeriveCommitmentKeys(
|
|
remoteCommitPoint, lntypes.Remote, chanType, ourChanCfg,
|
|
theirChanCfg,
|
|
)
|
|
|
|
ourCommitTx, err := CreateCommitTx(
|
|
chanType, fundingTxIn, localCommitmentKeys, ourChanCfg,
|
|
theirChanCfg, localBalance, remoteBalance, 0, initiator,
|
|
leaseExpiry, options.auxLeaves,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
otxn := btcutil.NewTx(ourCommitTx)
|
|
if err := blockchain.CheckTransactionSanity(otxn); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
theirCommitTx, err := CreateCommitTx(
|
|
chanType, fundingTxIn, remoteCommitmentKeys, theirChanCfg,
|
|
ourChanCfg, remoteBalance, localBalance, 0, !initiator,
|
|
leaseExpiry, options.auxLeaves,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
ttxn := btcutil.NewTx(theirCommitTx)
|
|
if err := blockchain.CheckTransactionSanity(ttxn); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return ourCommitTx, theirCommitTx, nil
|
|
}
|
|
|
|
// handleContributionMsg processes the second workflow step for the lifetime of
|
|
// a channel reservation. Upon completion, the reservation will carry a
|
|
// completed funding transaction (minus the counterparty's input signatures),
|
|
// both versions of the commitment transaction, and our signature for their
|
|
// version of the commitment transaction.
|
|
func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
|
|
|
|
l.limboMtx.Lock()
|
|
pendingReservation, ok := l.fundingLimbo[req.pendingFundingID]
|
|
l.limboMtx.Unlock()
|
|
if !ok {
|
|
req.err <- fmt.Errorf("attempted to update non-existent funding state")
|
|
return
|
|
}
|
|
|
|
// Grab the mutex on the ChannelReservation to ensure thread-safety
|
|
pendingReservation.Lock()
|
|
defer pendingReservation.Unlock()
|
|
|
|
// If UpfrontShutdownScript is set, validate that it is a valid script.
|
|
shutdown := req.contribution.UpfrontShutdown
|
|
if len(shutdown) > 0 {
|
|
// Validate the shutdown script.
|
|
if !ValidateUpfrontShutdown(shutdown, &l.Cfg.NetParams) {
|
|
req.err <- fmt.Errorf("invalid shutdown script")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Some temporary variables to cut down on the resolution verbosity.
|
|
pendingReservation.theirContribution = req.contribution
|
|
theirContribution := req.contribution
|
|
ourContribution := pendingReservation.ourContribution
|
|
|
|
// Perform bounds-checking on both ChannelReserve and DustLimit
|
|
// parameters.
|
|
if !pendingReservation.validateReserveBounds() {
|
|
req.err <- fmt.Errorf("invalid reserve and dust bounds")
|
|
return
|
|
}
|
|
|
|
var (
|
|
chanPoint *wire.OutPoint
|
|
err error
|
|
)
|
|
|
|
// At this point, we can now construct our channel point. Depending on
|
|
// which type of intent we obtained from our chanfunding.Assembler,
|
|
// we'll carry out a distinct set of steps.
|
|
switch fundingIntent := pendingReservation.fundingIntent.(type) {
|
|
// The transaction was created outside of the wallet and might already
|
|
// be published. Nothing left to do other than using the correct
|
|
// outpoint.
|
|
case *chanfunding.ShimIntent:
|
|
chanPoint, err = fundingIntent.ChanPoint()
|
|
if err != nil {
|
|
req.err <- fmt.Errorf("unable to obtain chan point: %w",
|
|
err)
|
|
return
|
|
}
|
|
|
|
pendingReservation.partialState.FundingOutpoint = *chanPoint
|
|
|
|
// The user has signaled that they want to use a PSBT to construct the
|
|
// funding transaction. Because we now have the multisig keys from both
|
|
// parties, we can create the multisig script that needs to be funded
|
|
// and then pause the process until the user supplies the PSBT
|
|
// containing the eventual funding transaction.
|
|
case *chanfunding.PsbtIntent:
|
|
if fundingIntent.PendingPsbt != nil {
|
|
req.err <- fmt.Errorf("PSBT funding already in" +
|
|
"progress")
|
|
return
|
|
}
|
|
|
|
// Now that we know our contribution, we can bind both the local
|
|
// and remote key which will be needed to calculate the multisig
|
|
// funding output in a next step.
|
|
pendingChanID := pendingReservation.pendingChanID
|
|
walletLog.Debugf("Advancing PSBT funding flow for "+
|
|
"pending_id(%x), binding keys local_key=%v, "+
|
|
"remote_key=%x", pendingChanID,
|
|
&ourContribution.MultiSigKey,
|
|
theirContribution.MultiSigKey.PubKey.SerializeCompressed())
|
|
fundingIntent.BindKeys(
|
|
&ourContribution.MultiSigKey,
|
|
theirContribution.MultiSigKey.PubKey,
|
|
)
|
|
|
|
// Exit early because we can't continue the funding flow yet.
|
|
req.err <- &PsbtFundingRequired{
|
|
Intent: fundingIntent,
|
|
}
|
|
return
|
|
|
|
case *chanfunding.FullIntent:
|
|
// Now that we know their public key, we can bind theirs as
|
|
// well as ours to the funding intent.
|
|
fundingIntent.BindKeys(
|
|
&pendingReservation.ourContribution.MultiSigKey,
|
|
theirContribution.MultiSigKey.PubKey,
|
|
)
|
|
|
|
// With our keys bound, we can now construct+sign the final
|
|
// funding transaction and also obtain the chanPoint that
|
|
// creates the channel.
|
|
fundingTx, err := fundingIntent.CompileFundingTx(
|
|
theirContribution.Inputs,
|
|
theirContribution.ChangeOutputs,
|
|
)
|
|
if err != nil {
|
|
req.err <- fmt.Errorf("unable to construct funding "+
|
|
"tx: %v", err)
|
|
return
|
|
}
|
|
chanPoint, err = fundingIntent.ChanPoint()
|
|
if err != nil {
|
|
req.err <- fmt.Errorf("unable to obtain chan "+
|
|
"point: %v", err)
|
|
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 = *chanPoint
|
|
pendingReservation.ourFundingInputScripts = make(
|
|
[]*input.Script, 0, len(ourContribution.Inputs),
|
|
)
|
|
for _, txIn := range fundingTx.TxIn {
|
|
_, err := l.FetchOutpointInfo(&txIn.PreviousOutPoint)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
pendingReservation.ourFundingInputScripts = append(
|
|
pendingReservation.ourFundingInputScripts,
|
|
&input.Script{
|
|
Witness: txIn.Witness,
|
|
SigScript: txIn.SignatureScript,
|
|
},
|
|
)
|
|
}
|
|
|
|
walletLog.Tracef("Funding tx for ChannelPoint(%v) "+
|
|
"generated: %v", chanPoint, spew.Sdump(fundingTx))
|
|
}
|
|
|
|
// If we landed here and didn't exit early, it means we already have
|
|
// the channel point ready. We can jump directly to the next step.
|
|
l.handleChanPointReady(&continueContributionMsg{
|
|
pendingFundingID: req.pendingFundingID,
|
|
err: req.err,
|
|
})
|
|
}
|
|
|
|
// genMusigSession generates a new musig2 pair session that we can use to sign
|
|
// the commitment transaction for the remote party, and verify their incoming
|
|
// partial signature.
|
|
func genMusigSession(ourContribution, theirContribution *ChannelContribution,
|
|
signer input.MuSig2Signer,
|
|
fundingOutput *wire.TxOut) *MusigPairSession {
|
|
|
|
return NewMusigPairSession(&MusigSessionCfg{
|
|
LocalKey: ourContribution.MultiSigKey,
|
|
RemoteKey: theirContribution.MultiSigKey,
|
|
LocalNonce: *ourContribution.LocalNonce,
|
|
RemoteNonce: *theirContribution.LocalNonce,
|
|
Signer: signer,
|
|
InputTxOut: fundingOutput,
|
|
})
|
|
}
|
|
|
|
// signCommitTx generates a valid input.Signature to send to the remote party
|
|
// for their version of the commitment transaction. For regular channels, this
|
|
// will be a normal ECDSA signature. For taproot channels, this will instead be
|
|
// a musig2 partial signature that also includes the nonce used to generate it.
|
|
func (l *LightningWallet) signCommitTx(pendingReservation *ChannelReservation,
|
|
commitTx *wire.MsgTx, fundingOutput *wire.TxOut,
|
|
fundingWitnessScript []byte) (input.Signature, error) {
|
|
|
|
ourContribution := pendingReservation.ourContribution
|
|
theirContribution := pendingReservation.theirContribution
|
|
|
|
var (
|
|
sigTheirCommit input.Signature
|
|
err error
|
|
)
|
|
switch {
|
|
// For regular channels, we can just send over a normal ECDSA signature
|
|
// w/o any extra steps.
|
|
case !pendingReservation.partialState.ChanType.IsTaproot():
|
|
ourKey := ourContribution.MultiSigKey
|
|
signDesc := input.SignDescriptor{
|
|
WitnessScript: fundingWitnessScript,
|
|
KeyDesc: ourKey,
|
|
Output: fundingOutput,
|
|
HashType: txscript.SigHashAll,
|
|
SigHashes: input.NewTxSigHashesV0Only(
|
|
commitTx,
|
|
),
|
|
InputIndex: 0,
|
|
}
|
|
sigTheirCommit, err = l.Cfg.Signer.SignOutputRaw(
|
|
commitTx, &signDesc,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If this is a taproot channel, then we'll need to create an initial
|
|
// musig2 session here as we'll be sending over a _partial_ signature.
|
|
default:
|
|
// We're now ready to sign the first commitment. However, we'll
|
|
// only create the session if that hasn't been done already.
|
|
if pendingReservation.musigSessions == nil {
|
|
musigSessions := genMusigSession(
|
|
ourContribution, theirContribution,
|
|
l.Cfg.Signer, fundingOutput,
|
|
)
|
|
pendingReservation.musigSessions = musigSessions
|
|
}
|
|
|
|
// Now that we have the funding outpoint, we'll generate a
|
|
// musig2 signature for their version of the commitment
|
|
// transaction. We use the remote session as this is for the
|
|
// remote commitment transaction.
|
|
musigSessions := pendingReservation.musigSessions
|
|
partialSig, err := musigSessions.RemoteSession.SignCommit(
|
|
commitTx,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to sign "+
|
|
"commitment: %w", err)
|
|
}
|
|
|
|
sigTheirCommit = partialSig
|
|
}
|
|
|
|
return sigTheirCommit, nil
|
|
}
|
|
|
|
// handleChanPointReady continues the funding process once the channel point is
|
|
// known and the funding transaction can be completed.
|
|
func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
|
|
l.limboMtx.Lock()
|
|
pendingReservation, ok := l.fundingLimbo[req.pendingFundingID]
|
|
l.limboMtx.Unlock()
|
|
if !ok {
|
|
req.err <- fmt.Errorf("attempted to update non-existent " +
|
|
"funding state")
|
|
return
|
|
}
|
|
|
|
ourContribution := pendingReservation.ourContribution
|
|
theirContribution := pendingReservation.theirContribution
|
|
chanPoint := pendingReservation.partialState.FundingOutpoint
|
|
|
|
// If we're in the PSBT funding flow, we now should have everything that
|
|
// 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 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 "+
|
|
"tx: %v", err)
|
|
return
|
|
}
|
|
chanPointPtr, err := psbtIntent.ChanPoint()
|
|
if err != nil {
|
|
req.err <- fmt.Errorf("unable to obtain chan "+
|
|
"point: %v", err)
|
|
return
|
|
}
|
|
|
|
pendingReservation.partialState.FundingOutpoint = *chanPointPtr
|
|
chanPoint = *chanPointPtr
|
|
|
|
// 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,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize an empty sha-chain for them, tracking the current pending
|
|
// revocation hash (we don't yet know the preimage so we can't add it
|
|
// to the chain).
|
|
s := shachain.NewRevocationStore()
|
|
pendingReservation.partialState.RevocationStore = s
|
|
|
|
// Store their current commitment point. We'll need this after the
|
|
// first state transition in order to verify the authenticity of the
|
|
// revocation.
|
|
chanState := pendingReservation.partialState
|
|
chanState.RemoteCurrentRevocation = theirContribution.FirstCommitmentPoint
|
|
|
|
// Create the txin to our commitment transaction; required to construct
|
|
// the commitment transactions.
|
|
fundingTxIn := wire.TxIn{
|
|
PreviousOutPoint: chanPoint,
|
|
}
|
|
|
|
// With the funding tx complete, create both commitment transactions.
|
|
localBalance := pendingReservation.partialState.LocalCommitment.LocalBalance.ToSatoshis()
|
|
remoteBalance := pendingReservation.partialState.LocalCommitment.RemoteBalance.ToSatoshis()
|
|
var leaseExpiry uint32
|
|
if pendingReservation.partialState.ChanType.HasLeaseExpiration() {
|
|
leaseExpiry = pendingReservation.partialState.ThawHeight
|
|
}
|
|
ourCommitTx, theirCommitTx, err := CreateCommitmentTxns(
|
|
localBalance, remoteBalance, ourContribution.ChannelConfig,
|
|
theirContribution.ChannelConfig,
|
|
ourContribution.FirstCommitmentPoint,
|
|
theirContribution.FirstCommitmentPoint, fundingTxIn,
|
|
pendingReservation.partialState.ChanType,
|
|
pendingReservation.partialState.IsInitiator, leaseExpiry,
|
|
)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
|
|
// With both commitment transactions constructed, generate the state
|
|
// obfuscator then use it to encode the current state number within
|
|
// both commitment transactions.
|
|
var stateObfuscator [StateHintSize]byte
|
|
if chanState.ChanType.IsSingleFunder() {
|
|
stateObfuscator = DeriveStateHintObfuscator(
|
|
ourContribution.PaymentBasePoint.PubKey,
|
|
theirContribution.PaymentBasePoint.PubKey,
|
|
)
|
|
} else {
|
|
ourSer := ourContribution.PaymentBasePoint.PubKey.SerializeCompressed()
|
|
theirSer := theirContribution.PaymentBasePoint.PubKey.SerializeCompressed()
|
|
switch bytes.Compare(ourSer, theirSer) {
|
|
case -1:
|
|
stateObfuscator = DeriveStateHintObfuscator(
|
|
ourContribution.PaymentBasePoint.PubKey,
|
|
theirContribution.PaymentBasePoint.PubKey,
|
|
)
|
|
default:
|
|
stateObfuscator = DeriveStateHintObfuscator(
|
|
theirContribution.PaymentBasePoint.PubKey,
|
|
ourContribution.PaymentBasePoint.PubKey,
|
|
)
|
|
}
|
|
}
|
|
err = initStateHints(ourCommitTx, theirCommitTx, stateObfuscator)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
|
|
// Sort both transactions according to the agreed upon canonical
|
|
// ordering. This lets us skip sending the entire transaction over,
|
|
// instead we'll just send signatures.
|
|
txsort.InPlaceSort(ourCommitTx)
|
|
txsort.InPlaceSort(theirCommitTx)
|
|
|
|
walletLog.Tracef("Local commit tx for ChannelPoint(%v): %v",
|
|
chanPoint, spew.Sdump(ourCommitTx))
|
|
walletLog.Tracef("Remote commit tx for ChannelPoint(%v): %v",
|
|
chanPoint, spew.Sdump(theirCommitTx))
|
|
|
|
// Record newly available information within the open channel state.
|
|
chanState.FundingOutpoint = chanPoint
|
|
chanState.LocalCommitment.CommitTx = ourCommitTx
|
|
chanState.RemoteCommitment.CommitTx = theirCommitTx
|
|
|
|
// Next, we'll obtain the funding witness script, and the funding
|
|
// output itself so we can generate a valid signature for the remote
|
|
// party.
|
|
fundingIntent := pendingReservation.fundingIntent
|
|
fundingWitnessScript, fundingOutput, err := fundingIntent.FundingOutput()
|
|
if err != nil {
|
|
req.err <- fmt.Errorf("unable to obtain funding "+
|
|
"output: %w", err)
|
|
return
|
|
}
|
|
|
|
// Generate a signature for their version of the initial commitment
|
|
// transaction.
|
|
sigTheirCommit, err := l.signCommitTx(
|
|
pendingReservation, theirCommitTx, fundingOutput,
|
|
fundingWitnessScript,
|
|
)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
|
|
pendingReservation.ourCommitmentSig = sigTheirCommit
|
|
|
|
req.err <- nil
|
|
}
|
|
|
|
// handleSingleContribution is called as the second step to a single funder
|
|
// workflow to which we are the responder. It simply saves the remote peer's
|
|
// contribution to the channel, as solely the remote peer will contribute any
|
|
// funds to the channel.
|
|
func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg) {
|
|
l.limboMtx.Lock()
|
|
pendingReservation, ok := l.fundingLimbo[req.pendingFundingID]
|
|
l.limboMtx.Unlock()
|
|
if !ok {
|
|
req.err <- fmt.Errorf("attempted to update non-existent funding state")
|
|
return
|
|
}
|
|
|
|
// Grab the mutex on the channelReservation to ensure thread-safety.
|
|
pendingReservation.Lock()
|
|
defer pendingReservation.Unlock()
|
|
|
|
// Validate that the remote's UpfrontShutdownScript is a valid script
|
|
// if it's set.
|
|
shutdown := req.contribution.UpfrontShutdown
|
|
if len(shutdown) > 0 {
|
|
// Validate the shutdown script.
|
|
if !ValidateUpfrontShutdown(shutdown, &l.Cfg.NetParams) {
|
|
req.err <- fmt.Errorf("invalid shutdown script")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Simply record the counterparty's contribution into the pending
|
|
// reservation data as they'll be solely funding the channel entirely.
|
|
pendingReservation.theirContribution = req.contribution
|
|
theirContribution := pendingReservation.theirContribution
|
|
chanState := pendingReservation.partialState
|
|
|
|
// Perform bounds checking on both ChannelReserve and DustLimit
|
|
// parameters. The ChannelReserve may have been changed by the
|
|
// ChannelAcceptor RPC, so this is necessary.
|
|
if !pendingReservation.validateReserveBounds() {
|
|
req.err <- fmt.Errorf("invalid reserve and dust bounds")
|
|
return
|
|
}
|
|
|
|
// Initialize an empty sha-chain for them, tracking the current pending
|
|
// revocation hash (we don't yet know the preimage so we can't add it
|
|
// to the chain).
|
|
remotePreimageStore := shachain.NewRevocationStore()
|
|
chanState.RevocationStore = remotePreimageStore
|
|
|
|
// Now that we've received their first commitment point, we'll store it
|
|
// within the channel state so we can sync it to disk once the funding
|
|
// process is complete.
|
|
chanState.RemoteCurrentRevocation = theirContribution.FirstCommitmentPoint
|
|
|
|
req.err <- nil
|
|
}
|
|
|
|
// verifyFundingInputs attempts to verify all remote inputs to the funding
|
|
// transaction.
|
|
func (l *LightningWallet) verifyFundingInputs(fundingTx *wire.MsgTx,
|
|
remoteInputScripts []*input.Script) error {
|
|
|
|
sigIndex := 0
|
|
fundingHashCache := input.NewTxSigHashesV0Only(fundingTx)
|
|
inputScripts := remoteInputScripts
|
|
for i, txin := range fundingTx.TxIn {
|
|
if len(inputScripts) != 0 && len(txin.Witness) == 0 {
|
|
// Attach the input scripts so we can verify it below.
|
|
txin.Witness = inputScripts[sigIndex].Witness
|
|
txin.SignatureScript = inputScripts[sigIndex].SigScript
|
|
|
|
// Fetch the alleged previous output along with the
|
|
// pkscript referenced by this input.
|
|
//
|
|
// TODO(roasbeef): when dual funder pass actual
|
|
// height-hint
|
|
//
|
|
// TODO(roasbeef): this fails for neutrino always as it
|
|
// treats the height hint as an exact birthday of the
|
|
// utxo rather than a lower bound
|
|
pkScript, err := txscript.ComputePkScript(
|
|
txin.SignatureScript, txin.Witness,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create script: %w",
|
|
err)
|
|
}
|
|
output, err := l.Cfg.ChainIO.GetUtxo(
|
|
&txin.PreviousOutPoint,
|
|
pkScript.Script(), 0, l.quit,
|
|
)
|
|
if output == nil {
|
|
return fmt.Errorf("input to funding tx does "+
|
|
"not exist: %v", err)
|
|
}
|
|
|
|
// Ensure that the witness+sigScript combo is valid.
|
|
vm, err := txscript.NewEngine(
|
|
output.PkScript, fundingTx, i,
|
|
txscript.StandardVerifyFlags, nil,
|
|
fundingHashCache, output.Value,
|
|
txscript.NewCannedPrevOutputFetcher(
|
|
output.PkScript, output.Value,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create script "+
|
|
"engine: %s", err)
|
|
}
|
|
if err = vm.Execute(); err != nil {
|
|
return fmt.Errorf("cannot validate "+
|
|
"transaction: %s", err)
|
|
}
|
|
|
|
sigIndex++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// verifyCommitSig verifies an incoming signature for our version of the
|
|
// commitment transaction. For normal channels, this will verify that the ECDSA
|
|
// signature is valid. For taproot channels, we'll verify that their partial
|
|
// signature is valid, so it can properly be combined with our eventual
|
|
// signature when we need to broadcast.
|
|
func (l *LightningWallet) verifyCommitSig(res *ChannelReservation,
|
|
commitSig input.Signature, commitTx *wire.MsgTx) error {
|
|
|
|
localKey := res.ourContribution.MultiSigKey.PubKey
|
|
remoteKey := res.theirContribution.MultiSigKey.PubKey
|
|
channelValue := int64(res.partialState.Capacity)
|
|
|
|
switch {
|
|
// If this isn't a taproot channel, then we'll construct a segwit v0
|
|
// p2wsh sighash.
|
|
case !res.partialState.ChanType.IsTaproot():
|
|
hashCache := input.NewTxSigHashesV0Only(commitTx)
|
|
witnessScript, _, err := input.GenFundingPkScript(
|
|
localKey.SerializeCompressed(),
|
|
remoteKey.SerializeCompressed(), channelValue,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sigHash, err := txscript.CalcWitnessSigHash(
|
|
witnessScript, hashCache, txscript.SigHashAll,
|
|
commitTx, 0, channelValue,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify that we've received a valid signature from the remote
|
|
// party for our version of the commitment transaction.
|
|
if !commitSig.Verify(sigHash, remoteKey) {
|
|
return fmt.Errorf("counterparty's commitment " +
|
|
"signature is invalid")
|
|
}
|
|
|
|
return nil
|
|
|
|
// Otherwise for taproot channels, we'll compute the segwit v1 sighash,
|
|
// which is slightly different.
|
|
default:
|
|
// First, check to see if we've generated the musig session
|
|
// already. If we're the responder in the funding flow, we may
|
|
// not have generated it already.
|
|
if res.musigSessions == nil {
|
|
_, fundingOutput, err := input.GenTaprootFundingScript(
|
|
localKey, remoteKey, channelValue,
|
|
res.partialState.TapscriptRoot,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
res.musigSessions = genMusigSession(
|
|
res.ourContribution, res.theirContribution,
|
|
l.Cfg.Signer, fundingOutput,
|
|
)
|
|
}
|
|
|
|
// For the musig2 based channels, we'll use the generated local
|
|
// musig2 session to verify the signature.
|
|
localSession := res.musigSessions.LocalSession
|
|
|
|
// At this point, the commitment signature passed in should
|
|
// actually be a wrapped musig2 signature, so we'll do a type
|
|
// asset to the get the signature we actually need.
|
|
partialSig, ok := commitSig.(*MusigPartialSig)
|
|
if !ok {
|
|
return fmt.Errorf("expected *musig2.PartialSignature, "+
|
|
"got: %T", commitSig)
|
|
}
|
|
|
|
_, err := localSession.VerifyCommitSig(
|
|
commitTx, partialSig.ToWireSig(),
|
|
)
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
// handleFundingCounterPartySigs is the final step in the channel reservation
|
|
// workflow. During this step, we validate *all* the received signatures for
|
|
// inputs to the funding transaction. If any of these are invalid, we bail,
|
|
// and forcibly cancel this funding request. Additionally, we ensure that the
|
|
// signature we received from the counterparty for our version of the commitment
|
|
// transaction allows us to spend from the funding output with the addition of
|
|
// our signature.
|
|
func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigsMsg) {
|
|
l.limboMtx.RLock()
|
|
res, ok := l.fundingLimbo[msg.pendingFundingID]
|
|
l.limboMtx.RUnlock()
|
|
if !ok {
|
|
msg.err <- fmt.Errorf("attempted to update non-existent funding state")
|
|
return
|
|
}
|
|
|
|
// Grab the mutex on the ChannelReservation to ensure thread-safety
|
|
res.Lock()
|
|
defer res.Unlock()
|
|
|
|
// Now we can complete the funding transaction by adding their
|
|
// signatures to their inputs.
|
|
res.theirFundingInputScripts = msg.theirFundingInputScripts
|
|
inputScripts := msg.theirFundingInputScripts
|
|
|
|
// Only if we have the final funding transaction do we need to verify
|
|
// the final set of inputs. Otherwise, it may be the case that the
|
|
// channel was funded via an external wallet.
|
|
fundingTx := res.fundingTx
|
|
if res.partialState.ChanType.HasFundingTx() {
|
|
err := l.verifyFundingInputs(fundingTx, inputScripts)
|
|
if err != nil {
|
|
msg.err <- err
|
|
msg.completeChan <- nil
|
|
return
|
|
}
|
|
}
|
|
|
|
// At this point, we can also record and verify their signature for our
|
|
// commitment transaction.
|
|
res.theirCommitmentSig = msg.theirCommitmentSig
|
|
commitTx := res.partialState.LocalCommitment.CommitTx
|
|
|
|
err := l.verifyCommitSig(res, msg.theirCommitmentSig, commitTx)
|
|
if err != nil {
|
|
msg.err <- fmt.Errorf("counterparty's commitment signature is "+
|
|
"invalid: %w", err)
|
|
msg.completeChan <- nil
|
|
return
|
|
}
|
|
|
|
theirCommitSigBytes := msg.theirCommitmentSig.Serialize()
|
|
res.partialState.LocalCommitment.CommitSig = theirCommitSigBytes
|
|
|
|
// Funding complete, this entry can be removed from limbo.
|
|
l.limboMtx.Lock()
|
|
delete(l.fundingLimbo, res.reservationID)
|
|
delete(l.reservationIDs, res.pendingChanID)
|
|
l.limboMtx.Unlock()
|
|
|
|
l.intentMtx.Lock()
|
|
delete(l.fundingIntents, res.pendingChanID)
|
|
l.intentMtx.Unlock()
|
|
|
|
// As we're about to broadcast the funding transaction, we'll take note
|
|
// of the current height for record keeping purposes.
|
|
//
|
|
// TODO(roasbeef): this info can also be piped into light client's
|
|
// basic fee estimation?
|
|
_, bestHeight, err := l.Cfg.ChainIO.GetBestBlock()
|
|
if err != nil {
|
|
msg.err <- err
|
|
msg.completeChan <- nil
|
|
return
|
|
}
|
|
|
|
// As we've completed the funding process, we'll no convert the
|
|
// contribution structs into their underlying channel config objects to
|
|
// he stored within the database.
|
|
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
|
|
|
|
// Set optional upfront shutdown scripts on the channel state so that they
|
|
// are persisted. These values may be nil.
|
|
res.partialState.LocalShutdownScript =
|
|
res.ourContribution.UpfrontShutdown
|
|
res.partialState.RemoteShutdownScript =
|
|
res.theirContribution.UpfrontShutdown
|
|
|
|
res.partialState.RevocationKeyLocator = res.nextRevocationKeyLoc
|
|
|
|
// Add the complete funding transaction to the DB, in its open bucket
|
|
// which will be used for the lifetime of this channel.
|
|
nodeAddr := res.nodeAddr
|
|
err = res.partialState.SyncPending(nodeAddr, uint32(bestHeight))
|
|
if err != nil {
|
|
msg.err <- err
|
|
msg.completeChan <- nil
|
|
return
|
|
}
|
|
|
|
msg.completeChan <- res.partialState
|
|
msg.err <- nil
|
|
}
|
|
|
|
// handleSingleFunderSigs is called once the remote peer who initiated the
|
|
// single funder workflow has assembled the funding transaction, and generated
|
|
// a signature for our version of the commitment transaction. This method
|
|
// progresses the workflow by generating a signature for the remote peer's
|
|
// version of the commitment transaction.
|
|
func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
|
|
l.limboMtx.RLock()
|
|
pendingReservation, ok := l.fundingLimbo[req.pendingFundingID]
|
|
l.limboMtx.RUnlock()
|
|
if !ok {
|
|
req.err <- fmt.Errorf("attempted to update non-existent funding state")
|
|
req.completeChan <- nil
|
|
return
|
|
}
|
|
|
|
// Grab the mutex on the ChannelReservation to ensure thread-safety
|
|
pendingReservation.Lock()
|
|
defer pendingReservation.Unlock()
|
|
|
|
chanState := pendingReservation.partialState
|
|
chanType := pendingReservation.partialState.ChanType
|
|
chanState.FundingOutpoint = *req.fundingOutpoint
|
|
fundingTxIn := wire.NewTxIn(req.fundingOutpoint, nil, nil)
|
|
|
|
// Now that we have the funding outpoint, we can generate both versions
|
|
// of the commitment transaction, and generate a signature for the
|
|
// remote node's commitment transactions.
|
|
localBalance := pendingReservation.partialState.LocalCommitment.LocalBalance.ToSatoshis()
|
|
remoteBalance := pendingReservation.partialState.LocalCommitment.RemoteBalance.ToSatoshis()
|
|
var leaseExpiry uint32
|
|
if pendingReservation.partialState.ChanType.HasLeaseExpiration() {
|
|
leaseExpiry = pendingReservation.partialState.ThawHeight
|
|
}
|
|
ourCommitTx, theirCommitTx, err := CreateCommitmentTxns(
|
|
localBalance, remoteBalance,
|
|
pendingReservation.ourContribution.ChannelConfig,
|
|
pendingReservation.theirContribution.ChannelConfig,
|
|
pendingReservation.ourContribution.FirstCommitmentPoint,
|
|
pendingReservation.theirContribution.FirstCommitmentPoint,
|
|
*fundingTxIn, chanType,
|
|
pendingReservation.partialState.IsInitiator, leaseExpiry,
|
|
)
|
|
if err != nil {
|
|
req.err <- err
|
|
req.completeChan <- nil
|
|
return
|
|
}
|
|
|
|
// With both commitment transactions constructed, we can now use the
|
|
// generator state obfuscator to encode the current state number within
|
|
// both commitment transactions.
|
|
stateObfuscator := DeriveStateHintObfuscator(
|
|
pendingReservation.theirContribution.PaymentBasePoint.PubKey,
|
|
pendingReservation.ourContribution.PaymentBasePoint.PubKey,
|
|
)
|
|
err = initStateHints(ourCommitTx, theirCommitTx, stateObfuscator)
|
|
if err != nil {
|
|
req.err <- err
|
|
req.completeChan <- nil
|
|
return
|
|
}
|
|
|
|
// Sort both transactions according to the agreed upon canonical
|
|
// ordering. This ensures that both parties sign the same sighash
|
|
// without further synchronization.
|
|
txsort.InPlaceSort(ourCommitTx)
|
|
txsort.InPlaceSort(theirCommitTx)
|
|
chanState.LocalCommitment.CommitTx = ourCommitTx
|
|
chanState.RemoteCommitment.CommitTx = theirCommitTx
|
|
|
|
walletLog.Debugf("Local commit tx for ChannelPoint(%v): %v",
|
|
req.fundingOutpoint, spew.Sdump(ourCommitTx))
|
|
walletLog.Debugf("Remote commit tx for ChannelPoint(%v): %v",
|
|
req.fundingOutpoint, spew.Sdump(theirCommitTx))
|
|
|
|
// With both commitment transactions created, we'll now verify their
|
|
// signature on our commitment.
|
|
err = l.verifyCommitSig(
|
|
pendingReservation, req.theirCommitmentSig, ourCommitTx,
|
|
)
|
|
if err != nil {
|
|
req.err <- err
|
|
req.completeChan <- nil
|
|
return
|
|
}
|
|
|
|
theirCommitSigBytes := req.theirCommitmentSig.Serialize()
|
|
chanState.LocalCommitment.CommitSig = theirCommitSigBytes
|
|
|
|
channelValue := int64(pendingReservation.partialState.Capacity)
|
|
theirKey := pendingReservation.theirContribution.MultiSigKey
|
|
ourKey := pendingReservation.ourContribution.MultiSigKey
|
|
|
|
var (
|
|
fundingWitnessScript []byte
|
|
fundingTxOut *wire.TxOut
|
|
)
|
|
if chanType.IsTaproot() {
|
|
//nolint:lll
|
|
fundingWitnessScript, fundingTxOut, err = input.GenTaprootFundingScript(
|
|
ourKey.PubKey, theirKey.PubKey, channelValue,
|
|
pendingReservation.partialState.TapscriptRoot,
|
|
)
|
|
} else {
|
|
//nolint:lll
|
|
fundingWitnessScript, fundingTxOut, err = input.GenFundingPkScript(
|
|
ourKey.PubKey.SerializeCompressed(),
|
|
theirKey.PubKey.SerializeCompressed(), channelValue,
|
|
)
|
|
}
|
|
if err != nil {
|
|
req.err <- err
|
|
req.completeChan <- nil
|
|
return
|
|
}
|
|
|
|
// With their signature for our version of the commitment transactions
|
|
// verified, we can now generate a signature for their version,
|
|
// allowing the funding transaction to be safely broadcast.
|
|
sigTheirCommit, err := l.signCommitTx(
|
|
pendingReservation, theirCommitTx, fundingTxOut,
|
|
fundingWitnessScript,
|
|
)
|
|
if err != nil {
|
|
req.err <- err
|
|
req.completeChan <- nil
|
|
return
|
|
}
|
|
|
|
pendingReservation.ourCommitmentSig = sigTheirCommit
|
|
|
|
_, bestHeight, err := l.Cfg.ChainIO.GetBestBlock()
|
|
if err != nil {
|
|
req.err <- err
|
|
req.completeChan <- nil
|
|
return
|
|
}
|
|
|
|
// Set optional upfront shutdown scripts on the channel state so that they
|
|
// are persisted. These values may be nil.
|
|
chanState.LocalShutdownScript =
|
|
pendingReservation.ourContribution.UpfrontShutdown
|
|
chanState.RemoteShutdownScript =
|
|
pendingReservation.theirContribution.UpfrontShutdown
|
|
|
|
// Add the complete funding transaction to the DB, in it's open bucket
|
|
// which will be used for the lifetime of this channel.
|
|
chanState.LocalChanCfg = pendingReservation.ourContribution.toChanConfig()
|
|
chanState.RemoteChanCfg = pendingReservation.theirContribution.toChanConfig()
|
|
|
|
chanState.RevocationKeyLocator = pendingReservation.nextRevocationKeyLoc
|
|
|
|
err = chanState.SyncPending(pendingReservation.nodeAddr, uint32(bestHeight))
|
|
if err != nil {
|
|
req.err <- err
|
|
req.completeChan <- nil
|
|
return
|
|
}
|
|
|
|
req.completeChan <- chanState
|
|
req.err <- nil
|
|
|
|
l.limboMtx.Lock()
|
|
delete(l.fundingLimbo, req.pendingFundingID)
|
|
delete(l.reservationIDs, pendingReservation.pendingChanID)
|
|
l.limboMtx.Unlock()
|
|
|
|
l.intentMtx.Lock()
|
|
delete(l.fundingIntents, pendingReservation.pendingChanID)
|
|
l.intentMtx.Unlock()
|
|
}
|
|
|
|
// WithCoinSelectLock will execute the passed function closure in a
|
|
// synchronized manner preventing any coin selection operations from proceeding
|
|
// while the closure is executing. This can be seen as the ability to execute a
|
|
// function closure under an exclusive coin selection lock.
|
|
func (l *LightningWallet) WithCoinSelectLock(f func() error) error {
|
|
l.coinSelectMtx.Lock()
|
|
defer l.coinSelectMtx.Unlock()
|
|
|
|
return f()
|
|
}
|
|
|
|
// DeriveStateHintObfuscator derives the bytes to be used for obfuscating the
|
|
// state hints from the root to be used for a new channel. The obfuscator is
|
|
// generated via the following computation:
|
|
//
|
|
// - sha256(initiatorKey || responderKey)[26:]
|
|
// -- where both keys are the multi-sig keys of the respective parties
|
|
//
|
|
// The first 6 bytes of the resulting hash are used as the state hint.
|
|
func DeriveStateHintObfuscator(key1, key2 *btcec.PublicKey) [StateHintSize]byte {
|
|
h := sha256.New()
|
|
h.Write(key1.SerializeCompressed())
|
|
h.Write(key2.SerializeCompressed())
|
|
|
|
sha := h.Sum(nil)
|
|
|
|
var obfuscator [StateHintSize]byte
|
|
copy(obfuscator[:], sha[26:])
|
|
|
|
return obfuscator
|
|
}
|
|
|
|
// initStateHints properly sets the obfuscated state hints on both commitment
|
|
// transactions using the passed obfuscator.
|
|
func initStateHints(commit1, commit2 *wire.MsgTx,
|
|
obfuscator [StateHintSize]byte) error {
|
|
|
|
if err := SetStateNumHint(commit1, 0, obfuscator); err != nil {
|
|
return err
|
|
}
|
|
if err := SetStateNumHint(commit2, 0, obfuscator); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateChannel will attempt to fully validate a newly mined channel, given
|
|
// its funding transaction and existing channel state. If this method returns
|
|
// an error, then the mined channel is invalid, and shouldn't be used.
|
|
func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel,
|
|
fundingTx *wire.MsgTx) error {
|
|
|
|
// First, we'll obtain a fully signed commitment transaction so we can
|
|
// pass into it on the chanvalidate package for verification.
|
|
channel, err := NewLightningChannel(l.Cfg.Signer, channelState, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
localKey := channelState.LocalChanCfg.MultiSigKey.PubKey
|
|
remoteKey := channelState.RemoteChanCfg.MultiSigKey.PubKey
|
|
|
|
// We'll also need the multi-sig witness script itself so the
|
|
// chanvalidate package can check it for correctness against the
|
|
// funding transaction, and also commitment validity.
|
|
var fundingScript []byte
|
|
if channelState.ChanType.IsTaproot() {
|
|
fundingScript, _, err = input.GenTaprootFundingScript(
|
|
localKey, remoteKey, int64(channel.Capacity),
|
|
channelState.TapscriptRoot,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
witnessScript, err := input.GenMultiSigScript(
|
|
localKey.SerializeCompressed(),
|
|
remoteKey.SerializeCompressed(),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fundingScript, err = input.WitnessScriptHash(witnessScript)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
signedCommitTx, err := channel.getSignedCommitTx()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
commitCtx := &chanvalidate.CommitmentContext{
|
|
Value: channel.Capacity,
|
|
FullySignedCommitTx: signedCommitTx,
|
|
}
|
|
|
|
// Finally, we'll pass in all the necessary context needed to fully
|
|
// validate that this channel is indeed what we expect, and can be
|
|
// used.
|
|
_, err = chanvalidate.Validate(&chanvalidate.Context{
|
|
Locator: &chanvalidate.OutPointChanLocator{
|
|
ChanPoint: channelState.FundingOutpoint,
|
|
},
|
|
MultiSigPkScript: fundingScript,
|
|
FundingTx: fundingTx,
|
|
CommitCtx: commitCtx,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CancelRebroadcast cancels the rebroadcast of the given transaction.
|
|
func (l *LightningWallet) CancelRebroadcast(txid chainhash.Hash) {
|
|
// For neutrino, we don't config the rebroadcaster for the wallet as it
|
|
// manages the rebroadcasting logic in neutrino itself.
|
|
if l.Cfg.Rebroadcaster != nil {
|
|
l.Cfg.Rebroadcaster.MarkAsConfirmed(txid)
|
|
}
|
|
}
|
|
|
|
// CoinSource is a wrapper around the wallet that implements the
|
|
// chanfunding.CoinSource interface.
|
|
type CoinSource struct {
|
|
wallet *LightningWallet
|
|
allowUtxo func(Utxo) bool
|
|
}
|
|
|
|
// NewCoinSource creates a new instance of the CoinSource wrapper struct.
|
|
func NewCoinSource(w *LightningWallet, allowUtxo func(Utxo) bool) *CoinSource {
|
|
return &CoinSource{
|
|
wallet: w,
|
|
allowUtxo: allowUtxo,
|
|
}
|
|
}
|
|
|
|
// ListCoins returns all UTXOs from the source that have between
|
|
// minConfs and maxConfs number of confirmations.
|
|
func (c *CoinSource) ListCoins(minConfs int32,
|
|
maxConfs int32) ([]wallet.Coin, error) {
|
|
|
|
utxos, err := c.wallet.ListUnspentWitnessFromDefaultAccount(
|
|
minConfs, maxConfs,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var coins []wallet.Coin
|
|
|
|
for _, utxo := range utxos {
|
|
// If there is a filter function supplied all utxos not adhering
|
|
// to these conditions will be discarded.
|
|
if c.allowUtxo != nil && !c.allowUtxo(*utxo) {
|
|
walletLog.Infof("Cannot use unconfirmed "+
|
|
"utxo=%v because it is unstable and could be "+
|
|
"replaced", utxo.OutPoint)
|
|
|
|
continue
|
|
}
|
|
|
|
coins = append(coins, wallet.Coin{
|
|
TxOut: wire.TxOut{
|
|
Value: int64(utxo.Value),
|
|
PkScript: utxo.PkScript,
|
|
},
|
|
OutPoint: utxo.OutPoint,
|
|
})
|
|
}
|
|
|
|
return coins, nil
|
|
}
|
|
|
|
// CoinFromOutPoint attempts to locate details pertaining to a coin based on
|
|
// its outpoint. If the coin isn't under the control of the backing CoinSource,
|
|
// then an error should be returned.
|
|
func (c *CoinSource) CoinFromOutPoint(op wire.OutPoint) (*wallet.Coin, error) {
|
|
inputInfo, err := c.wallet.FetchOutpointInfo(&op)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &wallet.Coin{
|
|
TxOut: wire.TxOut{
|
|
Value: int64(inputInfo.Value),
|
|
PkScript: inputInfo.PkScript,
|
|
},
|
|
OutPoint: inputInfo.OutPoint,
|
|
}, nil
|
|
}
|
|
|
|
// shimKeyRing is a wrapper struct that's used to provide the proper multi-sig
|
|
// key for an initiated external funding flow.
|
|
type shimKeyRing struct {
|
|
keychain.KeyRing
|
|
|
|
*chanfunding.ShimIntent
|
|
}
|
|
|
|
// DeriveNextKey intercepts the normal DeriveNextKey call to a keychain.KeyRing
|
|
// instance, and supplies the multi-sig key specified by the ShimIntent. This
|
|
// allows us to transparently insert new keys into the existing funding flow,
|
|
// as these keys may not come from the wallet itself.
|
|
func (s *shimKeyRing) DeriveNextKey(keyFam keychain.KeyFamily) (keychain.KeyDescriptor, error) {
|
|
if keyFam != keychain.KeyFamilyMultiSig {
|
|
return s.KeyRing.DeriveNextKey(keyFam)
|
|
}
|
|
|
|
fundingKeys, err := s.ShimIntent.MultiSigKeys()
|
|
if err != nil {
|
|
return keychain.KeyDescriptor{}, err
|
|
}
|
|
|
|
return *fundingKeys.LocalKey, nil
|
|
}
|
|
|
|
// ValidateUpfrontShutdown checks whether the provided upfront_shutdown_script
|
|
// is of a valid type that we accept.
|
|
func ValidateUpfrontShutdown(shutdown lnwire.DeliveryAddress,
|
|
params *chaincfg.Params) bool {
|
|
|
|
// We don't need to worry about a large UpfrontShutdownScript since it
|
|
// was already checked in lnwire when decoding from the wire.
|
|
scriptClass, _, _, _ := txscript.ExtractPkScriptAddrs(shutdown, params)
|
|
|
|
switch {
|
|
case scriptClass == txscript.WitnessV0PubKeyHashTy,
|
|
scriptClass == txscript.WitnessV0ScriptHashTy,
|
|
scriptClass == txscript.WitnessV1TaprootTy:
|
|
|
|
// The above three types are permitted according to BOLT#02 and
|
|
// BOLT#05. Everything else is disallowed.
|
|
return true
|
|
|
|
// In this case, we don't know about the actual script template, but it
|
|
// might be a witness program with versions 2-16. So we'll check that
|
|
// now
|
|
case txscript.IsWitnessProgram(shutdown):
|
|
version, _, err := txscript.ExtractWitnessProgramInfo(shutdown)
|
|
if err != nil {
|
|
walletLog.Warnf("unable to extract witness program "+
|
|
"version (script=%x): %v", shutdown, err)
|
|
return false
|
|
}
|
|
|
|
return version >= 1 && version <= 16
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// WalletPrevOutputFetcher is a txscript.PrevOutputFetcher that can fetch
|
|
// outputs from a given wallet controller.
|
|
type WalletPrevOutputFetcher struct {
|
|
wc WalletController
|
|
}
|
|
|
|
// A compile time assertion that WalletPrevOutputFetcher implements the
|
|
// txscript.PrevOutputFetcher interface.
|
|
var _ txscript.PrevOutputFetcher = (*WalletPrevOutputFetcher)(nil)
|
|
|
|
// NewWalletPrevOutputFetcher creates a new WalletPrevOutputFetcher that fetches
|
|
// previous outputs from the given wallet controller.
|
|
func NewWalletPrevOutputFetcher(wc WalletController) *WalletPrevOutputFetcher {
|
|
return &WalletPrevOutputFetcher{
|
|
wc: wc,
|
|
}
|
|
}
|
|
|
|
// FetchPrevOutput attempts to fetch the previous output referenced by the
|
|
// passed outpoint. A nil value will be returned if the passed outpoint doesn't
|
|
// exist.
|
|
func (w *WalletPrevOutputFetcher) FetchPrevOutput(op wire.OutPoint) *wire.TxOut {
|
|
utxo, err := w.wc.FetchOutpointInfo(&op)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return &wire.TxOut{
|
|
Value: int64(utxo.Value),
|
|
PkScript: utxo.PkScript,
|
|
}
|
|
}
|