You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1625 lines
54 KiB
1625 lines
54 KiB
package lnwallet |
|
|
|
import ( |
|
"bytes" |
|
"crypto/sha256" |
|
"errors" |
|
"fmt" |
|
"math" |
|
"net" |
|
"sync" |
|
"sync/atomic" |
|
|
|
"github.com/btcsuite/btcd/blockchain" |
|
"github.com/btcsuite/btcd/btcec" |
|
"github.com/btcsuite/btcd/chaincfg/chainhash" |
|
"github.com/btcsuite/btcd/txscript" |
|
"github.com/btcsuite/btcd/wire" |
|
"github.com/btcsuite/btcutil" |
|
"github.com/btcsuite/btcutil/txsort" |
|
"github.com/davecgh/go-spew/spew" |
|
"github.com/lightningnetwork/lnd/channeldb" |
|
"github.com/lightningnetwork/lnd/input" |
|
"github.com/lightningnetwork/lnd/keychain" |
|
"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 |
|
) |
|
|
|
// ErrInsufficientFunds is a type matching the error interface which is |
|
// returned when coin selection for a new funding transaction fails to due |
|
// having an insufficient amount of confirmed funds. |
|
type ErrInsufficientFunds struct { |
|
amountAvailable btcutil.Amount |
|
amountSelected btcutil.Amount |
|
} |
|
|
|
func (e *ErrInsufficientFunds) Error() string { |
|
return fmt.Sprintf("not enough witness outputs to create funding transaction,"+ |
|
" need %v only have %v available", e.amountAvailable, |
|
e.amountSelected) |
|
} |
|
|
|
// 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 |
|
|
|
// 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 |
|
|
|
// 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 SatPerKWeight |
|
|
|
// FundingFeePerKw is the fee rate in sat/kw to use for the initial |
|
// funding transaction. |
|
FundingFeePerKw 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 |
|
|
|
// 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 |
|
} |
|
|
|
// 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 []byte |
|
|
|
// 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 []byte |
|
|
|
// 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 |
|
} |
|
|
|
// 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 |
|
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{} |
|
|
|
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), |
|
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 |
|
} |
|
|
|
// 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 { |
|
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) |
|
|
|
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-cancelled) 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() { |
|
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 <-l.quit: |
|
// TODO: do some clean up |
|
break out |
|
} |
|
} |
|
|
|
l.wg.Done() |
|
} |
|
|
|
// 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 |
|
} |
|
|
|
// handleFundingReserveRequest processes a message intending to create, and |
|
// validate a funding reservation request. |
|
func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg) { |
|
// It isn't possible to create a channel with zero funds committed. |
|
if req.LocalFundingAmt+req.RemoteFundingAmt == 0 { |
|
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 |
|
} |
|
|
|
localFundingAmt := req.LocalFundingAmt |
|
|
|
var ( |
|
selected *coinSelection |
|
err error |
|
) |
|
|
|
// If we're on the receiving end of a single funder channel then we |
|
// don't need to perform any coin selection, and the remote contributes |
|
// all funds. Otherwise, attempt to obtain enough coins to meet the |
|
// required funding amount. |
|
if req.LocalFundingAmt != 0 { |
|
// Coin selection is done on the basis of sat/kw, so we'll use |
|
// the fee rate passed in to perform coin selection. |
|
var err error |
|
selected, err = l.selectCoinsAndChange( |
|
req.FundingFeePerKw, req.LocalFundingAmt, req.MinConfs, |
|
req.SubtractFees, |
|
) |
|
if err != nil { |
|
req.err <- err |
|
req.resp <- nil |
|
return |
|
} |
|
|
|
localFundingAmt = selected.fundingAmt |
|
} |
|
|
|
// The total channel capacity will be the size of the funding output we |
|
// created plus the remote contribution. |
|
capacity := localFundingAmt + req.RemoteFundingAmt |
|
|
|
id := atomic.AddUint64(&l.nextFundingID, 1) |
|
reservation, err := NewChannelReservation( |
|
capacity, localFundingAmt, req.CommitFeePerKw, l, id, |
|
req.PushMSat, l.Cfg.NetParams.GenesisHash, req.Flags, |
|
) |
|
if err != nil { |
|
selected.unlockCoins() |
|
req.err <- err |
|
req.resp <- nil |
|
return |
|
} |
|
|
|
err = l.initOurContribution( |
|
reservation, selected, req.NodeAddr, req.NodeID, |
|
) |
|
if err != nil { |
|
selected.unlockCoins() |
|
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.limboMtx.Unlock() |
|
|
|
// Funding reservation request successfully handled. The funding inputs |
|
// will be marked as unavailable until the reservation is either |
|
// completed, or cancelled. |
|
req.resp <- reservation |
|
req.err <- 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, |
|
selected *coinSelection, nodeAddr net.Addr, nodeID *btcec.PublicKey) error { |
|
|
|
// Grab the mutex on the ChannelReservation to ensure thread-safety |
|
reservation.Lock() |
|
defer reservation.Unlock() |
|
|
|
if selected != nil { |
|
reservation.ourContribution.Inputs = selected.coins |
|
reservation.ourContribution.ChangeOutputs = selected.change |
|
} |
|
|
|
reservation.nodeAddr = nodeAddr |
|
reservation.partialState.IdentityPub = nodeID |
|
|
|
// Next, we'll grab a series of keys from the wallet which will be used |
|
// for the duration of the channel. The keys include: our multi-sig |
|
// key, the base revocation key, the base htlc key,the base payment |
|
// key, and the delayed payment key. |
|
// |
|
// TODO(roasbeef): "salt" each key as well? |
|
var err error |
|
reservation.ourContribution.MultiSigKey, err = l.DeriveNextKey( |
|
keychain.KeyFamilyMultiSig, |
|
) |
|
if err != nil { |
|
return err |
|
} |
|
reservation.ourContribution.RevocationBasePoint, err = l.DeriveNextKey( |
|
keychain.KeyFamilyRevocationBase, |
|
) |
|
if err != nil { |
|
return err |
|
} |
|
reservation.ourContribution.HtlcBasePoint, err = l.DeriveNextKey( |
|
keychain.KeyFamilyHtlcBase, |
|
) |
|
if err != nil { |
|
return err |
|
} |
|
reservation.ourContribution.PaymentBasePoint, err = l.DeriveNextKey( |
|
keychain.KeyFamilyPaymentBase, |
|
) |
|
if err != nil { |
|
return err |
|
} |
|
reservation.ourContribution.DelayBasePoint, err = l.DeriveNextKey( |
|
keychain.KeyFamilyDelayBase, |
|
) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
// With the above keys created, we'll also need to initialization our |
|
// initial revocation tree state. |
|
nextRevocationKeyDesc, err := l.DeriveNextKey( |
|
keychain.KeyFamilyRevocationRoot, |
|
) |
|
if err != nil { |
|
return err |
|
} |
|
revocationRoot, err := l.DerivePrivKey(nextRevocationKeyDesc) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
// Once we have the root, we can then generate our shachain producer |
|
// and from that generate the per-commitment point. |
|
revRoot, err := chainhash.NewHash(revocationRoot.Serialize()) |
|
if err != nil { |
|
return err |
|
} |
|
producer := shachain.NewRevocationProducer(*revRoot) |
|
firstPreimage, err := producer.AtIndex(0) |
|
if err != nil { |
|
return err |
|
} |
|
reservation.ourContribution.FirstCommitmentPoint = input.ComputeCommitmentPoint( |
|
firstPreimage[:], |
|
) |
|
|
|
reservation.partialState.RevocationProducer = producer |
|
reservation.ourContribution.ChannelConstraints = l.Cfg.DefaultConstraints |
|
|
|
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 useable 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 unused keys? |
|
|
|
// TODO(roasbeef): Is it possible to mark the unused change also as |
|
// available? |
|
|
|
delete(l.fundingLimbo, req.pendingFundingID) |
|
|
|
req.err <- nil |
|
} |
|
|
|
// 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) (*wire.MsgTx, *wire.MsgTx, error) { |
|
|
|
localCommitmentKeys := deriveCommitmentKeys(localCommitPoint, true, |
|
ourChanCfg, theirChanCfg) |
|
remoteCommitmentKeys := deriveCommitmentKeys(remoteCommitPoint, false, |
|
ourChanCfg, theirChanCfg) |
|
|
|
ourCommitTx, err := CreateCommitTx(fundingTxIn, localCommitmentKeys, |
|
uint32(ourChanCfg.CsvDelay), localBalance, remoteBalance, |
|
ourChanCfg.DustLimit) |
|
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(fundingTxIn, remoteCommitmentKeys, |
|
uint32(theirChanCfg.CsvDelay), remoteBalance, localBalance, |
|
theirChanCfg.DustLimit) |
|
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() |
|
|
|
// Create a blank, fresh transaction. Soon to be a complete funding |
|
// transaction which will allow opening a lightning channel. |
|
pendingReservation.fundingTx = wire.NewMsgTx(1) |
|
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.ourContribution.MultiSigKey |
|
theirKey := theirContribution.MultiSigKey |
|
|
|
// Finally, add the 2-of-2 multi-sig output which will set up the lightning |
|
// channel. |
|
channelCapacity := int64(pendingReservation.partialState.Capacity) |
|
witnessScript, multiSigOut, err := input.GenFundingPkScript( |
|
ourKey.PubKey.SerializeCompressed(), |
|
theirKey.PubKey.SerializeCompressed(), channelCapacity, |
|
) |
|
if err != nil { |
|
req.err <- err |
|
return |
|
} |
|
|
|
// Sort the transaction. Since both side agree to a canonical 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([]*input.Script, 0, |
|
len(ourContribution.Inputs)) |
|
signDesc := input.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 = &wire.TxOut{ |
|
PkScript: info.PkScript, |
|
Value: int64(info.Value), |
|
} |
|
signDesc.InputIndex = i |
|
|
|
inputScript, err := l.Cfg.Signer.ComputeInputScript( |
|
fundingTx, &signDesc, |
|
) |
|
if err != nil { |
|
req.err <- err |
|
return |
|
} |
|
|
|
txIn.SignatureScript = inputScript.SigScript |
|
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 canonically sorted. If this is a single funder |
|
// workflow, then we'll also need to send this to the remote node. |
|
fundingTxID := fundingTx.TxHash() |
|
_, multiSigIndex := input.FindScriptOutputIndex(fundingTx, multiSigOut.PkScript) |
|
fundingOutpoint := wire.NewOutPoint(&fundingTxID, multiSigIndex) |
|
pendingReservation.partialState.FundingOutpoint = *fundingOutpoint |
|
|
|
walletLog.Debugf("Funding tx for ChannelPoint(%v) generated: %v", |
|
fundingOutpoint, spew.Sdump(fundingTx)) |
|
|
|
// 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: wire.OutPoint{ |
|
Hash: fundingTxID, |
|
Index: multiSigIndex, |
|
}, |
|
} |
|
|
|
// With the funding tx complete, create both commitment transactions. |
|
localBalance := pendingReservation.partialState.LocalCommitment.LocalBalance.ToSatoshis() |
|
remoteBalance := pendingReservation.partialState.LocalCommitment.RemoteBalance.ToSatoshis() |
|
ourCommitTx, theirCommitTx, err := CreateCommitmentTxns( |
|
localBalance, remoteBalance, ourContribution.ChannelConfig, |
|
theirContribution.ChannelConfig, |
|
ourContribution.FirstCommitmentPoint, |
|
theirContribution.FirstCommitmentPoint, fundingTxIn, |
|
) |
|
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 == channeldb.SingleFunder { |
|
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.Debugf("Local commit tx for ChannelPoint(%v): %v", |
|
fundingOutpoint, spew.Sdump(ourCommitTx)) |
|
walletLog.Debugf("Remote commit tx for ChannelPoint(%v): %v", |
|
fundingOutpoint, spew.Sdump(theirCommitTx)) |
|
|
|
// Record newly available information within the open channel state. |
|
chanState.FundingOutpoint = *fundingOutpoint |
|
chanState.LocalCommitment.CommitTx = ourCommitTx |
|
chanState.RemoteCommitment.CommitTx = theirCommitTx |
|
|
|
// Generate a signature for their version of the initial commitment |
|
// transaction. |
|
signDesc = input.SignDescriptor{ |
|
WitnessScript: witnessScript, |
|
KeyDesc: ourKey, |
|
Output: multiSigOut, |
|
HashType: txscript.SigHashAll, |
|
SigHashes: txscript.NewTxSigHashes(theirCommitTx), |
|
InputIndex: 0, |
|
} |
|
sigTheirCommit, err := l.Cfg.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-existent funding state") |
|
return |
|
} |
|
|
|
// Grab the mutex on the channelReservation to ensure thread-safety. |
|
pendingReservation.Lock() |
|
defer pendingReservation.Unlock() |
|
|
|
// TODO(roasbeef): verify sanity of remote party's parameters, fail if |
|
// disagree |
|
|
|
// 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 |
|
|
|
// 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 |
|
return |
|
} |
|
|
|
// openChanDetails contains a "finalized" channel which can be considered |
|
// "open" according to the requested confirmation depth at reservation |
|
// initialization. Additionally, the struct contains additional details |
|
// pertaining to the exact location in the main chain in-which the transaction |
|
// was confirmed. |
|
type openChanDetails struct { |
|
} |
|
|
|
// 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 |
|
fundingTx := res.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].SigScript |
|
|
|
// Fetch the alleged previous output along with the |
|
// pkscript referenced by this input. |
|
// |
|
// TODO(roasbeef): when dual funder pass actual |
|
// height-hint |
|
pkScript, err := input.WitnessScriptHash( |
|
txin.Witness[len(txin.Witness)-1], |
|
) |
|
if err != nil { |
|
msg.err <- fmt.Errorf("cannot create script: "+ |
|
"%v", err) |
|
msg.completeChan <- nil |
|
return |
|
} |
|
|
|
output, err := l.Cfg.ChainIO.GetUtxo( |
|
&txin.PreviousOutPoint, |
|
pkScript, 0, l.quit, |
|
) |
|
if output == nil { |
|
msg.err <- fmt.Errorf("input to funding tx "+ |
|
"does not exist: %v", err) |
|
msg.completeChan <- nil |
|
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 { |
|
msg.err <- fmt.Errorf("cannot create script "+ |
|
"engine: %s", err) |
|
msg.completeChan <- nil |
|
return |
|
} |
|
if err = vm.Execute(); err != nil { |
|
msg.err <- fmt.Errorf("cannot validate "+ |
|
"transaction: %s", err) |
|
msg.completeChan <- nil |
|
return |
|
} |
|
|
|
sigIndex++ |
|
} |
|
} |
|
|
|
// At this point, we can also record and verify their signature for our |
|
// commitment transaction. |
|
res.theirCommitmentSig = msg.theirCommitmentSig |
|
commitTx := res.partialState.LocalCommitment.CommitTx |
|
ourKey := res.ourContribution.MultiSigKey |
|
theirKey := res.theirContribution.MultiSigKey |
|
|
|
// Re-generate both the witnessScript and p2sh output. We sign the |
|
// witnessScript script, but include the p2sh output as the subscript |
|
// for verification. |
|
witnessScript, _, err := input.GenFundingPkScript( |
|
ourKey.PubKey.SerializeCompressed(), |
|
theirKey.PubKey.SerializeCompressed(), |
|
int64(res.partialState.Capacity), |
|
) |
|
if err != nil { |
|
msg.err <- err |
|
msg.completeChan <- nil |
|
return |
|
} |
|
|
|
// Next, create the spending scriptSig, and then verify that the script |
|
// is complete, allowing us to spend from the funding transaction. |
|
channelValue := int64(res.partialState.Capacity) |
|
hashCache := txscript.NewTxSigHashes(commitTx) |
|
sigHash, err := txscript.CalcWitnessSigHash(witnessScript, hashCache, |
|
txscript.SigHashAll, commitTx, 0, channelValue) |
|
if err != nil { |
|
msg.err <- err |
|
msg.completeChan <- nil |
|
return |
|
} |
|
|
|
// Verify that we've received a valid signature from the remote party |
|
// for our version of the commitment transaction. |
|
theirCommitSig := msg.theirCommitmentSig |
|
sig, err := btcec.ParseSignature(theirCommitSig, btcec.S256()) |
|
if err != nil { |
|
msg.err <- err |
|
msg.completeChan <- nil |
|
return |
|
} else if !sig.Verify(sigHash, theirKey.PubKey) { |
|
msg.err <- fmt.Errorf("counterparty's commitment signature is invalid") |
|
msg.completeChan <- nil |
|
return |
|
} |
|
res.partialState.LocalCommitment.CommitSig = theirCommitSig |
|
|
|
// Funding complete, this entry can be removed from limbo. |
|
l.limboMtx.Lock() |
|
delete(l.fundingLimbo, res.reservationID) |
|
l.limboMtx.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 |
|
|
|
// 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 |
|
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() |
|
ourCommitTx, theirCommitTx, err := CreateCommitmentTxns( |
|
localBalance, remoteBalance, |
|
pendingReservation.ourContribution.ChannelConfig, |
|
pendingReservation.theirContribution.ChannelConfig, |
|
pendingReservation.ourContribution.FirstCommitmentPoint, |
|
pendingReservation.theirContribution.FirstCommitmentPoint, |
|
*fundingTxIn, |
|
) |
|
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)) |
|
|
|
channelValue := int64(pendingReservation.partialState.Capacity) |
|
hashCache := txscript.NewTxSigHashes(ourCommitTx) |
|
theirKey := pendingReservation.theirContribution.MultiSigKey |
|
ourKey := pendingReservation.ourContribution.MultiSigKey |
|
witnessScript, _, err := input.GenFundingPkScript( |
|
ourKey.PubKey.SerializeCompressed(), |
|
theirKey.PubKey.SerializeCompressed(), channelValue, |
|
) |
|
if err != nil { |
|
req.err <- err |
|
req.completeChan <- nil |
|
return |
|
} |
|
|
|
sigHash, err := txscript.CalcWitnessSigHash(witnessScript, hashCache, |
|
txscript.SigHashAll, ourCommitTx, 0, channelValue) |
|
if err != nil { |
|
req.err <- err |
|
req.completeChan <- nil |
|
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 |
|
req.completeChan <- nil |
|
return |
|
} else if !sig.Verify(sigHash, theirKey.PubKey) { |
|
req.err <- fmt.Errorf("counterparty's commitment signature " + |
|
"is invalid") |
|
req.completeChan <- nil |
|
return |
|
} |
|
chanState.LocalCommitment.CommitSig = 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 := input.WitnessScriptHash(witnessScript) |
|
if err != nil { |
|
req.err <- err |
|
req.completeChan <- nil |
|
return |
|
} |
|
signDesc := input.SignDescriptor{ |
|
WitnessScript: witnessScript, |
|
KeyDesc: ourKey, |
|
Output: &wire.TxOut{ |
|
PkScript: p2wsh, |
|
Value: channelValue, |
|
}, |
|
HashType: txscript.SigHashAll, |
|
SigHashes: txscript.NewTxSigHashes(theirCommitTx), |
|
InputIndex: 0, |
|
} |
|
sigTheirCommit, err := l.Cfg.Signer.SignOutputRaw(theirCommitTx, &signDesc) |
|
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 |
|
} |
|
|
|
// 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() |
|
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) |
|
l.limboMtx.Unlock() |
|
} |
|
|
|
// WithCoinSelectLock will execute the passed function closure in a |
|
// synchronized manner preventing any coin selection operations from proceeding |
|
// while the closure if 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() |
|
} |
|
|
|
// coinSelection holds the result from selectCoinsAndChange. |
|
type coinSelection struct { |
|
coins []*wire.TxIn |
|
change []*wire.TxOut |
|
fundingAmt btcutil.Amount |
|
unlockCoins func() |
|
} |
|
|
|
// selectCoinsAndChange performs coin selection in order to obtain witness |
|
// outputs which sum to at least 'amt' amount of satoshis. If necessary, |
|
// a change address will also be generated. If coin selection is |
|
// successful/possible, then the selected coins and change outputs are |
|
// returned, and the value of the resulting funding output. This method locks |
|
// the selected outputs, and a function closure to unlock them in case of an |
|
// error is returned. |
|
func (l *LightningWallet) selectCoinsAndChange(feeRate SatPerKWeight, |
|
amt btcutil.Amount, minConfs int32, subtractFees bool) ( |
|
*coinSelection, error) { |
|
|
|
// We hold the coin select mutex while querying for outputs, and |
|
// performing coin selection in order to avoid inadvertent double |
|
// spends across funding transactions. |
|
l.coinSelectMtx.Lock() |
|
defer l.coinSelectMtx.Unlock() |
|
|
|
walletLog.Infof("Performing funding tx coin selection using %v "+ |
|
"sat/kw as fee rate", int64(feeRate)) |
|
|
|
// Find all unlocked unspent witness outputs that satisfy the minimum |
|
// number of confirmations required. |
|
coins, err := l.ListUnspentWitness(minConfs, math.MaxInt32) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
var ( |
|
selectedCoins []*Utxo |
|
fundingAmt btcutil.Amount |
|
changeAmt btcutil.Amount |
|
) |
|
|
|
// Perform coin selection over our available, unlocked unspent outputs |
|
// in order to find enough coins to meet the funding amount |
|
// requirements. |
|
switch { |
|
// In case this request want the fees subtracted from the local amount, |
|
// we'll call the specialized method for that. This ensures that we |
|
// won't deduct more that the specified balance from our wallet. |
|
case subtractFees: |
|
dustLimit := l.Cfg.DefaultConstraints.DustLimit |
|
selectedCoins, fundingAmt, changeAmt, err = coinSelectSubtractFees( |
|
feeRate, amt, dustLimit, coins, |
|
) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// Ótherwise do a normal coin selection where we target a given funding |
|
// amount. |
|
default: |
|
fundingAmt = amt |
|
selectedCoins, changeAmt, err = coinSelect(feeRate, amt, coins) |
|
if err != nil { |
|
return nil, err |
|
} |
|
} |
|
|
|
// Record any change output(s) generated as a result of the coin |
|
// selection, but only if the addition of the output won't lead to the |
|
// creation of dust. |
|
var changeOutputs []*wire.TxOut |
|
if changeAmt != 0 && changeAmt > DefaultDustLimit() { |
|
changeAddr, err := l.NewAddress(WitnessPubKey, true) |
|
if err != nil { |
|
return nil, err |
|
} |
|
changeScript, err := txscript.PayToAddrScript(changeAddr) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
changeOutputs = make([]*wire.TxOut, 1) |
|
changeOutputs[0] = &wire.TxOut{ |
|
Value: int64(changeAmt), |
|
PkScript: changeScript, |
|
} |
|
} |
|
|
|
// 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. |
|
inputs := make([]*wire.TxIn, len(selectedCoins)) |
|
for i, coin := range selectedCoins { |
|
outpoint := &coin.OutPoint |
|
l.lockedOutPoints[*outpoint] = struct{}{} |
|
l.LockOutpoint(*outpoint) |
|
|
|
// Empty sig script, we'll actually sign if this reservation is |
|
// queued up to be completed (the other side accepts). |
|
inputs[i] = wire.NewTxIn(outpoint, nil, nil) |
|
} |
|
|
|
unlock := func() { |
|
l.coinSelectMtx.Lock() |
|
defer l.coinSelectMtx.Unlock() |
|
|
|
for _, coin := range selectedCoins { |
|
outpoint := &coin.OutPoint |
|
delete(l.lockedOutPoints, *outpoint) |
|
l.UnlockOutpoint(*outpoint) |
|
} |
|
} |
|
|
|
return &coinSelection{ |
|
coins: inputs, |
|
change: changeOutputs, |
|
fundingAmt: fundingAmt, |
|
unlockCoins: unlock, |
|
}, nil |
|
} |
|
|
|
// 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 |
|
} |
|
|
|
// selectInputs selects a slice of inputs necessary to meet the specified |
|
// selection amount. If input selection is unable to succeed due to insufficient |
|
// 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, []*Utxo, error) { |
|
satSelected := btcutil.Amount(0) |
|
for i, coin := range coins { |
|
satSelected += coin.Value |
|
if satSelected >= amt { |
|
return satSelected, coins[:i+1], nil |
|
} |
|
} |
|
return 0, nil, &ErrInsufficientFunds{amt, satSelected} |
|
} |
|
|
|
// coinSelect attempts to select a sufficient amount of coins, including a |
|
// change output to fund amt satoshis, adhering to the specified fee rate. The |
|
// specified fee rate should be expressed in sat/kw for coin selection to |
|
// function properly. |
|
func coinSelect(feeRate SatPerKWeight, amt btcutil.Amount, |
|
coins []*Utxo) ([]*Utxo, btcutil.Amount, error) { |
|
|
|
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 |
|
} |
|
|
|
var weightEstimate input.TxWeightEstimator |
|
|
|
for _, utxo := range selectedUtxos { |
|
switch utxo.AddressType { |
|
case WitnessPubKey: |
|
weightEstimate.AddP2WKHInput() |
|
case NestedWitnessPubKey: |
|
weightEstimate.AddNestedP2WKHInput() |
|
default: |
|
return nil, 0, fmt.Errorf("unsupported address type: %v", |
|
utxo.AddressType) |
|
} |
|
} |
|
|
|
// Channel funding multisig output is P2WSH. |
|
weightEstimate.AddP2WSHOutput() |
|
|
|
// Assume that change output is a P2WKH output. |
|
// |
|
// TODO: Handle wallets that generate non-witness change |
|
// addresses. |
|
// TODO(halseth): make coinSelect not estimate change output |
|
// for dust change. |
|
weightEstimate.AddP2WKHOutput() |
|
|
|
// The difference between the selected amount and the amount |
|
// requested will be used to pay fees, and generate a change |
|
// output with the remaining. |
|
overShootAmt := totalSat - amt |
|
|
|
// 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. |
|
totalWeight := int64(weightEstimate.Weight()) |
|
requiredFee := feeRate.FeeForWeight(totalWeight) |
|
if overShootAmt < requiredFee { |
|
amtNeeded = amt + requiredFee |
|
continue |
|
} |
|
|
|
// If the fee is sufficient, then calculate the size of the |
|
// change output. |
|
changeAmt := overShootAmt - requiredFee |
|
|
|
return selectedUtxos, changeAmt, nil |
|
} |
|
} |
|
|
|
// coinSelectSubtractFees attempts to select coins such that we'll spend up to |
|
// amt in total after fees, adhering to the specified fee rate. The selected |
|
// coins, the final output and change values are returned. |
|
func coinSelectSubtractFees(feeRate SatPerKWeight, amt, |
|
dustLimit btcutil.Amount, coins []*Utxo) ([]*Utxo, btcutil.Amount, |
|
btcutil.Amount, error) { |
|
|
|
// First perform an initial round of coin selection to estimate |
|
// the required fee. |
|
totalSat, selectedUtxos, err := selectInputs(amt, coins) |
|
if err != nil { |
|
return nil, 0, 0, err |
|
} |
|
|
|
var weightEstimate input.TxWeightEstimator |
|
for _, utxo := range selectedUtxos { |
|
switch utxo.AddressType { |
|
case WitnessPubKey: |
|
weightEstimate.AddP2WKHInput() |
|
case NestedWitnessPubKey: |
|
weightEstimate.AddNestedP2WKHInput() |
|
default: |
|
return nil, 0, 0, fmt.Errorf("unsupported "+ |
|
"address type: %v", utxo.AddressType) |
|
} |
|
} |
|
|
|
// Channel funding multisig output is P2WSH. |
|
weightEstimate.AddP2WSHOutput() |
|
|
|
// At this point we've got two possibilities, either create a |
|
// change output, or not. We'll first try without creating a |
|
// change output. |
|
// |
|
// Estimate the fee required for a transaction without a change |
|
// output. |
|
totalWeight := int64(weightEstimate.Weight()) |
|
requiredFee := feeRate.FeeForWeight(totalWeight) |
|
|
|
// For a transaction without a change output, we'll let everything go |
|
// to our multi-sig output after subtracting fees. |
|
outputAmt := totalSat - requiredFee |
|
changeAmt := btcutil.Amount(0) |
|
|
|
// If the the output is too small after subtracting the fee, the coin |
|
// selection cannot be performed with an amount this small. |
|
if outputAmt <= dustLimit { |
|
return nil, 0, 0, fmt.Errorf("output amount(%v) after "+ |
|
"subtracting fees(%v) below dust limit(%v)", outputAmt, |
|
requiredFee, dustLimit) |
|
} |
|
|
|
// We were able to create a transaction with no change from the |
|
// selected inputs. We'll remember the resulting values for |
|
// now, while we try to add a change output. Assume that change output |
|
// is a P2WKH output. |
|
weightEstimate.AddP2WKHOutput() |
|
|
|
// Now that we have added the change output, redo the fee |
|
// estimate. |
|
totalWeight = int64(weightEstimate.Weight()) |
|
requiredFee = feeRate.FeeForWeight(totalWeight) |
|
|
|
// For a transaction with a change output, everything we don't spend |
|
// will go to change. |
|
newChange := totalSat - amt |
|
newOutput := amt - requiredFee |
|
|
|
// If adding a change output leads to both outputs being above |
|
// the dust limit, we'll add the change output. Otherwise we'll |
|
// go with the no change tx we originally found. |
|
if newChange > dustLimit && newOutput > dustLimit { |
|
outputAmt = newOutput |
|
changeAmt = newChange |
|
} |
|
|
|
// Sanity check the resulting output values to make sure we |
|
// don't burn a great part to fees. |
|
totalOut := outputAmt + changeAmt |
|
fee := totalSat - totalOut |
|
|
|
// Fail if more than 20% goes to fees. |
|
// TODO(halseth): smarter fee limit. Make configurable or dynamic wrt |
|
// total funding size? |
|
if fee > totalOut/5 { |
|
return nil, 0, 0, fmt.Errorf("fee %v on total output"+ |
|
"value %v", fee, totalOut) |
|
} |
|
|
|
return selectedUtxos, outputAmt, changeAmt, nil |
|
}
|
|
|