671098325d
This commit performs a major refactor of the current wallet, reservation, and channel code in order to call into a WalletController implementation rather than directly into btcwallet. The current set of wallets tests have been modified in order to test against *all* registered WalletController implementations rather than only btcwallet. As a result, all future WalletControllers primary need to ensure that their implementation passes the current set of tests (which will be expanded into the future), providing an easy path of integration assurance. Rather than directly holding the private keys throughout funding and channel creation, the burden of securing keys has been shifted to the specified WalletController and Signer interfaces. All signing is done via the Signer interface rather than directly, increasing flexibility dramatically. During channel funding, rather than creating a txscript.Engine to verify commitment signatures, regular ECDSA sig verification is now used instead. This is faster and more efficient. Finally certain fields/methods within ChannelReservation and LightningChannel have been exposed publicly in order to restrict the amount of modifications the prior tests needed to undergo in order to support testing directly agains the WalletController interface.
1376 lines
48 KiB
Go
1376 lines
48 KiB
Go
package lnwallet
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/elkrem"
|
|
"github.com/roasbeef/btcd/chaincfg"
|
|
"github.com/roasbeef/btcutil/hdkeychain"
|
|
|
|
"github.com/roasbeef/btcd/btcec"
|
|
"github.com/roasbeef/btcd/txscript"
|
|
"github.com/roasbeef/btcd/wire"
|
|
"github.com/roasbeef/btcutil"
|
|
"github.com/roasbeef/btcutil/txsort"
|
|
)
|
|
|
|
const (
|
|
// The size of the buffered queue of requests to the wallet from the
|
|
// outside word.
|
|
msgBufferSize = 100
|
|
|
|
// elkremRootIndex is the top level HD key index from which secrets
|
|
// used to generate elkrem roots should be derived from.
|
|
elkremRootIndex = hdkeychain.HardenedKeyStart + 1
|
|
|
|
// identityKeyIndex is the top level HD key index which is used to
|
|
// generate/rotate identity keys.
|
|
//
|
|
// TODO(roasbeef): should instead be child to make room for future
|
|
// rotations, etc.
|
|
identityKeyIndex = hdkeychain.HardenedKeyStart + 2
|
|
)
|
|
|
|
var (
|
|
// Error types
|
|
ErrInsufficientFunds = errors.New("not enough available outputs to " +
|
|
"create funding transaction")
|
|
|
|
// Namespace bucket keys.
|
|
lightningNamespaceKey = []byte("ln-wallet")
|
|
waddrmgrNamespaceKey = []byte("waddrmgr")
|
|
wtxmgrNamespaceKey = []byte("wtxmgr")
|
|
)
|
|
|
|
// initFundingReserveReq is the first message sent to initiate the workflow
|
|
// required to open a payment channel with a remote peer. The initial required
|
|
// paramters are configurable accross channels. These paramters 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
|
|
// after a timeout period in order to avoid "exhaustion" attacks.
|
|
// NOTE: The workflow currently assumes fully balanced symmetric channels.
|
|
// Meaning both parties must encumber the same amount of funds.
|
|
// TODO(roasbeef): zombie reservation sweeper goroutine.
|
|
type initFundingReserveMsg struct {
|
|
// The number of confirmations required before the channel is considered
|
|
// open.
|
|
numConfs uint16
|
|
|
|
// The amount of funds requested for this channel.
|
|
fundingAmount btcutil.Amount
|
|
|
|
// The total capacity of the channel which includes the amount of funds
|
|
// the remote party contributes (if any).
|
|
capacity btcutil.Amount
|
|
|
|
// The minimum accepted satoshis/KB fee for the funding transaction. In
|
|
// order to ensure timely confirmation, it is recomened that this fee
|
|
// should be generous, paying some multiple of the accepted base fee
|
|
// rate of the network.
|
|
// TODO(roasbeef): integrate fee estimation project...
|
|
minFeeRate btcutil.Amount
|
|
|
|
// The ID of the remote node we would like to open a channel with.
|
|
// TODO(roasbeef): switch to just reg pubkey?
|
|
nodeID [32]byte
|
|
|
|
// The delay on the "pay-to-self" output(s) of the commitment transaction.
|
|
csvDelay uint32
|
|
|
|
// A channel in which all errors will be sent accross. Will be nil if
|
|
// this initial set is succesful.
|
|
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
|
err chan error
|
|
|
|
// A ChannelReservation with our contributions filled in will be sent
|
|
// accross this channel in the case of a succesfully 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
|
|
}
|
|
|
|
// 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 []*InputScript
|
|
|
|
// This should be 1/2 of the signatures needed to succesfully spend our
|
|
// version of the commitment transaction.
|
|
theirCommitmentSig []byte
|
|
|
|
// 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
|
|
|
|
// revokeKey is the revocation public key derived by the remote node to
|
|
// be used within the initial version of the commitment transaction we
|
|
// construct for them.
|
|
revokeKey *btcec.PublicKey
|
|
|
|
// This should be 1/2 of the signatures needed to succesfully spend our
|
|
// version of the commitment transaction.
|
|
theirCommitmentSig []byte
|
|
|
|
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
|
err chan error
|
|
}
|
|
|
|
// channelOpenMsg is the final message sent to finalize a single funder channel
|
|
// workflow to which we are the responder to. This message is sent once the
|
|
// remote peer deems the channel open, meaning it has reached a sufficient
|
|
// number of confirmations in the blockchain.
|
|
type channelOpenMsg struct {
|
|
pendingFundingID uint64
|
|
|
|
// TODO(roasbeef): move verification up to upper layer, yeh?
|
|
spvProof []byte
|
|
|
|
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
|
err chan error
|
|
}
|
|
|
|
// 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 embedds 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 independant 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 dependant
|
|
// on btcd's websockets notifications as even 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 inot btcwallet.
|
|
type LightningWallet struct {
|
|
// This mutex is to be held when generating external keys to be used
|
|
// as multi-sig, and commitment keys within the channel.
|
|
keyGenMtx sync.RWMutex
|
|
|
|
// This mutex MUST be held when performing coin selection in order to
|
|
// avoid inadvertently creating multiple funding transaction which
|
|
// double spend inputs accross each other.
|
|
coinSelectMtx sync.RWMutex
|
|
|
|
// A wrapper around a namespace within boltdb reserved for ln-based
|
|
// wallet meta-data. See the 'channeldb' package for further
|
|
// information.
|
|
ChannelDB *channeldb.DB
|
|
|
|
// Used by in order to obtain notifications about funding transaction
|
|
// reaching a specified confirmation depth, and to catch
|
|
// counterparty's broadcasting revoked commitment states.
|
|
chainNotifier chainntnfs.ChainNotifier
|
|
|
|
// wallet is the the core wallet, all non Lightning Network specific
|
|
// interaction is proxied to the internal wallet.
|
|
WalletController
|
|
|
|
// Signer is the wallet's current Signer implementation. This Signer is
|
|
// used to generate signature for all inputs to potential funding
|
|
// transactions, as well as for spends from the funding transaction to
|
|
// update the commitment state.
|
|
Signer Signer
|
|
|
|
// chainIO is an instance of the BlockChainIO interface. chainIO is
|
|
// used to lookup the existance of outputs within the utxo set.
|
|
chainIO BlockChainIO
|
|
|
|
// rootKey is the root HD key dervied from a WalletController private
|
|
// key. This rootKey is used to derive all LN specific secrets.
|
|
rootKey *hdkeychain.ExtendedKey
|
|
|
|
// All messages to the wallet are to be sent accross 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
|
|
nextFundingID uint64
|
|
limboMtx sync.RWMutex
|
|
// TODO(roasbeef): zombie garbage collection routine to solve
|
|
// lost-object/starvation problem/attack.
|
|
|
|
// 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{}
|
|
|
|
netParams *chaincfg.Params
|
|
|
|
started int32
|
|
shutdown int32
|
|
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.
|
|
//
|
|
// NOTE: The passed channeldb, and ChainNotifier should already be fully
|
|
// initialized/started before being passed as a function arugment.
|
|
func NewLightningWallet(cdb *channeldb.DB, notifier chainntnfs.ChainNotifier,
|
|
wallet WalletController, signer Signer, bio BlockChainIO,
|
|
netParams *chaincfg.Params) (*LightningWallet, error) {
|
|
|
|
// TODO(roasbeef): need a another wallet level config
|
|
|
|
// Fetch the root derivation key from the wallet's HD chain. We'll use
|
|
// this to generate specific Lightning related secrets on the fly.
|
|
rootKey, err := wallet.FetchRootKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO(roasbeef): always re-derive on the fly?
|
|
rootKeyRaw := rootKey.Serialize()
|
|
rootMasterKey, err := hdkeychain.NewMaster(rootKeyRaw, netParams)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &LightningWallet{
|
|
rootKey: rootMasterKey,
|
|
chainNotifier: notifier,
|
|
Signer: signer,
|
|
WalletController: wallet,
|
|
chainIO: bio,
|
|
ChannelDB: cdb,
|
|
msgChan: make(chan interface{}, msgBufferSize),
|
|
nextFundingID: 0,
|
|
fundingLimbo: make(map[uint64]*ChannelReservation),
|
|
lockedOutPoints: make(map[wire.OutPoint]struct{}),
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
close(l.quit)
|
|
l.wg.Wait()
|
|
return nil
|
|
}
|
|
|
|
// LockOutpoints 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 {
|
|
outPoints = append(outPoints, &outPoint)
|
|
}
|
|
|
|
return outPoints
|
|
}
|
|
|
|
// ResetReservations reset the volatile wallet state which trakcs all currently
|
|
// active reservations.
|
|
func (l *LightningWallet) ResetReservations() {
|
|
l.nextFundingID = 0
|
|
l.fundingLimbo = make(map[uint64]*ChannelReservation)
|
|
|
|
for outpoint := range l.lockedOutPoints {
|
|
l.UnlockOutpoint(outpoint)
|
|
}
|
|
l.lockedOutPoints = make(map[wire.OutPoint]struct{})
|
|
}
|
|
|
|
// ActiveReservations returns a slice of all the currently active
|
|
// (non-cancalled) reservations.
|
|
func (l *LightningWallet) ActiveReservations() []*ChannelReservation {
|
|
reservations := make([]*ChannelReservation, 0, len(l.fundingLimbo))
|
|
for _, reservation := range l.fundingLimbo {
|
|
reservations = append(reservations, reservation)
|
|
}
|
|
|
|
return reservations
|
|
}
|
|
|
|
// GetIdentitykey returns the identity private key of the wallet.
|
|
// TODO(roasbeef): should be moved elsewhere
|
|
func (l *LightningWallet) GetIdentitykey() (*btcec.PrivateKey, error) {
|
|
identityKey, err := l.rootKey.Child(identityKeyIndex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return identityKey.ECPrivKey()
|
|
}
|
|
|
|
// requestHandler is the primary goroutine(s) resposible for handling, and
|
|
// dispatching relies to all messages.
|
|
func (l *LightningWallet) requestHandler() {
|
|
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 *addSingleFunderSigsMsg:
|
|
l.handleSingleFunderSigs(msg)
|
|
case *addCounterPartySigsMsg:
|
|
l.handleFundingCounterPartySigs(msg)
|
|
case *channelOpenMsg:
|
|
l.handleChannelOpen(msg)
|
|
}
|
|
case <-l.quit:
|
|
// TODO: do some clean up
|
|
break out
|
|
}
|
|
}
|
|
|
|
l.wg.Done()
|
|
}
|
|
|
|
// InitChannelReservation kicks off the 3-step workflow required to succesfully
|
|
// 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
|
|
// succesful, a ChannelReservation containing our completed contribution is
|
|
// returned. Our contribution contains all the items neccessary to allow the
|
|
// counter party to build the funding transaction, and both versions of the
|
|
// commitment transaction. Otherwise, an error occured 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 records for our version of the
|
|
// commitment transaction is valid.
|
|
func (l *LightningWallet) InitChannelReservation(capacity,
|
|
ourFundAmt btcutil.Amount, theirID [32]byte, numConfs uint16,
|
|
csvDelay uint32) (*ChannelReservation, error) {
|
|
|
|
errChan := make(chan error, 1)
|
|
respChan := make(chan *ChannelReservation, 1)
|
|
|
|
l.msgChan <- &initFundingReserveMsg{
|
|
capacity: capacity,
|
|
numConfs: numConfs,
|
|
fundingAmount: ourFundAmt,
|
|
csvDelay: csvDelay,
|
|
nodeID: theirID,
|
|
err: errChan,
|
|
resp: respChan,
|
|
}
|
|
|
|
return <-respChan, <-errChan
|
|
}
|
|
|
|
// handleFundingReserveRequest processes a message intending to create, and
|
|
// validate a funding reservation request.
|
|
func (l *LightningWallet) handleFundingReserveRequest(req *initFundingReserveMsg) {
|
|
id := atomic.AddUint64(&l.nextFundingID, 1)
|
|
reservation := NewChannelReservation(req.capacity, req.fundingAmount,
|
|
req.minFeeRate, l, id, req.numConfs)
|
|
|
|
// Grab the mutex on the ChannelReservation to ensure thead-safety
|
|
reservation.Lock()
|
|
defer reservation.Unlock()
|
|
|
|
reservation.partialState.TheirLNID = req.nodeID
|
|
ourContribution := reservation.ourContribution
|
|
ourContribution.CsvDelay = req.csvDelay
|
|
reservation.partialState.LocalCsvDelay = req.csvDelay
|
|
|
|
// If we're on the receiving end of a single funder channel then we
|
|
// don't need to perform any coin selection. Otherwise, attempt to
|
|
// obtain enough coins to meet the required funding amount.
|
|
if req.fundingAmount != 0 {
|
|
// TODO(roasbeef): consult model for proper fee rate
|
|
feeRate := uint64(10)
|
|
if err := l.selectCoinsAndChange(feeRate, req.fundingAmount,
|
|
ourContribution); err != nil {
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
}
|
|
|
|
// Grab two fresh keys from our HD chain, one will be used for the
|
|
// multi-sig funding transaction, and the other for the commitment
|
|
// transaction.
|
|
multiSigKey, err := l.NewRawKey()
|
|
if err != nil {
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
commitKey, err := l.NewRawKey()
|
|
if err != nil {
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
reservation.partialState.OurMultiSigKey = multiSigKey
|
|
ourContribution.MultiSigKey = multiSigKey
|
|
reservation.partialState.OurCommitKey = commitKey
|
|
ourContribution.CommitKey = commitKey
|
|
|
|
// Generate a fresh address to be used in the case of a cooperative
|
|
// channel close.
|
|
deliveryAddress, err := l.NewAddress(WitnessPubKey, false)
|
|
if err != nil {
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
deliveryScript, err := txscript.PayToAddrScript(deliveryAddress)
|
|
if err != nil {
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
reservation.partialState.OurDeliveryScript = deliveryScript
|
|
ourContribution.DeliveryAddress = deliveryAddress
|
|
|
|
// Create a limbo and record entry for this newly pending funding
|
|
// request.
|
|
l.limboMtx.Lock()
|
|
l.fundingLimbo[id] = reservation
|
|
l.limboMtx.Unlock()
|
|
|
|
// Funding reservation request succesfully handled. The funding inputs
|
|
// will be marked as unavailable until the reservation is either
|
|
// completed, or cancecled.
|
|
req.resp <- reservation
|
|
req.err <- 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, "unkown funding state" or something
|
|
req.err <- fmt.Errorf("attempted to cancel non-existant funding state")
|
|
return
|
|
}
|
|
|
|
// Grab the mutex on the ChannelReservation to ensure thead-safety
|
|
pendingReservation.Lock()
|
|
defer pendingReservation.Unlock()
|
|
|
|
// Mark all previously locked outpoints as usuable for future funding
|
|
// requests.
|
|
for _, unusedInput := range pendingReservation.ourContribution.Inputs {
|
|
delete(l.lockedOutPoints, unusedInput.PreviousOutPoint)
|
|
l.UnlockOutpoint(unusedInput.PreviousOutPoint)
|
|
}
|
|
|
|
// TODO(roasbeef): is it even worth it to keep track of unsed keys?
|
|
|
|
// TODO(roasbeef): Is it possible to mark the unused change also as
|
|
// available?
|
|
|
|
delete(l.fundingLimbo, req.pendingFundingID)
|
|
|
|
req.err <- nil
|
|
}
|
|
|
|
// handleFundingCounterPartyFunds 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-existant funding state")
|
|
return
|
|
}
|
|
|
|
// Grab the mutex on the ChannelReservation to ensure thead-safety
|
|
pendingReservation.Lock()
|
|
defer pendingReservation.Unlock()
|
|
|
|
// Create a blank, fresh transaction. Soon to be a complete funding
|
|
// transaction which will allow opening a lightning channel.
|
|
pendingReservation.fundingTx = wire.NewMsgTx()
|
|
fundingTx := pendingReservation.fundingTx
|
|
|
|
// Some temporary variables to cut down on the resolution verbosity.
|
|
pendingReservation.theirContribution = req.contribution
|
|
theirContribution := req.contribution
|
|
ourContribution := pendingReservation.ourContribution
|
|
|
|
// Add all multi-party inputs and outputs to the transaction.
|
|
for _, ourInput := range ourContribution.Inputs {
|
|
fundingTx.AddTxIn(ourInput)
|
|
}
|
|
for _, theirInput := range theirContribution.Inputs {
|
|
fundingTx.AddTxIn(theirInput)
|
|
}
|
|
for _, ourChangeOutput := range ourContribution.ChangeOutputs {
|
|
fundingTx.AddTxOut(ourChangeOutput)
|
|
}
|
|
for _, theirChangeOutput := range theirContribution.ChangeOutputs {
|
|
fundingTx.AddTxOut(theirChangeOutput)
|
|
}
|
|
|
|
ourKey := pendingReservation.partialState.OurMultiSigKey
|
|
theirKey := theirContribution.MultiSigKey
|
|
|
|
// Finally, add the 2-of-2 multi-sig output which will set up the lightning
|
|
// channel.
|
|
channelCapacity := int64(pendingReservation.partialState.Capacity)
|
|
redeemScript, multiSigOut, err := GenFundingPkScript(ourKey.SerializeCompressed(),
|
|
theirKey.SerializeCompressed(), channelCapacity)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
pendingReservation.partialState.FundingRedeemScript = redeemScript
|
|
|
|
// Sort the transaction. Since both side agree to a cannonical
|
|
// ordering, by sorting we no longer need to send the entire
|
|
// transaction. Only signatures will be exchanged.
|
|
fundingTx.AddTxOut(multiSigOut)
|
|
txsort.InPlaceSort(pendingReservation.fundingTx)
|
|
|
|
// Next, sign all inputs that are ours, collecting the signatures in
|
|
// order of the inputs.
|
|
pendingReservation.ourFundingInputScripts = make([]*InputScript, 0, len(ourContribution.Inputs))
|
|
signDesc := SignDescriptor{
|
|
HashType: txscript.SigHashAll,
|
|
SigHashes: txscript.NewTxSigHashes(fundingTx),
|
|
}
|
|
for i, txIn := range fundingTx.TxIn {
|
|
info, err := l.FetchInputInfo(&txIn.PreviousOutPoint)
|
|
if err == ErrNotMine {
|
|
continue
|
|
} else if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
|
|
signDesc.Output = info
|
|
signDesc.InputIndex = i
|
|
|
|
inputScript, err := l.Signer.ComputeInputScript(fundingTx, &signDesc)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
|
|
txIn.SignatureScript = inputScript.ScriptSig
|
|
txIn.Witness = inputScript.Witness
|
|
pendingReservation.ourFundingInputScripts = append(
|
|
pendingReservation.ourFundingInputScripts,
|
|
inputScript,
|
|
)
|
|
}
|
|
|
|
// Locate the index of the multi-sig outpoint in order to record it
|
|
// since the outputs are cannonically sorted. If this is a single funder
|
|
// workflow, then we'll also need to send this to the remote node.
|
|
fundingTxID := fundingTx.TxSha()
|
|
_, multiSigIndex := FindScriptOutputIndex(fundingTx, multiSigOut.PkScript)
|
|
fundingOutpoint := wire.NewOutPoint(&fundingTxID, multiSigIndex)
|
|
pendingReservation.partialState.FundingOutpoint = fundingOutpoint
|
|
|
|
// Initialize an empty sha-chain for them, tracking the current pending
|
|
// revocation hash (we don't yet know the pre-image so we can't add it
|
|
// to the chain).
|
|
e := &elkrem.ElkremReceiver{}
|
|
pendingReservation.partialState.RemoteElkrem = e
|
|
pendingReservation.partialState.TheirCurrentRevocation = theirContribution.RevocationKey
|
|
|
|
masterElkremRoot, err := l.deriveMasterElkremRoot()
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
|
|
// Now that we have their commitment key, we can create the revocation
|
|
// key for the first version of our commitment transaction. To do so,
|
|
// we'll first create our elkrem root, then grab the first pre-iamge
|
|
// from it.
|
|
elkremRoot := deriveElkremRoot(masterElkremRoot, ourKey, theirKey)
|
|
elkremSender := elkrem.NewElkremSender(elkremRoot)
|
|
pendingReservation.partialState.LocalElkrem = elkremSender
|
|
firstPreimage, err := elkremSender.AtIndex(0)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
theirCommitKey := theirContribution.CommitKey
|
|
ourRevokeKey := DeriveRevocationPubkey(theirCommitKey, firstPreimage[:])
|
|
|
|
// Create the txIn to our commitment transaction; required to construct
|
|
// the commitment transactions.
|
|
fundingTxIn := wire.NewTxIn(wire.NewOutPoint(&fundingTxID, multiSigIndex), nil, nil)
|
|
|
|
// With the funding tx complete, create both commitment transactions.
|
|
// TODO(roasbeef): much cleanup + de-duplication
|
|
pendingReservation.fundingLockTime = theirContribution.CsvDelay
|
|
ourBalance := ourContribution.FundingAmount
|
|
theirBalance := theirContribution.FundingAmount
|
|
ourCommitKey := ourContribution.CommitKey
|
|
ourCommitTx, err := CreateCommitTx(fundingTxIn, ourCommitKey, theirCommitKey,
|
|
ourRevokeKey, ourContribution.CsvDelay,
|
|
ourBalance, theirBalance)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
theirCommitTx, err := CreateCommitTx(fundingTxIn, theirCommitKey, ourCommitKey,
|
|
theirContribution.RevocationKey, theirContribution.CsvDelay,
|
|
theirBalance, ourBalance)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
|
|
// Sort both transactions according to the agreed upon cannonical
|
|
// ordering. This lets us skip sending the entire transaction over,
|
|
// instead we'll just send signatures.
|
|
txsort.InPlaceSort(ourCommitTx)
|
|
txsort.InPlaceSort(theirCommitTx)
|
|
|
|
deliveryScript, err := txscript.PayToAddrScript(theirContribution.DeliveryAddress)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
|
|
// Record newly available information witin the open channel state.
|
|
pendingReservation.partialState.RemoteCsvDelay = theirContribution.CsvDelay
|
|
pendingReservation.partialState.TheirDeliveryScript = deliveryScript
|
|
pendingReservation.partialState.ChanID = fundingOutpoint
|
|
pendingReservation.partialState.TheirCommitKey = theirCommitKey
|
|
pendingReservation.partialState.TheirMultiSigKey = theirContribution.MultiSigKey
|
|
pendingReservation.partialState.OurCommitTx = ourCommitTx
|
|
pendingReservation.ourContribution.RevocationKey = ourRevokeKey
|
|
|
|
// Generate a signature for their version of the initial commitment
|
|
// transaction.
|
|
signDesc = SignDescriptor{
|
|
RedeemScript: redeemScript,
|
|
PubKey: ourKey,
|
|
Output: multiSigOut,
|
|
HashType: txscript.SigHashAll,
|
|
SigHashes: txscript.NewTxSigHashes(theirCommitTx),
|
|
InputIndex: 0,
|
|
}
|
|
sigTheirCommit, err := l.Signer.SignOutputRaw(theirCommitTx, &signDesc)
|
|
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-existant funding state")
|
|
return
|
|
}
|
|
|
|
// Grab the mutex on the ChannelReservation to ensure thead-safety
|
|
pendingReservation.Lock()
|
|
defer pendingReservation.Unlock()
|
|
|
|
// 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
|
|
|
|
// Additionally, we can now also record the redeem script of the
|
|
// funding transaction.
|
|
// TODO(roasbeef): switch to proper pubkey derivation
|
|
ourKey := pendingReservation.partialState.OurMultiSigKey
|
|
theirKey := theirContribution.MultiSigKey
|
|
channelCapacity := int64(pendingReservation.partialState.Capacity)
|
|
redeemScript, _, err := GenFundingPkScript(ourKey.SerializeCompressed(),
|
|
theirKey.SerializeCompressed(), channelCapacity)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
pendingReservation.partialState.FundingRedeemScript = redeemScript
|
|
|
|
masterElkremRoot, err := l.deriveMasterElkremRoot()
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
|
|
// Now that we know their commitment key, we can create the revocation
|
|
// key for our version of the initial commitment transaction.
|
|
elkremRoot := deriveElkremRoot(masterElkremRoot, ourKey, theirKey)
|
|
elkremSender := elkrem.NewElkremSender(elkremRoot)
|
|
firstPreimage, err := elkremSender.AtIndex(0)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
pendingReservation.partialState.LocalElkrem = elkremSender
|
|
theirCommitKey := theirContribution.CommitKey
|
|
ourRevokeKey := DeriveRevocationPubkey(theirCommitKey, firstPreimage[:])
|
|
|
|
// Initialize an empty sha-chain for them, tracking the current pending
|
|
// revocation hash (we don't yet know the pre-image so we can't add it
|
|
// to the chain).
|
|
remoteElkrem := &elkrem.ElkremReceiver{}
|
|
pendingReservation.partialState.RemoteElkrem = remoteElkrem
|
|
|
|
// Record the counterpaty's remaining contributions to the channel,
|
|
// converting their delivery address into a public key script.
|
|
deliveryScript, err := txscript.PayToAddrScript(theirContribution.DeliveryAddress)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
pendingReservation.partialState.RemoteCsvDelay = theirContribution.CsvDelay
|
|
pendingReservation.partialState.TheirDeliveryScript = deliveryScript
|
|
pendingReservation.partialState.TheirCommitKey = theirContribution.CommitKey
|
|
pendingReservation.partialState.TheirMultiSigKey = theirContribution.MultiSigKey
|
|
pendingReservation.ourContribution.RevocationKey = ourRevokeKey
|
|
|
|
req.err <- nil
|
|
return
|
|
}
|
|
|
|
// handleFundingCounterPartySigs is the final step in the channel reservation
|
|
// workflow. During this setp, 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()
|
|
pendingReservation, ok := l.fundingLimbo[msg.pendingFundingID]
|
|
l.limboMtx.RUnlock()
|
|
if !ok {
|
|
msg.err <- fmt.Errorf("attempted to update non-existant funding state")
|
|
return
|
|
}
|
|
|
|
// Grab the mutex on the ChannelReservation to ensure thead-safety
|
|
pendingReservation.Lock()
|
|
defer pendingReservation.Unlock()
|
|
|
|
// Now we can complete the funding transaction by adding their
|
|
// signatures to their inputs.
|
|
pendingReservation.theirFundingInputScripts = msg.theirFundingInputScripts
|
|
inputScripts := msg.theirFundingInputScripts
|
|
fundingTx := pendingReservation.fundingTx
|
|
sigIndex := 0
|
|
fundingHashCache := txscript.NewTxSigHashes(fundingTx)
|
|
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].ScriptSig
|
|
|
|
// Fetch the alleged previous output along with the
|
|
// pkscript referenced by this input.
|
|
prevOut := txin.PreviousOutPoint
|
|
output, err := l.chainIO.GetUtxo(&prevOut.Hash, prevOut.Index)
|
|
if output == nil {
|
|
msg.err <- fmt.Errorf("input to funding tx does not exist: %v", err)
|
|
return
|
|
}
|
|
|
|
// Ensure that the witness+sigScript combo is valid.
|
|
vm, err := txscript.NewEngine(output.PkScript,
|
|
fundingTx, i, txscript.StandardVerifyFlags, nil,
|
|
fundingHashCache, output.Value)
|
|
if err != nil {
|
|
// TODO(roasbeef): cancel at this stage if invalid sigs?
|
|
msg.err <- fmt.Errorf("cannot create script engine: %s", err)
|
|
return
|
|
}
|
|
if err = vm.Execute(); err != nil {
|
|
msg.err <- fmt.Errorf("cannot validate transaction: %s", err)
|
|
return
|
|
}
|
|
|
|
sigIndex++
|
|
}
|
|
}
|
|
|
|
// At this point, we can also record and verify their signature for our
|
|
// commitment transaction.
|
|
pendingReservation.theirCommitmentSig = msg.theirCommitmentSig
|
|
commitTx := pendingReservation.partialState.OurCommitTx
|
|
theirKey := pendingReservation.theirContribution.MultiSigKey
|
|
|
|
// Re-generate both the redeemScript and p2sh output. We sign the
|
|
// redeemScript script, but include the p2sh output as the subscript
|
|
// for verification.
|
|
redeemScript := pendingReservation.partialState.FundingRedeemScript
|
|
|
|
// Next, create the spending scriptSig, and then verify that the script
|
|
// is complete, allowing us to spend from the funding transaction.
|
|
theirCommitSig := msg.theirCommitmentSig
|
|
channelValue := int64(pendingReservation.partialState.Capacity)
|
|
hashCache := txscript.NewTxSigHashes(commitTx)
|
|
sigHash, err := txscript.CalcWitnessSigHash(redeemScript, hashCache,
|
|
txscript.SigHashAll, commitTx, 0, channelValue)
|
|
if err != nil {
|
|
msg.err <- fmt.Errorf("counterparty's commitment signature is invalid: %v", err)
|
|
return
|
|
}
|
|
|
|
walletLog.Infof("sighash verify: %v", hex.EncodeToString(sigHash))
|
|
walletLog.Infof("initer verifying tx: %v", spew.Sdump(commitTx))
|
|
|
|
// Verify that we've received a valid signature from the remote party
|
|
// for our version of the commitment transaction.
|
|
sig, err := btcec.ParseSignature(theirCommitSig, btcec.S256())
|
|
if err != nil {
|
|
msg.err <- err
|
|
return
|
|
} else if !sig.Verify(sigHash, theirKey) {
|
|
msg.err <- fmt.Errorf("counterparty's commitment signature is invalid")
|
|
return
|
|
}
|
|
pendingReservation.partialState.OurCommitSig = theirCommitSig
|
|
|
|
// Funding complete, this entry can be removed from limbo.
|
|
l.limboMtx.Lock()
|
|
delete(l.fundingLimbo, pendingReservation.reservationID)
|
|
// TODO(roasbeef): unlock outputs here, Store.InsertTx will handle marking
|
|
// input in unconfirmed tx, so future coin selects don't pick it up
|
|
// * also record location of change address so can use AddCredit
|
|
l.limboMtx.Unlock()
|
|
|
|
walletLog.Infof("Broadcasting funding tx for ChannelPoint(%v): %v",
|
|
pendingReservation.partialState.FundingOutpoint,
|
|
spew.Sdump(fundingTx))
|
|
|
|
// Broacast the finalized funding transaction to the network.
|
|
if err := l.PublishTransaction(fundingTx); err != nil {
|
|
msg.err <- err
|
|
return
|
|
}
|
|
|
|
// Add the complete funding transaction to the DB, in it's open bucket
|
|
// which will be used for the lifetime of this channel.
|
|
if err := pendingReservation.partialState.FullSync(); err != nil {
|
|
msg.err <- err
|
|
return
|
|
}
|
|
|
|
// Create a goroutine to watch the chain so we can open the channel once
|
|
// the funding tx has enough confirmations.
|
|
go l.openChannelAfterConfirmations(pendingReservation)
|
|
|
|
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-existant funding state")
|
|
return
|
|
}
|
|
|
|
// Grab the mutex on the ChannelReservation to ensure thead-safety
|
|
pendingReservation.Lock()
|
|
defer pendingReservation.Unlock()
|
|
|
|
pendingReservation.partialState.FundingOutpoint = req.fundingOutpoint
|
|
pendingReservation.partialState.TheirCurrentRevocation = req.revokeKey
|
|
pendingReservation.partialState.ChanID = 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.
|
|
ourCommitKey := pendingReservation.ourContribution.CommitKey
|
|
theirCommitKey := pendingReservation.theirContribution.CommitKey
|
|
ourBalance := pendingReservation.ourContribution.FundingAmount
|
|
theirBalance := pendingReservation.theirContribution.FundingAmount
|
|
ourCommitTx, err := CreateCommitTx(fundingTxIn, ourCommitKey, theirCommitKey,
|
|
pendingReservation.ourContribution.RevocationKey,
|
|
pendingReservation.ourContribution.CsvDelay, ourBalance, theirBalance)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
theirCommitTx, err := CreateCommitTx(fundingTxIn, theirCommitKey, ourCommitKey,
|
|
req.revokeKey, pendingReservation.theirContribution.CsvDelay,
|
|
theirBalance, ourBalance)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
|
|
// Sort both transactions according to the agreed upon cannonical
|
|
// ordering. This ensures that both parties sign the same sighash
|
|
// without further synchronization.
|
|
txsort.InPlaceSort(ourCommitTx)
|
|
pendingReservation.partialState.OurCommitTx = ourCommitTx
|
|
txsort.InPlaceSort(theirCommitTx)
|
|
|
|
redeemScript := pendingReservation.partialState.FundingRedeemScript
|
|
channelValue := int64(pendingReservation.partialState.Capacity)
|
|
hashCache := txscript.NewTxSigHashes(ourCommitTx)
|
|
theirKey := pendingReservation.theirContribution.MultiSigKey
|
|
ourKey := pendingReservation.partialState.OurMultiSigKey
|
|
|
|
sigHash, err := txscript.CalcWitnessSigHash(redeemScript, hashCache,
|
|
txscript.SigHashAll, ourCommitTx, 0, channelValue)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
|
|
// Verify that we've received a valid signature from the remote party
|
|
// for our version of the commitment transaction.
|
|
sig, err := btcec.ParseSignature(req.theirCommitmentSig, btcec.S256())
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
} else if !sig.Verify(sigHash, theirKey) {
|
|
req.err <- fmt.Errorf("counterparty's commitment signature is invalid")
|
|
return
|
|
}
|
|
pendingReservation.partialState.OurCommitSig = req.theirCommitmentSig
|
|
|
|
// 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.
|
|
p2wsh, err := witnessScriptHash(redeemScript)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
signDesc := SignDescriptor{
|
|
RedeemScript: redeemScript,
|
|
PubKey: ourKey,
|
|
Output: &wire.TxOut{
|
|
PkScript: p2wsh,
|
|
Value: channelValue,
|
|
},
|
|
HashType: txscript.SigHashAll,
|
|
SigHashes: txscript.NewTxSigHashes(theirCommitTx),
|
|
InputIndex: 0,
|
|
}
|
|
sigTheirCommit, err := l.Signer.SignOutputRaw(theirCommitTx, &signDesc)
|
|
if err != nil {
|
|
req.err <- err
|
|
return
|
|
}
|
|
pendingReservation.ourCommitmentSig = sigTheirCommit
|
|
|
|
req.err <- nil
|
|
}
|
|
|
|
// handleChannelOpen completes a single funder reservation to which we are the
|
|
// responder. This method saves the channel state to disk, finally "opening"
|
|
// the channel by sending it over to the caller of the reservation via the
|
|
// channel dispatch channel.
|
|
func (l *LightningWallet) handleChannelOpen(req *channelOpenMsg) {
|
|
l.limboMtx.RLock()
|
|
res, ok := l.fundingLimbo[req.pendingFundingID]
|
|
l.limboMtx.RUnlock()
|
|
if !ok {
|
|
req.err <- fmt.Errorf("attempted to update non-existant funding state")
|
|
res.chanOpen <- nil
|
|
return
|
|
}
|
|
|
|
// Grab the mutex on the ChannelReservation to ensure thead-safety
|
|
res.Lock()
|
|
defer res.Unlock()
|
|
|
|
// Funding complete, this entry can be removed from limbo.
|
|
l.limboMtx.Lock()
|
|
delete(l.fundingLimbo, res.reservationID)
|
|
l.limboMtx.Unlock()
|
|
|
|
// Add the complete funding transaction to the DB, in it's open bucket
|
|
// which will be used for the lifetime of this channel.
|
|
if err := res.partialState.FullSync(); err != nil {
|
|
req.err <- err
|
|
res.chanOpen <- nil
|
|
return
|
|
}
|
|
|
|
// Finally, create and officially open the payment channel!
|
|
// TODO(roasbeef): CreationTime once tx is 'open'
|
|
channel, _ := NewLightningChannel(l.Signer, l, l.chainNotifier, res.partialState)
|
|
|
|
res.chanOpen <- channel
|
|
req.err <- nil
|
|
}
|
|
|
|
// openChannelAfterConfirmations creates, and opens a payment channel after
|
|
// the funding transaction created within the passed channel reservation
|
|
// obtains the specified number of confirmations.
|
|
func (l *LightningWallet) openChannelAfterConfirmations(res *ChannelReservation) {
|
|
// Register with the ChainNotifier for a notification once the funding
|
|
// transaction reaches `numConfs` confirmations.
|
|
txid := res.fundingTx.TxSha()
|
|
numConfs := uint32(res.numConfsToOpen)
|
|
confNtfn, _ := l.chainNotifier.RegisterConfirmationsNtfn(&txid, numConfs)
|
|
|
|
walletLog.Infof("Waiting for funding tx (txid: %v) to reach %v confirmations",
|
|
txid, numConfs)
|
|
|
|
// Wait until the specified number of confirmations has been reached,
|
|
// or the wallet signals a shutdown.
|
|
out:
|
|
select {
|
|
case _, ok := <-confNtfn.Confirmed:
|
|
// Reading a falsey value for the second parameter indicates that
|
|
// the notifier is in the process of shutting down. Therefore, we
|
|
// don't count this as the signal that the funding transaction has
|
|
// been confirmed.
|
|
if !ok {
|
|
res.chanOpen <- nil
|
|
return
|
|
}
|
|
|
|
break out
|
|
case <-l.quit:
|
|
res.chanOpen <- nil
|
|
return
|
|
}
|
|
|
|
// Finally, create and officially open the payment channel!
|
|
// TODO(roasbeef): CreationTime once tx is 'open'
|
|
channel, _ := NewLightningChannel(l.Signer, l, l.chainNotifier,
|
|
res.partialState)
|
|
res.chanOpen <- channel
|
|
}
|
|
|
|
// selectCoinsAndChange performs coin selection in order to obtain witness
|
|
// outputs which sum to at least 'numCoins' amount of satoshis. If coin
|
|
// selection is succesful/possible, then the selected coins are available within
|
|
// the passed contribution's inputs. If necessary, a change address will also be
|
|
// generated.
|
|
// TODO(roasbeef): remove hardcoded fees and req'd confs for outputs.
|
|
func (l *LightningWallet) selectCoinsAndChange(feeRate uint64, amt btcutil.Amount,
|
|
contribution *ChannelContribution) error {
|
|
|
|
// We hold the coin select mutex while querying for outputs, and
|
|
// performing coin selection in order to avoid inadvertent double spends
|
|
// accross funding transactions.
|
|
// NOTE: We don't use defer her so we can properly release the lock
|
|
// when we encounter an error condition.
|
|
l.coinSelectMtx.Lock()
|
|
|
|
// Find all unlocked unspent witness outputs with greater than 1
|
|
// confirmation.
|
|
// TODO(roasbeef): make num confs a configuration paramter
|
|
coins, err := l.ListUnspentWitness(1)
|
|
if err != nil {
|
|
l.coinSelectMtx.Unlock()
|
|
return err
|
|
}
|
|
|
|
// Peform coin selection over our available, unlocked unspent outputs
|
|
// in order to find enough coins to meet the funding amount requirements.
|
|
// TODO(roasbeef): take in sat/byte
|
|
selectedCoins, changeAmt, err := coinSelect(feeRate, amt, coins)
|
|
if err != nil {
|
|
l.coinSelectMtx.Unlock()
|
|
return err
|
|
}
|
|
|
|
// Lock the selected coins. These coins are now "reserved", this
|
|
// prevents concurrent funding requests from referring to and this
|
|
// double-spending the same set of coins.
|
|
contribution.Inputs = make([]*wire.TxIn, len(selectedCoins))
|
|
for i, coin := range selectedCoins {
|
|
l.lockedOutPoints[*coin] = struct{}{}
|
|
l.LockOutpoint(*coin)
|
|
|
|
// Empty sig script, we'll actually sign if this reservation is
|
|
// queued up to be completed (the other side accepts).
|
|
contribution.Inputs[i] = wire.NewTxIn(coin, nil, nil)
|
|
}
|
|
|
|
// Record any change output(s) generated as a result of the coin
|
|
// selection.
|
|
if changeAmt != 0 {
|
|
changeAddr, err := l.NewAddress(WitnessPubKey, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
changeScript, err := txscript.PayToAddrScript(changeAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
contribution.ChangeOutputs = make([]*wire.TxOut, 1)
|
|
contribution.ChangeOutputs[0] = &wire.TxOut{
|
|
Value: int64(changeAmt),
|
|
PkScript: changeScript,
|
|
}
|
|
}
|
|
|
|
l.coinSelectMtx.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// deriveMasterElkremRoot derives the private key which serves as the master
|
|
// elkrem root. This master secret is used as the secret input to a HKDF to
|
|
// generate elkrem secrets based on random, but public data.
|
|
func (l *LightningWallet) deriveMasterElkremRoot() (*btcec.PrivateKey, error) {
|
|
masterElkremRoot, err := l.rootKey.Child(elkremRootIndex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return masterElkremRoot.ECPrivKey()
|
|
}
|
|
|
|
// selectInputs selects a slice of inputs necessary to meet the specified
|
|
// selection amount. If input selectino is unable to suceed to to insuffcient
|
|
// funds, a non-nil error is returned. Additionally, the total amount of the
|
|
// selected coins are returned in order for the caller to properly handle
|
|
// change+fees.
|
|
func selectInputs(amt btcutil.Amount, coins []*Utxo) (btcutil.Amount, []*wire.OutPoint, error) {
|
|
var (
|
|
selectedUtxos []*wire.OutPoint
|
|
satSelected btcutil.Amount
|
|
)
|
|
|
|
i := 0
|
|
for satSelected < amt {
|
|
// If we're about to go past the number of available coins,
|
|
// then exit with an error.
|
|
if i > len(coins)-1 {
|
|
return 0, nil, ErrInsufficientFunds
|
|
}
|
|
|
|
// Otherwise, collect this new coin as it may be used for final
|
|
// coin selection.
|
|
coin := coins[i]
|
|
utxo := &wire.OutPoint{
|
|
Hash: coin.Hash,
|
|
Index: coin.Index,
|
|
}
|
|
|
|
selectedUtxos = append(selectedUtxos, utxo)
|
|
satSelected += coin.Value
|
|
|
|
i++
|
|
}
|
|
|
|
return satSelected, selectedUtxos, nil
|
|
}
|
|
|
|
// coinSelect attemps to select a sufficient amount of coins, including a
|
|
// change output to fund amt satoshis, adhearing to the specified fee rate. The
|
|
// specified fee rate should be expressed in sat/byte for coin selection to
|
|
// function properly.
|
|
func coinSelect(feeRate uint64, amt btcutil.Amount,
|
|
coins []*Utxo) ([]*wire.OutPoint, btcutil.Amount, error) {
|
|
|
|
const (
|
|
// txOverhead is the overhead of a transaction residing within
|
|
// the version number and lock time.
|
|
txOverhead = 8
|
|
|
|
// p2wkhSpendSize an estimate of the number of bytes it takes
|
|
// to spend a p2wkh output.
|
|
//
|
|
// (p2wkh witness) + txid + index + varint script size + sequence
|
|
// TODO(roasbeef): div by 3 due to witness size?
|
|
p2wkhSpendSize = (1 + 73 + 1 + 33) + 32 + 4 + 1 + 4
|
|
|
|
// p2wkhOutputSize is an estimate of the size of a regualr
|
|
// p2wkh output.
|
|
//
|
|
// 8 (output) + 1 (var int script) + 22 (p2wkh output)
|
|
p2wkhOutputSize = 8 + 1 + 22
|
|
|
|
// p2wkhOutputSize is an estimate of the p2wsh funding uotput.
|
|
p2wshOutputSize = 8 + 1 + 34
|
|
)
|
|
|
|
var estimatedSize int
|
|
|
|
amtNeeded := amt
|
|
for {
|
|
// First perform an initial round of coin selection to estimate
|
|
// the required fee.
|
|
totalSat, selectedUtxos, err := selectInputs(amtNeeded, coins)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Based on the selected coins, estimate the size of the final
|
|
// fully signed transaction.
|
|
estimatedSize = ((len(selectedUtxos) * p2wkhSpendSize) +
|
|
p2wshOutputSize + txOverhead)
|
|
|
|
// The difference bteween the selected amount and the amount
|
|
// requested will be used to pay fees, and generate a change
|
|
// output with the remaining.
|
|
overShootAmt := totalSat - amtNeeded
|
|
|
|
// Based on the estimated size and fee rate, if the excess
|
|
// amount isn't enough to pay fees, then increase the requested
|
|
// coin amount by the estimate required fee, performing another
|
|
// round of coin selection.
|
|
requiredFee := btcutil.Amount(uint64(estimatedSize) * feeRate)
|
|
if overShootAmt < requiredFee {
|
|
amtNeeded += requiredFee
|
|
continue
|
|
}
|
|
|
|
// If the fee is sufficient, then calculate the size of the change output.
|
|
changeAmt := overShootAmt - requiredFee
|
|
|
|
return selectedUtxos, changeAmt, nil
|
|
}
|
|
}
|