diff --git a/wallet/wallet.go b/wallet/wallet.go new file mode 100644 index 00000000..0b38fea0 --- /dev/null +++ b/wallet/wallet.go @@ -0,0 +1,284 @@ +package wallet + +import ( + "encoding/hex" + "errors" + "sync" + "sync/atomic" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/coinset" + "github.com/btcsuite/btcwallet/waddrmgr" + btcwallet "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/walletdb" +) + +var ( + // Namespace bucket keys. + lightningNamespaceKey = []byte("lightning") + + // Error types + ErrInsufficientFunds = errors.New("not enough available outputs to create funding transaction") +) + +// FundingRequest... +type FundingReserveRequest struct { + fundingAmount btcutil.Amount + + // Insuffcient funds etc.. + err chan error // Buffered + + resp chan *FundingReserveResponse // Buffered +} + +// FundingResponse... +type FundingReserveResponse struct { + fundingInputs []*wire.TxIn + changeOutputs []*wire.TxOut +} + +// FundingReserveCancelMsg... +type FundingReserveCancelMsg struct { + reserveToCancel *FundingReserveResponse +} + +// FundingReserveRequest... +type FundingCompleteRequest struct { + fundingReservation *FundingReserveResponse + + theirInputs []*wire.TxIn // Includes sig-script to redeem. + theirChangeOutputs []*wire.TxOut + theirKey *btcec.PublicKey + + err chan error // Buffered + + resp chan FundingCompleteResponse // Buffered +} + +// FundingCompleteResponse.... +type FundingCompleteResponse struct { + fundingTx *btcutil.Tx +} + +// LightningWallet.... +// responsible for internal global (from the point of view of a user/node) +// channel state. Requests to modify this state come in via messages over +// channels, same with replies. +// Embedded wallet backed by boltdb... +type LightningWallet struct { + sync.RWMutex + + db *walletdb.DB + + // A namespace within boltdb reserved for ln-based wallet meta-data. + // TODO(roasbeef): which possible other namespaces are relevant? + lnNamespace *walletdb.Namespace + + btcwallet.Wallet + + msgChan chan interface{} + + //lockedInputs []*LockedPrevOut + //lockedOutputs []*LockedOutPoint + + started int32 + shutdown int32 + + quit chan struct{} + + wg sync.WaitGroup +} + +// Start... +func (l *LightningWallet) Start() error { + // Already started? + if atomic.AddInt32(&l.started, 1) != 1 { + return nil + } + + l.wg.Add(1) + go l.requestHandler() + + return nil +} + +// Stop... +func (l *LightningWallet) Stop() error { + if atomic.AddInt32(&l.shutdown, 1) != 1 { + return nil + } + + close(l.quit) + l.wg.Wait() + return nil +} + +// requestHandler.... +func (l *LightningWallet) requestHandler() { +out: + for { + select { + case m := <-l.msgChan: + switch msg := m.(type) { + case *FundingReserveRequest: + l.handleFundingReserveRequest(msg) + case *FundingReserveCancelMsg: + l.handleFundingCancelRequest(msg) + } + case <-l.quit: + // TODO: do some clean up + break out + } + } + + l.wg.Done() +} + +// handleFundingReserveRequest... +func (l *LightningWallet) handleFundingReserveRequest(req *FundingReserveRequest) { + fundingAmt := req.fundingAmount + + // Find all unlocked unspent outputs with greater than 6 confirmations. + maxConfs := ^int32(0) + unspentOutputs, err := l.ListUnspent(6, maxConfs, nil) + if err != nil { + req.err <- err + req.resp <- nil + return + } + + // Convert the outputs to coins for coin selection below. + coins, err := outputsToCoins(unspentOutputs) + if err != nil { + req.err <- err + req.resp <- nil + return + } + + // Peform coin selection over our available, unlocked unspent outputs + // in order to find enough coins to meet the funding amount requirements. + // + // TODO(roasbeef): Should extend coinset with optimal coin selection + // heuristics for our use case. + // TODO(roasbeef): factor in fees.. + // NOTE: this current selection assumes "priority" is still a thing. + selector := &coinset.MaxValueAgeCoinSelector{ + MaxInputs: 10, + MinChangeAmount: 10000, + } + selectedCoins, err := selector.CoinSelect(fundingAmt, coins) + + // 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. + fundingInputs := make([]*wire.TxIn, len(selectedCoins.Coins())) + for i, coin := range selectedCoins.Coins() { + txout := wire.NewOutPoint(coin.Hash(), coin.Index()) + l.LockOutpoint(*txout) + + // Empty sig script, we'll actually sign if this reservation is + // queued up to be completed (the other side accepts). + outPoint := wire.NewOutPoint(coin.Hash(), coin.Index()) + fundingInputs[i] = wire.NewTxIn(outPoint, nil) + } + + // Create some possibly neccessary change outputs. + selectedTotalValue := coinset.NewCoinSet(coins).TotalValue() + changeOutputs := make([]*wire.TxOut, 0, len(selectedCoins.Coins())) + if selectedTotalValue > fundingAmt { + // Change is necessary. Query for an available change address to + // send the remainder to. + changeAmount := selectedTotalValue - fundingAmt + changeAddr, err := l.NewChangeAddress(waddrmgr.DefaultAccountNum) + if err != nil { + req.err <- err + req.resp <- nil + } + + changeOutputs = append(changeOutputs, + wire.NewTxOut(int64(changeAmount), changeAddr.ScriptAddress())) + } + + // Funding reservation request succesfully handled. The funding inputs + // will be marked as unavailable until the reservation is either + // completed, or cancecled. + req.err <- nil + req.resp <- &FundingReserveResponse{ + fundingInputs: fundingInputs, + changeOutputs: changeOutputs, + } +} + +// lnCoin... +// to adhere to the coinset.Coin interface +type lnCoin struct { + hash *wire.ShaHash + index uint32 + value btcutil.Amount + pkScript []byte + numConfs int64 + valueAge int64 +} + +func (l *lnCoin) Hash() *wire.ShaHash { return l.hash } +func (l *lnCoin) Index() uint32 { return l.index } +func (l *lnCoin) Value() btcutil.Amount { return l.value } +func (l *lnCoin) PkScript() []byte { return l.pkScript } +func (l *lnCoin) NumConfs() int64 { return l.numConfs } +func (l *lnCoin) ValueAge() int64 { return l.valueAge } + +// Ensure lnCoin adheres to the coinset.Coin interface. +var _ coinset.Coin = (*lnCoin)(nil) + +// newLnCoin... +func newLnCoin(output *btcjson.ListUnspentResult) (coinset.Coin, error) { + txid, err := wire.NewShaHashFromStr(output.TxID) + if err != nil { + return nil, err + } + + pkScript, err := hex.DecodeString(output.ScriptPubKey) + if err != nil { + return nil, err + } + + return &lnCoin{ + hash: txid, + value: btcutil.Amount(output.Amount), + index: output.Vout, + pkScript: pkScript, + numConfs: output.Confirmations, + // TODO(roasbeef) outpout.Amount should be a int64 :/ + valueAge: output.Confirmations * int64(output.Amount), + }, nil +} + +// outputsToCoins... +func outputsToCoins(outputs []*btcjson.ListUnspentResult) ([]coinset.Coin, error) { + coins := make([]coinset.Coin, len(outputs)) + for i, output := range outputs { + coin, err := newLnCoin(output) + if err != nil { + return nil, err + } + + coins[i] = coin + } + + return coins, nil +} + +// handleFundingReserveCancel... +func (l *LightningWallet) handleFundingCancelRequest(req *FundingReserveCancelMsg) { + prevReservation := req.reserveToCancel + + for _, unusedInput := range prevReservation.fundingInputs { + l.UnlockOutpoint(unusedInput.PreviousOutPoint) + } + + // TODO(roasbeef): Is it possible to mark the unused change also as + // available? +} diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go new file mode 100644 index 00000000..23a75073 --- /dev/null +++ b/wallet/wallet_test.go @@ -0,0 +1 @@ +package wallet