initial draft of ln-wallet workflow, funding reservations implemented
This commit is contained in:
parent
85e1c7b91c
commit
0b66ed24be
284
wallet/wallet.go
Normal file
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
1
wallet/wallet_test.go
Normal file
@ -0,0 +1 @@
|
||||
package wallet
|
Loading…
Reference in New Issue
Block a user