rename wallet to lnwallet

This commit is contained in:
Olaoluwa Osuntokun committed 2015-12-16 14:51:59 -06:00
1 parent 9de294c31f
commit 147748d178
7 files changed
+7 -1

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
package wallet
import (
"sync"
"li.lan/labs/plasma/chainntfs"
"li.lan/labs/plasma/revocation"
"github.com/btcsuite/btcwallet/walletdb"
)
type LightningChannel struct {
shachan *revocation.HyperShaChain
wallet *LightningWallet
channelEvents *chainntnfs.ChainNotifier
sync.RWMutex
channelNamespace walletdb.Namespace
// TODO(roasbeef): create and embed 'Service' interface w/ below?
started int32
shutdown int32
quit chan struct{}
wg sync.WaitGroup
}
func newLightningChannel() *LightningChannel {
return nil
}
+71
View File
@@ -0,0 +1,71 @@
package wallet
import (
"encoding/hex"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/coinset"
)
// 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,
// btcjson.ListUnspentResult shows the amount in BTC,
// translate into Satoshi so coin selection can work properly.
value: btcutil.Amount(output.Amount * 1e8),
index: output.Vout,
pkScript: pkScript,
numConfs: output.Confirmations,
// TODO(roasbeef): output.Amount should be a int64, damn json-RPC :/
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
}
+143
View File
@@ -0,0 +1,143 @@
package wallet
import (
"sync"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
)
// ChannelReservation...
type ChannelReservation struct {
FundingType FundingType
FundingAmount btcutil.Amount
ReserveAmount btcutil.Amount
MinFeePerKb btcutil.Amount
sync.RWMutex // All fields below owned by the lnwallet.
theirInputs []*wire.TxIn
ourInputs []*wire.TxIn
theirChange []*wire.TxOut
ourChange []*wire.TxOut
ourKey *btcec.PrivateKey
theirKey *btcec.PublicKey
// In order of sorted inputs. Sorting is done in accordance
// to BIP-69: https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki.
theirSigs [][]byte
ourSigs [][]byte
normalizedTxID wire.ShaHash
fundingTx *wire.MsgTx
// TODO(roasbef): time locks, who pays fee etc.
// TODO(roasbeef): record Bob's ln-ID?
completedFundingTx *btcutil.Tx
reservationID uint64
wallet *LightningWallet
chanOpen chan *LightningChannel
}
// newChannelReservation...
func newChannelReservation(t FundingType, fundingAmt btcutil.Amount,
minFeeRate btcutil.Amount, wallet *LightningWallet, id uint64) *ChannelReservation {
return &ChannelReservation{
FundingType: t,
FundingAmount: fundingAmt,
MinFeePerKb: minFeeRate,
wallet: wallet,
reservationID: id,
}
}
// OurFunds...
func (r *ChannelReservation) OurFunds() ([]*wire.TxIn, []*wire.TxOut, *btcec.PublicKey) {
r.RLock()
defer r.RUnlock()
return r.ourInputs, r.ourChange, r.ourKey.PubKey()
}
// AddCounterPartyFunds...
func (r *ChannelReservation) AddFunds(theirInputs []*wire.TxIn, theirChangeOutputs []*wire.TxOut, multiSigKey *btcec.PublicKey) error {
errChan := make(chan error, 1)
r.wallet.msgChan <- &addCounterPartyFundsMsg{
pendingFundingID: r.reservationID,
theirInputs: theirInputs,
theirChangeOutputs: theirChangeOutputs,
theirKey: multiSigKey,
err: errChan,
}
return <-errChan
}
// OurSigs...
func (r *ChannelReservation) OurSigs() [][]byte {
r.RLock()
defer r.RUnlock()
return r.ourSigs
}
// TheirFunds...
// TODO(roasbeef): return error if accessors not yet populated?
func (r *ChannelReservation) TheirFunds() ([]*wire.TxIn, []*wire.TxOut, *btcec.PublicKey) {
r.RLock()
defer r.RUnlock()
return r.theirInputs, r.theirChange, r.theirKey
}
// CompleteFundingReservation...
func (r *ChannelReservation) CompleteReservation(theirSigs [][]byte) error {
errChan := make(chan error, 1)
r.wallet.msgChan <- &addCounterPartySigsMsg{
pendingFundingID: r.reservationID,
theirSigs: theirSigs,
err: errChan,
}
return <-errChan
}
// FinalFundingTransaction...
func (r *ChannelReservation) FinalFundingTx() *btcutil.Tx {
r.RLock()
defer r.RUnlock()
return r.completedFundingTx
}
// RequestFundingReserveCancellation...
// TODO(roasbeef): also return mutated state?
func (r *ChannelReservation) Cancel() error {
errChan := make(chan error, 1)
r.wallet.msgChan <- &fundingReserveCancelMsg{
pendingFundingID: r.reservationID,
err: errChan,
}
return <-errChan
}
// WaitForChannelOpen...
func (r *ChannelReservation) WaitForChannelOpen() *LightningChannel {
return nil
}
// * finish reset of tests
// * comment out stuff that'll need a node.
// * start on commitment side
// * implement rusty's shachain
// * set up logic to get notification from node when funding tx gets 6 deep.
// * prob spawn into ChainNotifier struct
// * create builder for initial funding transaction
// * fascade through the wallet, for signing and such.
// * channel should have active namespace to it's bucket, query at that point fo past commits etc
+236
View File
@@ -0,0 +1,236 @@
// Based on: https://github.com/btcsuite/btcwallet/blob/master/walletsetup.go
/*
* Copyright (c) 2014-2015 The btcsuite developers
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package wallet
import (
"bufio"
"bytes"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/crypto/ssh/terminal"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
)
var (
// TODO(roasbeef): lnwallet config file
lnwalletHomeDir = btcutil.AppDataDir("lnwallet", false)
defaultDataDir = lnwalletHomeDir
defaultLogFilename = "lnwallet.log"
defaultLogDirname = "logs"
// defaultPubPassphrase is the default public wallet passphrase which is
// used when the user indicates they do not want additional protection
// provided by having all public data in the wallet encrypted by a
// passphrase only known to them.
defaultPubPassphrase = []byte("public")
defaultLogDir = filepath.Join(lnwalletHomeDir, defaultLogDirname)
walletDbName = "lnwallet.db"
)
// filesExists reports whether the named file or directory exists.
func fileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
// networkDir returns the directory name of a network directory to hold wallet
// files.
func networkDir(dataDir string, chainParams *chaincfg.Params) string {
netname := chainParams.Name
// For now, we must always name the testnet data directory as "testnet"
// and not "testnet3" or any other version, as the chaincfg testnet3
// paramaters will likely be switched to being named "testnet3" in the
// future. This is done to future proof that change, and an upgrade
// plan to move the testnet3 data directory can be worked out later.
if chainParams.Net == wire.TestNet3 {
netname = "testnet"
}
return filepath.Join(dataDir, netname)
}
// checkCreateDir checks that the path exists and is a directory.
// If path does not exist, it is created.
func checkCreateDir(path string) error {
if fi, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
// Attempt data directory creation
if err = os.MkdirAll(path, 0700); err != nil {
return fmt.Errorf("cannot create directory: %s", err)
}
} else {
return fmt.Errorf("error checking directory: %s", err)
}
} else {
if !fi.IsDir() {
return fmt.Errorf("path '%s' is not a directory", path)
}
}
return nil
}
// createWallet generates a new wallet. The new wallet will reside at the
// provided path.
// TODO(roasbeef): maybe pass in config after all for testing purposes?
func createWallet(privPass, pubPass, userSeed []byte,
dbPath string) error {
// TODO(roasbeef): replace with tadge's seed format?
hdSeed := userSeed
var seedErr error
if userSeed == nil {
hdSeed, seedErr = hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen)
if seedErr != nil {
return seedErr
}
}
// Create the wallet.
fmt.Println("Creating the wallet...")
// Create the wallet database backed by bolt db.
db, err := walletdb.Create("bdb", dbPath)
if err != nil {
return err
}
// Create the address manager.
namespace, err := db.Namespace(waddrmgrNamespaceKey)
if err != nil {
return err
}
manager, err := waddrmgr.Create(namespace, hdSeed, []byte(pubPass),
[]byte(privPass), ActiveNetParams, nil)
if err != nil {
return err
}
manager.Close()
fmt.Println("The lnwallet has been created successfully.")
return nil
}
// openDb opens and returns a walletdb.DB (boltdb here) given the directory and
// dbname
func openDb(directory string, dbname string) (walletdb.DB, error) {
dbPath := filepath.Join(directory, dbname)
// Ensure that the network directory exists.
if err := checkCreateDir(directory); err != nil {
return nil, err
}
// Open the database using the boltdb backend.
return walletdb.Open("bdb", dbPath)
}
// promptSeed is used to prompt for the wallet seed which maybe required during
// upgrades.
func promptSeed() ([]byte, error) {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("Enter existing wallet seed: ")
seedStr, err := reader.ReadString('\n')
if err != nil {
return nil, err
}
seedStr = strings.TrimSpace(strings.ToLower(seedStr))
seed, err := hex.DecodeString(seedStr)
if err != nil || len(seed) < hdkeychain.MinSeedBytes ||
len(seed) > hdkeychain.MaxSeedBytes {
fmt.Printf("Invalid seed specified. Must be a "+
"hexadecimal value that is at least %d bits and "+
"at most %d bits\n", hdkeychain.MinSeedBytes*8,
hdkeychain.MaxSeedBytes*8)
continue
}
return seed, nil
}
}
// promptPrivPassPhrase is used to prompt for the private passphrase which maybe
// required during upgrades.
func promptPrivPassPhrase() ([]byte, error) {
prompt := "Enter the private passphrase of your wallet: "
for {
fmt.Print(prompt)
pass, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return nil, err
}
fmt.Print("\n")
pass = bytes.TrimSpace(pass)
if len(pass) == 0 {
continue
}
return pass, nil
}
}
// openWallet returns a wallet. The function handles opening an existing wallet
// database, the address manager and the transaction store and uses the values
// to open a wallet.Wallet
func openWallet(pubPass []byte, dbDir string) (*wallet.Wallet, walletdb.DB, error) {
db, err := openDb(dbDir, walletDbName)
if err != nil {
return nil, nil, fmt.Errorf("Failed to open database: %v", err)
}
addrMgrNS, err := db.Namespace(waddrmgrNamespaceKey)
if err != nil {
return nil, nil, err
}
txMgrNS, err := db.Namespace(wtxmgrNamespaceKey)
if err != nil {
return nil, nil, err
}
// TODO(roasbeef): pass these in as funcs instead, priv pass already
// loaded into memory, use tadge's format to read HD seed.
cbs := &waddrmgr.OpenCallbacks{
ObtainSeed: promptSeed,
ObtainPrivatePass: promptPrivPassPhrase,
}
w, err := wallet.Open(pubPass, ActiveNetParams, db, addrMgrNS, txMgrNS,
cbs)
return w, db, err
}
+685
View File
@@ -0,0 +1,685 @@
package wallet
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math"
"os"
"path/filepath"
"sync"
"sync/atomic"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/coinset"
"github.com/btcsuite/btcutil/txsort"
"github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/waddrmgr"
btcwallet "github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb"
)
const (
msgBufferSize = 100
)
var (
// Namespace bucket keys.
lightningNamespaceKey = []byte("ln-wallet")
waddrmgrNamespaceKey = []byte("waddrmgr")
wtxmgrNamespaceKey = []byte("wtxmgr")
openChannelBucket = []byte("o-chans")
closedChannelBucket = []byte("c-chans")
fundingTxKey = []byte("funding")
// Error types
ErrInsufficientFunds = errors.New("not enough available outputs to " +
"create funding transaction")
// Which bitcoin network are we using?
ActiveNetParams = &chaincfg.TestNet3Params
endian = binary.BigEndian
)
type FundingType uint16
const (
// Use SegWit, assumes CSV+CLTV
SEGWIT FundingType = iota
// Use SIGHASH_NOINPUT, assumes CSV+CLTV
SIGHASH
// Use CSV without reserve
CSV
// Use CSV with reserve
// Reserve is a permanent amount of funds locked and the capacity.
CSV_RESERVE
// CLTV with reserve.
CLTV_RESERVE
)
// initFundingReserveReq...
type initFundingReserveMsg struct {
fundingAmount btcutil.Amount
fundingType FundingType
minFeeRate btcutil.Amount
// TODO(roasbeef): optional reserve for CLTV, etc.
// Insuffcient funds etc..
err chan error // Buffered
resp chan *ChannelReservation // Buffered
}
// FundingReserveCancelMsg...
type fundingReserveCancelMsg struct {
pendingFundingID uint64
// Buffered, used for optionally synchronization.
err chan error // Buffered
}
// addCounterPartySigsMsg...
type addCounterPartyFundsMsg struct {
pendingFundingID uint64
// TODO(roasbeef): Should also carry SPV proofs in we're in SPV mode
theirInputs []*wire.TxIn
theirChangeOutputs []*wire.TxOut
theirKey *btcec.PublicKey
err chan error // Buffered
}
// partiallySignedFundingState...
type partiallySignedFundingState struct {
// In order of sorted inputs that are ours. Sorting is done in accordance
// to BIP-69: https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki.
OurSigs [][]byte
NormalizedTxID wire.ShaHash
}
// addCounterPartySigsMsg...
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.
theirSigs [][]byte
err chan error // Buffered
}
// FundingCompleteResp...
type finalizedFundingState struct {
FundingTxId wire.ShaHash
NormalizedFundingTXID wire.ShaHash
CompletedFundingTx *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 {
// TODO(roasbeef): add btcwallet/chain for notifications initially, then
// abstract out in order to accomodate zeroMQ/BitcoinCore
lmtx 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
wallet *btcwallet.Wallet
rpc *chain.Client
msgChan chan interface{}
// TODO(roasbeef): zombie garbage collection routine to solve
// lost-object/starvation problem/attack.
limboMtx sync.RWMutex
nextFundingID uint64
fundingLimbo map[uint64]*ChannelReservation
started int32
shutdown int32
quit chan struct{}
wg sync.WaitGroup
// TODO(roasbeef): handle wallet lock/unlock
}
// NewLightningWallet...
// TODO(roasbeef): fin...add config
func NewLightningWallet(privWalletPass, pubWalletPass, hdSeed []byte, dataDir string) (*LightningWallet, error) {
// Ensure the wallet exists or create it when the create flag is set.
netDir := networkDir(dataDir, ActiveNetParams)
dbPath := filepath.Join(netDir, walletDbName)
var pubPass []byte
if pubWalletPass == nil {
pubPass = defaultPubPassphrase
} else {
pubPass = pubWalletPass
}
// Wallet has never been created, perform initial set up.
if !fileExists(dbPath) {
// Ensure the data directory for the network exists.
if err := checkCreateDir(netDir); err != nil {
fmt.Fprintln(os.Stderr, err)
return nil, err
}
// Attempt to create a new wallet
if err := createWallet(privWalletPass, pubPass, hdSeed, dbPath); err != nil {
fmt.Fprintln(os.Stderr, err)
return nil, err
}
}
// Wallet has been created and been initialized at this point, open it
// along with all the required DB namepsaces, and the DB itself.
wallet, db, err := openWallet(pubPass, netDir)
if err != nil {
return nil, err
}
// Create a special namespace for our unique payment channel related
// meta-data.
lnNamespace, err := db.Namespace(lightningNamespaceKey)
if err != nil {
return nil, err
}
// TODO(roasbeef): logging
return &LightningWallet{
db: db,
wallet: wallet,
lnNamespace: lnNamespace,
msgChan: make(chan interface{}, msgBufferSize),
// TODO(roasbeef): make this atomic.Uint32 instead? Which is
// faster, locks or CAS? I'm guessing CAS because assembly:
// * https://golang.org/src/sync/atomic/asm_amd64.s
nextFundingID: 0,
fundingLimbo: make(map[uint64]*ChannelReservation),
quit: make(chan struct{}),
}, nil
}
// Start...
func (l *LightningWallet) Start() error {
// Already started?
if atomic.AddInt32(&l.started, 1) != 1 {
return nil
}
// TODO(roasbeef): config...
rpcc, err := chain.NewClient(ActiveNetParams,
"host", "user", "pass", []byte("cert"), true)
if err != nil {
return err
}
// Start the goroutines in the underlying wallet.
l.rpc = rpcc
l.wallet.Start(rpcc)
l.wg.Add(1)
// TODO(roasbeef): multiple request handlers?
go l.requestHandler()
return nil
}
// Stop...
func (l *LightningWallet) Stop() error {
if atomic.AddInt32(&l.shutdown, 1) != 1 {
return nil
}
l.wallet.Stop()
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 *initFundingReserveMsg:
l.handleFundingReserveRequest(msg)
case *fundingReserveCancelMsg:
l.handleFundingCancelRequest(msg)
case *addCounterPartyFundsMsg:
l.handleFundingCounterPartyFunds(msg)
case *addCounterPartySigsMsg:
l.handleFundingCounterPartySigs(msg)
}
case <-l.quit:
// TODO: do some clean up
break out
}
}
l.wg.Done()
}
// RequestFundingReservation...
func (l *LightningWallet) InitChannelReservation(a btcutil.Amount, t FundingType) (*ChannelReservation, error) {
errChan := make(chan error, 1)
respChan := make(chan *ChannelReservation, 1)
l.msgChan <- &initFundingReserveMsg{
fundingAmount: a,
fundingType: t,
err: errChan,
resp: respChan,
}
return <-respChan, <-errChan
}
// handleFundingReserveRequest...
func (l *LightningWallet) handleFundingReserveRequest(req *initFundingReserveMsg) {
// Create a limbo and record entry for this newly pending funding request.
l.limboMtx.Lock()
id := l.nextFundingID
partialState := newChannelReservation(req.fundingType, req.fundingAmount, req.minFeeRate, l, id)
l.nextFundingID++
l.fundingLimbo[id] = partialState
l.limboMtx.Unlock()
// Grab the mutex on the ChannelReservation to ensure thead-safety
partialState.Lock()
defer partialState.Unlock()
// Find all unlocked unspent outputs with greater than 6 confirmations.
maxConfs := int32(math.MaxInt32)
unspentOutputs, err := l.wallet.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..
// TODO(roasbeef): possibly integrate the fee prediction project? if
// results hold up...
// NOTE: this current selection assumes "priority" is still a thing.
selector := &coinset.MaxValueAgeCoinSelector{
MaxInputs: 10,
MinChangeAmount: 10000,
}
selectedCoins, err := selector.CoinSelect(req.fundingAmount, coins)
if err != nil {
req.err <- err
req.resp <- nil
return
}
// 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.
partialState.ourInputs = make([]*wire.TxIn, len(selectedCoins.Coins()))
for i, coin := range selectedCoins.Coins() {
txout := wire.NewOutPoint(coin.Hash(), coin.Index())
l.wallet.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())
partialState.ourInputs[i] = wire.NewTxIn(outPoint, nil)
}
// Create some possibly neccessary change outputs.
selectedTotalValue := coinset.NewCoinSet(selectedCoins.Coins()).TotalValue()
partialState.ourChange = make([]*wire.TxOut, 0, len(selectedCoins.Coins()))
if selectedTotalValue > req.fundingAmount {
// Change is necessary. Query for an available change address to
// send the remainder to.
changeAmount := selectedTotalValue - req.fundingAmount
addrs, err := l.wallet.Manager.NextInternalAddresses(waddrmgr.DefaultAccountNum, 1)
if err != nil {
req.err <- err
req.resp <- nil
return
}
changeAddrScript, err := txscript.PayToAddrScript(addrs[0].Address())
if err != nil {
req.err <- err
req.resp <- nil
return
}
// TODO(roasbeef): re-enable after tests are connected to real node.
// * or the change to btcwallet is made to reverse the dependancy
// between chain-client and wallet.
//changeAddr, err := l.wallet.NewChangeAddress(waddrmgr.DefaultAccountNum)
partialState.ourChange = append(partialState.ourChange,
wire.NewTxOut(int64(changeAmount), changeAddrScript))
}
// TODO(roasbeef): re-calculate fees here to minFeePerKB, may need more inputs
multiSigKey, err := l.getNextMultiSigKey()
if err != nil {
req.err <- err
req.resp <- nil
return
}
partialState.ourKey = multiSigKey
// Funding reservation request succesfully handled. The funding inputs
// will be marked as unavailable until the reservation is either
// completed, or cancecled.
req.resp <- partialState
req.err <- nil
}
// handleFundingReserveCancel...
func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMsg) {
// TODO(roasbeef): holding lock too long
// RLOCK?
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.ourInputs {
l.wallet.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...
func (l *LightningWallet) handleFundingCounterPartyFunds(req *addCounterPartyFundsMsg) {
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()
// First, add all multi-party inputs to the transaction
// TODO(roasbeef); handle case that tx doesn't exist, fake input
// TODO(roasbeef): validate SPV proof from other side if in SPV mode.
// * actually, pure SPV would need fraud proofs right? must prove input
// is unspent
// * or, something like getutxo?
for _, ourInput := range pendingReservation.ourInputs {
pendingReservation.fundingTx.AddTxIn(ourInput)
}
pendingReservation.theirInputs = req.theirInputs
for _, theirInput := range pendingReservation.theirInputs {
pendingReservation.fundingTx.AddTxIn(theirInput)
}
// Next, add all multi-party outputs to the transaction. This includes
// change outputs for both side.
for _, ourChangeOutput := range pendingReservation.ourChange {
pendingReservation.fundingTx.AddTxOut(ourChangeOutput)
}
pendingReservation.theirChange = req.theirChangeOutputs
for _, theirChangeOutput := range pendingReservation.theirChange {
pendingReservation.fundingTx.AddTxOut(theirChangeOutput)
}
// Finally, add the 2-of-2 multi-sig output which will set up the lightning
// channel.
// TODO(roasbeef): Cannonical sorting of keys here?
// * also, also this is currently bare-multi sig, keep this for network
// transparency or switch to P2SH?
keys := make([]*btcutil.AddressPubKey, 2)
ourKey := pendingReservation.ourKey.PubKey().SerializeCompressed()
keys[0], _ = btcutil.NewAddressPubKey(ourKey, ActiveNetParams)
pendingReservation.theirKey = req.theirKey
keys[1], _ = btcutil.NewAddressPubKey(pendingReservation.theirKey.SerializeCompressed(), ActiveNetParams)
multiSigScript, err := txscript.MultiSigScript(keys, 2)
if err != nil {
req.err <- err
return
}
multiSigOut := wire.NewTxOut(int64(pendingReservation.FundingAmount),
multiSigScript)
pendingReservation.fundingTx.AddTxOut(multiSigOut)
// 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.
txsort.InPlaceSort(pendingReservation.fundingTx)
// Now that the transaction has been cannonically sorted, compute the
// normalized transation ID before we attach our signatures.
// TODO(roasbeef): this isn't the normalized txid, this isn't recursive...
pendingReservation.normalizedTxID = pendingReservation.fundingTx.TxSha()
// Now, sign all inputs that are ours, collecting the signatures in
// order of the inputs.
pendingReservation.ourSigs = make([][]byte, 0, len(pendingReservation.ourInputs))
for i, txIn := range pendingReservation.fundingTx.TxIn {
// Does the wallet know about the txin?
txDetail, _ := l.wallet.TxStore.TxDetails(&txIn.PreviousOutPoint.Hash)
if txDetail == nil {
continue
}
// Is this our txin? TODO(roasbeef): assumes all inputs are P2PKH...
prevIndex := txIn.PreviousOutPoint.Index
prevOut := txDetail.TxRecord.MsgTx.TxOut[prevIndex]
_, addrs, _, _ := txscript.ExtractPkScriptAddrs(prevOut.PkScript, ActiveNetParams)
apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash)
if !ok {
req.err <- btcwallet.ErrUnsupportedTransactionType
return
}
ai, err := l.wallet.Manager.Address(apkh)
if err != nil {
req.err <- fmt.Errorf("cannot get address info: %v", err)
return
}
pka := ai.(waddrmgr.ManagedPubKeyAddress)
privkey, err := pka.PrivKey()
if err != nil {
req.err <- fmt.Errorf("cannot get private key: %v", err)
return
}
sigscript, err := txscript.SignatureScript(pendingReservation.fundingTx, i,
prevOut.PkScript, txscript.SigHashAll, privkey,
ai.Compressed())
if err != nil {
req.err <- fmt.Errorf("cannot create sigscript: %s", err)
return
}
pendingReservation.fundingTx.TxIn[i].SignatureScript = sigscript
pendingReservation.ourSigs = append(pendingReservation.ourSigs, sigscript)
}
req.err <- nil
}
// handleFundingCounterPartySigs...
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.theirSigs = msg.theirSigs
fundingTx := pendingReservation.fundingTx
for i, txin := range fundingTx.TxIn {
if txin.SignatureScript == nil {
txin.SignatureScript = pendingReservation.theirSigs[i]
/*// Fetch the alleged previous output along with the
// pkscript referenced by this input.
prevOut := txin.PreviousOutPoint
output, err := l.rpc.GetTxOut(&prevOut.Hash, prevOut.Index, false)
if err != nil {
// TODO(roasbeef): do this at the start to avoid wasting out time?
// 8 or a set of nodes "we" run with exposed unauthenticated RPC?
msg.err <- err
return
}
pkscript, err := hex.DecodeString(output.ScriptPubKey.Hex)
if err != nil {
msg.err <- err
return
}
// Ensure that the signature is valid.
vm, err := txscript.NewEngine(pkscript,
fundingTx, i, txscript.StandardVerifyFlags, nil)
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
}*/
}
}
// Now that all signatures are in place, and valid record the regular txid.
pendingReservation.completedFundingTx = btcutil.NewTx(pendingReservation.fundingTx)
txID := pendingReservation.fundingTx.TxSha()
l.limboMtx.Lock()
delete(l.fundingLimbo, pendingReservation.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.
writeErr := l.lnNamespace.Update(func(tx walletdb.Tx) error {
// Get the bucket dedicated to storing the meta-data for open
// channels.
// TODO(roasbeef): CHECKSUMS, REDUNDANCY, etc etc.
rootBucket := tx.RootBucket()
openChanBucket, err := rootBucket.CreateBucketIfNotExists(openChannelBucket)
if err != nil {
return err
}
// Create a new sub-bucket within the open channel bucket
// specifically for this channel.
// TODO(roasbeef): should def be indexed by LNID, cuz mal etc.
chanBucket, err := openChanBucket.CreateBucketIfNotExists(txID.Bytes())
if err != nil {
return err
}
// TODO(roasbeef): sync.Pool of buffers in the future.
var buf bytes.Buffer
fundingTx.Serialize(&buf)
return chanBucket.Put(fundingTxKey, buf.Bytes())
})
// TODO(roasbeef): broadcast now?
// * create goroutine, listens on blockconnected+blockdisconnected channels
// * after six blocks, then will create an LightningChannel struct and
// send over reservation.
// * will need a multi-plexer to fan out, to listen on ListenConnectedBlocks
// * should prob be a separate struct/modele
// * use NotifySpent in order to catch non-cooperative spends of revoked
// commitment txns. Hmm using p2sh or bare multi-sig?
msg.err <- writeErr
}
// nextMultiSigKey...
// TODO(roasbeef): on shutdown, write state of pending keys, then read back?
func (l *LightningWallet) getNextMultiSigKey() (*btcec.PrivateKey, error) {
l.lmtx.Lock()
defer l.lmtx.Unlock()
nextAddr, err := l.wallet.Manager.NextExternalAddresses(waddrmgr.DefaultAccountNum, 1)
if err != nil {
return nil, err
}
pkAddr := nextAddr[0].(waddrmgr.ManagedPubKeyAddress)
return pkAddr.PrivKey()
}
+573
View File
@@ -0,0 +1,573 @@
package wallet
import (
"bytes"
"crypto/sha256"
"io/ioutil"
"os"
"testing"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/coinset"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr"
)
var (
privPass = []byte("private-test")
// For simplicity a single priv key controls all of our test outputs.
testWalletPrivKey = []byte{
0x2b, 0xd8, 0x06, 0xc9, 0x7f, 0x0e, 0x00, 0xaf,
0x1a, 0x1f, 0xc3, 0x32, 0x8f, 0xa7, 0x63, 0xa9,
0x26, 0x97, 0x23, 0xc8, 0xdb, 0x8f, 0xac, 0x4f,
0x93, 0xaf, 0x71, 0xdb, 0x18, 0x6d, 0x6e, 0x90,
}
// We're alice :)
bobsPrivKey = []byte{
0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda,
0x63, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17,
0xd, 0xe7, 0x95, 0xe4, 0xb7, 0x25, 0xb8, 0x4d,
0x1e, 0xb, 0x4c, 0xfd, 0x9e, 0xc5, 0x8c, 0xe9,
}
// Use a hard-coded HD seed in order to avoid derivation.
testHdSeed = []byte{
0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab,
0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4,
0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9,
0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53,
}
)
func assertProperBalance(t *testing.T, lw *LightningWallet, numConfirms, amount int32) {
balance, err := lw.wallet.TxStore.Balance(0, 20)
if err != nil {
t.Fatalf("unable to query for balance: %v", err)
}
if balance != btcutil.Amount(20*1e8) {
t.Fatalf("wallet credits not properly loaded, should have 20BTC, "+
"instead have %v", balance)
}
}
// bobNode represents the other party involved as a node within LN. Bob is our
// only "default-route", we have a direct connection with him.
type bobNode struct {
privKey *btcec.PrivateKey
multiSigKey *btcec.PublicKey
availableOutputs []*wire.TxIn
changeOutputs []*wire.TxOut
}
// signFundingTx generates signatures for all the inputs in the funding tx
// belonging to Bob.
func (b *bobNode) signFundingTx(fundingTx *wire.MsgTx) ([][]byte, error) {
bobSigs := make([][]byte, 0, len(b.availableOutputs))
bobPkScript := b.changeOutputs[0].PkScript
for i, _ := range fundingTx.TxIn {
// Alice has already signed this input
if fundingTx.TxIn[i].SignatureScript != nil {
continue
}
sigScript, err := txscript.SignatureScript(fundingTx, i,
bobPkScript, txscript.SigHashAll, b.privKey,
true)
if err != nil {
return nil, err
}
bobSigs = append(bobSigs, sigScript)
}
return bobSigs, nil
}
// newBobNode generates a test "ln node" to interact with Alice (us). For the
// funding transaction, bob has a single output totaling 7BTC. For our basic
// test, he'll fund the channel with 5BTC, leaving 2BTC to the change output.
// TODO(roasbeef): proper handling of change etc.
func newBobNode() (*bobNode, error) {
// First, parse Bob's priv key in order to obtain a key he'll use for the
// multi-sig funding transaction.
privKey, pubKey := btcec.PrivKeyFromBytes(btcec.S256(), bobsPrivKey)
// Next, generate an output redeemable by bob.
bobAddr, err := btcutil.NewAddressPubKey(privKey.PubKey().SerializeCompressed(),
ActiveNetParams)
if err != nil {
return nil, err
}
bobAddrScript, err := txscript.PayToAddrScript(bobAddr.AddressPubKeyHash())
if err != nil {
return nil, err
}
prevOut := wire.NewOutPoint(&wire.ShaHash{}, ^uint32(0))
// TODO(roasbeef): When the chain rpc is hooked in, assert bob's output
// actually exists and it unspent in the chain.
bobTxIn := wire.NewTxIn(prevOut, nil)
// Using bobs priv key above, create a change address he can spend.
bobChangeOutput := wire.NewTxOut(2*1e8, bobAddrScript)
return &bobNode{
privKey: privKey,
multiSigKey: pubKey,
availableOutputs: []*wire.TxIn{bobTxIn},
changeOutputs: []*wire.TxOut{bobChangeOutput},
}, nil
}
// addTestTx adds a output spendable by our test wallet, marked as included in
// 'block'.
func addTestTx(w *LightningWallet, rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) error {
err := w.wallet.TxStore.InsertTx(rec, block)
if err != nil {
return err
}
// Check every output to determine whether it is controlled by a wallet
// key. If so, mark the output as a credit.
for i, output := range rec.MsgTx.TxOut {
_, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript,
ActiveNetParams)
if err != nil {
// Non-standard outputs are skipped.
continue
}
for _, addr := range addrs {
ma, err := w.wallet.Manager.Address(addr)
if err == nil {
err = w.wallet.TxStore.AddCredit(rec, block, uint32(i),
ma.Internal())
if err != nil {
return err
}
err = w.wallet.Manager.MarkUsed(addr)
if err != nil {
return err
}
continue
}
// Missing addresses are skipped. Other errors should
// be propagated.
if !waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) {
return err
}
}
}
return nil
}
func genBlockHash(n int) *wire.ShaHash {
sha := sha256.Sum256([]byte{byte(n)})
hash, _ := wire.NewShaHash(sha[:])
return hash
}
func loadTestCredits(w *LightningWallet, numOutputs, btcPerOutput int) error {
// Import the priv key (converting to WIF) above that controls all our
// available outputs.
privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), testWalletPrivKey)
if err := w.wallet.Unlock(privPass, time.Duration(0)); err != nil {
return err
}
bs := &waddrmgr.BlockStamp{Hash: *genBlockHash(1), Height: 1}
wif, err := btcutil.NewWIF(privKey, ActiveNetParams, true)
if err != nil {
return err
}
if _, err := w.wallet.ImportPrivateKey(wif, bs, false); err != nil {
return nil
}
if err := w.wallet.Manager.SetSyncedTo(&waddrmgr.BlockStamp{int32(1), *genBlockHash(1)}); err != nil {
return err
}
blk := wtxmgr.BlockMeta{wtxmgr.Block{Hash: *genBlockHash(2), Height: 2}, time.Now()}
// Create a simple P2PKH pubkey script spendable by Alice. For simplicity
// all of Alice's spendable funds will reside in this output.
satosihPerOutput := int64(btcPerOutput * 1e8)
walletAddr, err := btcutil.NewAddressPubKey(privKey.PubKey().SerializeCompressed(),
ActiveNetParams)
if err != nil {
return err
}
walletScriptCredit, err := txscript.PayToAddrScript(walletAddr.AddressPubKeyHash())
if err != nil {
return err
}
// Create numOutputs outputs spendable by our wallet each holding btcPerOutput
// in satoshis.
tx := wire.NewMsgTx()
prevOut := wire.NewOutPoint(genBlockHash(999), 1)
txIn := wire.NewTxIn(prevOut, []byte{txscript.OP_0, txscript.OP_0})
tx.AddTxIn(txIn)
for i := 0; i < numOutputs; i++ {
tx.AddTxOut(wire.NewTxOut(satosihPerOutput, walletScriptCredit))
}
txCredit, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now())
if err != nil {
return err
}
if err := addTestTx(w, txCredit, &blk); err != nil {
return err
}
if err := w.wallet.Manager.SetSyncedTo(&waddrmgr.BlockStamp{int32(2), *genBlockHash(2)}); err != nil {
return err
}
// Make the wallet think it's been synced to block 10. This way the
// outputs we added above will have sufficient confirmations
// (hard coded to 6 atm).
for i := 3; i < 10; i++ {
sha := *genBlockHash(i)
if err := w.wallet.Manager.SetSyncedTo(&waddrmgr.BlockStamp{int32(i), sha}); err != nil {
return err
}
}
return nil
}
// createTestWallet creates a test LightningWallet will a total of 20BTC
// available for funding channels.
func createTestWallet() (string, *LightningWallet, error) {
privPass := []byte("private-test")
tempTestDir, err := ioutil.TempDir("", "lnwallet")
if err != nil {
return "", nil, nil
}
wallet, err := NewLightningWallet(privPass, nil, testHdSeed, tempTestDir)
if err != nil {
return "", nil, err
}
wallet.Start()
// Load our test wallet with 5 outputs each holding 4BTC.
if err := loadTestCredits(wallet, 5, 4); err != nil {
return "", nil, err
}
return tempTestDir, wallet, nil
}
func TestBasicWalletReservationWorkFlow(t *testing.T) {
// Create our test wallet, will have a total of 20 BTC available for
// funding via 5 outputs with 4BTC each.
testDir, lnwallet, err := createTestWallet()
if err != nil {
t.Fatalf("unable to create test ln wallet: %v", err)
}
defer os.RemoveAll(testDir)
defer lnwallet.Stop()
// The wallet should now have 20BTC available for spending.
assertProperBalance(t, lnwallet, 1, 20)
bobNode, err := newBobNode()
if err != nil {
t.Fatalf("unable to create bob node: %v", err)
}
// Bob initiates a channel funded with 5 BTC for each side, so 10
// BTC total. He also generates 2 BTC in change.
fundingAmount := btcutil.Amount(5 * 1e8)
chanReservation, err := lnwallet.InitChannelReservation(fundingAmount, SIGHASH)
if err != nil {
t.Fatalf("unable to initialize funding reservation: %v", err)
}
// The channel reservation should now be populated with a multi-sig key
// from our HD chain, a change output with 3 BTC, and 2 outputs selected
// of 4 BTC each.
ourInputs, ourChange, ourMultsigKey := chanReservation.OurFunds()
if len(ourInputs) != 2 {
t.Fatalf("outputs for funding tx not properly selected, have %v "+
"outputs should have 2", len(ourInputs))
}
if ourMultsigKey == nil {
t.Fatalf("alice's key for multi-sig not found")
}
if ourChange[0].Value != 3e8 {
t.Fatalf("coin selection failed, change output should be 3e8 "+
"satoshis, is instead %v", ourChange[0].Value)
}
// Bob sends over his output, change addr and multi-sig key.
if err := chanReservation.AddFunds(bobNode.availableOutputs,
bobNode.changeOutputs, bobNode.multiSigKey); err != nil {
t.Fatalf("unable to add bob's funds to the funding tx: %v", err)
}
// At this point, the reservation should have our signatures, and a
// partial funding transaction (missing bob's sigs).
if len(chanReservation.OurSigs()) != 2 {
t.Fatalf("only %v of our sigs present, should have 2",
len(chanReservation.OurSigs()))
}
// Additionally, the funding tx should have been populated.
if chanReservation.fundingTx == nil {
t.Fatalf("funding transaction never created!")
}
// Their funds should also be filled in.
theirInputs, theirChange, theirMultsigKey := chanReservation.TheirFunds()
if len(theirInputs) != 1 {
t.Fatalf("bob's outputs for funding tx not properly selected, have %v "+
"outputs should have 2", len(theirInputs))
}
if theirMultsigKey == nil {
t.Fatalf("alice's key for multi-sig not found")
}
if theirChange[0].Value != 2e8 {
t.Fatalf("bob should have one change output with value 2e8"+
"satoshis, is instead %v", theirChange[0].Value)
}
// Alice responds with her output, change addr, multi-sig key and signatures.
// Bob then responds with his signatures.
bobsSigs, err := bobNode.signFundingTx(chanReservation.fundingTx)
if err != nil {
t.Fatalf("unable to sign inputs for bob: %v", err)
}
if err := chanReservation.CompleteReservation(bobsSigs); err != nil {
t.Fatalf("unable to complete funding tx: %v", err)
}
// At this point, the channel can be considered "open" when the funding
// txn hits a "comfortable" depth.
// A bucket should have been created for this pending channel, and the
// state commited.
err = lnwallet.lnNamespace.View(func(tx walletdb.Tx) error {
// The open channel bucket should have been created.
rootBucket := tx.RootBucket()
openChanBucket := rootBucket.Bucket(openChannelBucket)
if openChanBucket == nil {
t.Fatalf("openChanBucket should be created, but isn't")
}
// The sub-bucket for this channel, keyed by the txid of the
// funding transaction
txid := chanReservation.fundingTx.TxSha()
bobChanBucket := openChanBucket.Bucket(txid[:])
if bobChanBucket == nil {
t.Fatalf("bucket for the alice-bob channe should exist, " +
"but doesn't")
}
var storedTx wire.MsgTx
b := bytes.NewReader(bobChanBucket.Get(fundingTxKey))
storedTx.Deserialize(b)
// The hash of the stored tx should be indentical to our funding tx.
storedTxId := storedTx.TxSha()
actualTxId := chanReservation.fundingTx.TxSha()
if !bytes.Equal(storedTxId.Bytes(), actualTxId.Bytes()) {
t.Fatalf("stored funding tx doesn't match actual")
}
return nil
})
// The funding tx should now be valid and complete.
// Check each input and ensure all scripts are fully valid.
var zeroHash wire.ShaHash
fundingTx := chanReservation.fundingTx
for i, input := range chanReservation.fundingTx.TxIn {
var pkscript []byte
// Bob's txin
if bytes.Equal(input.PreviousOutPoint.Hash.Bytes(),
zeroHash.Bytes()) {
pkscript = bobNode.changeOutputs[0].PkScript
} else {
// Does the wallet know about the txin?
txDetail, err := lnwallet.wallet.TxStore.TxDetails(&input.PreviousOutPoint.Hash)
if txDetail == nil || err != nil {
t.Fatalf("txstore can't find tx detail, err: %v", err)
}
prevIndex := input.PreviousOutPoint.Index
pkscript = txDetail.TxRecord.MsgTx.TxOut[prevIndex].PkScript
}
vm, err := txscript.NewEngine(pkscript,
fundingTx, i, txscript.StandardVerifyFlags, nil)
if err != nil {
// TODO(roasbeef): cancel at this stage if invalid sigs?
t.Fatalf("cannot create script engine: %s", err)
}
if err = vm.Execute(); err != nil {
t.Fatalf("cannot validate transaction: %s", err)
}
}
}
// TODO(roasbeef): why is wallet so slow to create+open?
// * investigate
// * re-use same lnwallet instance accross tests resetting each time?
func TestFundingTransactionLockedOutputs(t *testing.T) {
// Create our test wallet, will have a total of 20 BTC available for
// funding via 5 outputs with 4BTC each.
testDir, lnwallet, err := createTestWallet()
if err != nil {
t.Fatalf("unable to create test ln wallet: %v", err)
}
defer os.RemoveAll(testDir)
defer lnwallet.Stop()
// The wallet should now have 20BTC available for spending.
assertProperBalance(t, lnwallet, 1, 20)
// Create two channels, both asking for 8 BTC each, totalling 16
// BTC.
// TODO(roasbeef): tests for concurrent funding.
// * also func for below
fundingAmount := btcutil.Amount(8 * 1e8)
chanReservation1, err := lnwallet.InitChannelReservation(fundingAmount, SIGHASH)
if err != nil {
t.Fatalf("unable to initialize funding reservation 1: %v", err)
}
chanReservation2, err := lnwallet.InitChannelReservation(fundingAmount, SIGHASH)
if err != nil {
t.Fatalf("unable to initialize funding reservation 2: %v", err)
}
// Neither should have any change, as all our output sizes are
// identical (4BTC).
ourInputs1, ourChange1, _ := chanReservation1.OurFunds()
if len(ourInputs1) != 2 {
t.Fatalf("outputs for funding tx not properly selected, has %v "+
"outputs should have 2", len(ourInputs1))
}
if len(ourChange1) != 0 {
t.Fatalf("funding transaction should have no change, instead has %v",
len(ourChange1))
}
ourInputs2, ourChange2, _ := chanReservation2.OurFunds()
if len(ourInputs2) != 2 {
t.Fatalf("outputs for funding tx not properly selected, have %v "+
"outputs should have 2", len(ourInputs2))
}
if len(ourChange2) != 0 {
t.Fatalf("funding transaction should have no change, instead has %v",
len(ourChange2))
}
// Now attempt to reserve funds for another channel, this time requesting
// 5 BTC. We only have 4BTC worth of outpoints that aren't locked, so
// this should fail.
amt := btcutil.Amount(8 * 1e8)
failedReservation, err := lnwallet.InitChannelReservation(amt, SIGHASH)
if err == nil {
t.Fatalf("not error returned, should fail on coin selection")
}
if err != coinset.ErrCoinsNoSelectionAvailable {
t.Fatalf("error not coinselect error: %v", err)
}
if failedReservation != nil {
t.Fatalf("reservation should be nil")
}
}
func TestFundingCancellationNotEnoughFunds(t *testing.T) {
// TODO(roasbeef): factor out, such copy pasta
// Create our test wallet, will have a total of 20 BTC available for
// funding via 5 outputs with 4BTC each.
testDir, lnwallet, err := createTestWallet()
if err != nil {
t.Fatalf("unable to create test ln wallet: %v", err)
}
defer os.RemoveAll(testDir)
defer lnwallet.Stop()
// The wallet should now have 20BTC available for spending.
assertProperBalance(t, lnwallet, 1, 20)
// Create a reservation for 12 BTC.
fundingAmount := btcutil.Amount(12 * 1e8)
chanReservation, err := lnwallet.InitChannelReservation(fundingAmount, SIGHASH)
if err != nil {
t.Fatalf("unable to initialize funding reservation: %v", err)
}
// There should be three locked outpoints.
lockedOutPoints := lnwallet.wallet.LockedOutpoints()
if len(lockedOutPoints) != 3 {
t.Fatalf("two outpoints should now be locked, instead %v are",
lockedOutPoints)
}
// Attempt to create another channel with 12 BTC, this should fail.
failedReservation, err := lnwallet.InitChannelReservation(fundingAmount, SIGHASH)
if err != coinset.ErrCoinsNoSelectionAvailable {
t.Fatalf("coin selection succeded should have insufficient funds: %+v",
failedReservation)
}
// Now cancel that old reservation.
if err := chanReservation.Cancel(); err != nil {
t.Fatalf("unable to cancel reservation: %v", err)
}
// Those outpoints should no longer be locked.
lockedOutPoints = lnwallet.wallet.LockedOutpoints()
if len(lockedOutPoints) != 0 {
t.Fatalf("outpoints still locked")
}
// Reservation ID should now longer be tracked.
_, ok := lnwallet.fundingLimbo[chanReservation.reservationID]
if ok {
t.Fatalf("funding reservation still in map")
}
// TODO(roasbeef): create method like Balance that ignores locked
// outpoints, will let us fail early/fast instead of querying and
// attempting coin selection.
// Request to fund a new channel should now succeeed.
_, err = lnwallet.InitChannelReservation(fundingAmount, SIGHASH)
if err != nil {
t.Fatalf("unable to initialize funding reservation: %v", err)
}
}
func TestCancelNonExistantReservation(t *testing.T) {
// Create our test wallet, will have a total of 20 BTC available for
// funding via 5 outputs with 4BTC each.
testDir, lnwallet, err := createTestWallet()
if err != nil {
t.Fatalf("unable to create test ln wallet: %v", err)
}
defer os.RemoveAll(testDir)
defer lnwallet.Stop()
// Create our own reservation, give it some ID.
res := newChannelReservation(SIGHASH, 1000, 5000, lnwallet, 22)
// Attempt to cancel this reservation. This should fail, we know
// nothing of it.
if err = res.Cancel(); err == nil {
t.Fatalf("cancelled non-existant reservation")
}
}
func TestFundingReservationInvalidCounterpartySigs(t *testing.T) {
}
func TestFundingTransactionTxFees(t *testing.T) {
}