initial draft of ln-wallet workflow, funding reservations implemented

This commit is contained in:
Olaoluwa Osuntokun 2015-10-27 15:50:30 -07:00
parent 85e1c7b91c
commit 0b66ed24be
2 changed files with 285 additions and 0 deletions

284
wallet/wallet.go Normal file

@ -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?
}

1
wallet/wallet_test.go Normal file

@ -0,0 +1 @@
package wallet