rename wallet to lnwallet
This commit is contained in:
1 parent
9de294c31f
commit
147748d178
7 files changed
+7
-1
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
Reference in new issue
Block a user