From dc00514c42aa67828ac919ed6ca97c3623dc2e52 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 14:57:27 -0700 Subject: [PATCH 01/15] channeldb: remove EncryptorDecryptor interface This commit removes the EncryptorDecryptor interface, and all related usage within channeldb. This interface is no longer needed as wallet specific secrets such as private keys are no longer stored within the database. --- channeldb/channel.go | 28 +++++++++++----------------- channeldb/channel_test.go | 18 ------------------ channeldb/db.go | 18 +----------------- 3 files changed, 12 insertions(+), 52 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 945f1f62..6ec88ae0 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -200,7 +200,7 @@ func (c *OpenChannel) FullSync() error { chanIDBucket.Put(b.Bytes(), nil) } - return putOpenChannel(chanBucket, nodeChanBucket, c, c.Db.cryptoSystem) + return putOpenChannel(chanBucket, nodeChanBucket, c) }) } @@ -362,7 +362,7 @@ func putClosedChannelSummary(tx *bolt.Tx, chanID []byte) error { // putChannel serializes, and stores the current state of the channel in its // entirety. func putOpenChannel(openChanBucket *bolt.Bucket, nodeChanBucket *bolt.Bucket, - channel *OpenChannel, encryptor EncryptorDecryptor) error { + channel *OpenChannel) error { // First write out all the "common" fields using the field's prefix // appened with the channel's ID. These fields go into a top-level bucket @@ -387,13 +387,13 @@ func putOpenChannel(openChanBucket *bolt.Bucket, nodeChanBucket *bolt.Bucket, if err := putChannelIDs(nodeChanBucket, channel); err != nil { return err } - if err := putChanCommitKeys(nodeChanBucket, channel, encryptor); err != nil { + if err := putChanCommitKeys(nodeChanBucket, channel); err != nil { return err } if err := putChanCommitTxns(nodeChanBucket, channel); err != nil { return err } - if err := putChanFundingInfo(nodeChanBucket, channel, encryptor); err != nil { + if err := putChanFundingInfo(nodeChanBucket, channel); err != nil { return err } if err := putChanEklremState(nodeChanBucket, channel); err != nil { @@ -411,7 +411,7 @@ func putOpenChannel(openChanBucket *bolt.Bucket, nodeChanBucket *bolt.Bucket, // An EncryptorDecryptor is required to decrypt sensitive information stored // within the database. func fetchOpenChannel(openChanBucket *bolt.Bucket, nodeChanBucket *bolt.Bucket, - chanID *wire.OutPoint, decryptor EncryptorDecryptor) (*OpenChannel, error) { + chanID *wire.OutPoint) (*OpenChannel, error) { channel := &OpenChannel{ ChanID: chanID, @@ -421,13 +421,13 @@ func fetchOpenChannel(openChanBucket *bolt.Bucket, nodeChanBucket *bolt.Bucket, if err := fetchChannelIDs(nodeChanBucket, channel); err != nil { return nil, err } - if err := fetchChanCommitKeys(nodeChanBucket, channel, decryptor); err != nil { + if err := fetchChanCommitKeys(nodeChanBucket, channel); err != nil { return nil, err } if err := fetchChanCommitTxns(nodeChanBucket, channel); err != nil { return nil, err } - if err := fetchChanFundingInfo(nodeChanBucket, channel, decryptor); err != nil { + if err := fetchChanFundingInfo(nodeChanBucket, channel); err != nil { return nil, err } if err := fetchChanEklremState(nodeChanBucket, channel); err != nil { @@ -791,8 +791,7 @@ func fetchChannelIDs(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error { return nil } -func putChanCommitKeys(nodeChanBucket *bolt.Bucket, channel *OpenChannel, - ed EncryptorDecryptor) error { +func putChanCommitKeys(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error { // Construct the key which stores the commitment keys: ckk || channelID. // TODO(roasbeef): factor into func @@ -829,8 +828,7 @@ func deleteChanCommitKeys(nodeChanBucket *bolt.Bucket, chanID []byte) error { return nodeChanBucket.Delete(commitKey) } -func fetchChanCommitKeys(nodeChanBucket *bolt.Bucket, channel *OpenChannel, - ed EncryptorDecryptor) error { +func fetchChanCommitKeys(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error { // Construct the key which stores the commitment keys: ckk || channelID. // TODO(roasbeef): factor into func @@ -939,9 +937,7 @@ func fetchChanCommitTxns(nodeChanBucket *bolt.Bucket, channel *OpenChannel) erro return nil } -func putChanFundingInfo(nodeChanBucket *bolt.Bucket, channel *OpenChannel, - ed EncryptorDecryptor) error { - +func putChanFundingInfo(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error { var bc bytes.Buffer if err := writeOutpoint(&bc, channel.ChanID); err != nil { return err @@ -989,9 +985,7 @@ func deleteChanFundingInfo(nodeChanBucket *bolt.Bucket, chanID []byte) error { return nodeChanBucket.Delete(fundTxnKey) } -func fetchChanFundingInfo(nodeChanBucket *bolt.Bucket, channel *OpenChannel, - ed EncryptorDecryptor) error { - +func fetchChanFundingInfo(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error { var b bytes.Buffer if err := writeOutpoint(&b, channel.ChanID); err != nil { return err diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 3d2c62e5..c46efdef 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -78,23 +78,6 @@ var ( } ) -type MockEncryptorDecryptor struct { -} - -func (m *MockEncryptorDecryptor) Encrypt(n []byte) ([]byte, error) { - return n, nil -} - -func (m *MockEncryptorDecryptor) Decrypt(n []byte) ([]byte, error) { - return n, nil -} - -func (m *MockEncryptorDecryptor) OverheadSize() uint32 { - return 0 -} - -var _ EncryptorDecryptor = (*MockEncryptorDecryptor)(nil) - func TestOpenChannelPutGetDelete(t *testing.T) { // First, create a temporary directory to be used for the duration of // this test. @@ -111,7 +94,6 @@ func TestOpenChannelPutGetDelete(t *testing.T) { if err != nil { t.Fatalf("unable to create channeldb: %v", err) } - cdb.RegisterCryptoSystem(&MockEncryptorDecryptor{}) defer cdb.Close() privKey, pubKey := btcec.PrivKeyFromBytes(btcec.S256(), key[:]) diff --git a/channeldb/db.go b/channeldb/db.go index 2d24bc30..d1f6f73b 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -27,14 +27,6 @@ var bufPool = &sync.Pool{ New: func() interface{} { return new(bytes.Buffer) }, } -// EncryptorDecryptor... -// TODO(roasbeef): ability to rotate EncryptorDecryptor's across DB -type EncryptorDecryptor interface { - Encrypt(in []byte) ([]byte, error) - Decrypt(in []byte) ([]byte, error) - OverheadSize() uint32 -} - // DB is the primary datastore for the LND daemon. The database stores // information related to nodes, routing data, open/closed channels, fee // schedules, and reputation data. @@ -42,8 +34,6 @@ type DB struct { store *bolt.DB netParams *chaincfg.Params - - cryptoSystem EncryptorDecryptor } // Open opens an existing channeldb created under the passed namespace with @@ -66,12 +56,6 @@ func Open(dbPath string, netParams *chaincfg.Params) (*DB, error) { return &DB{store: bdb, netParams: netParams}, nil } -// RegisterCryptoSystem registers an implementation of the EncryptorDecryptor -// interface for use within the database to encrypt/decrypt sensitive data. -func (d *DB) RegisterCryptoSystem(ed EncryptorDecryptor) { - d.cryptoSystem = ed -} - // Wipe completely deletes all saved state within all used buckets within the // database. The deletion is done in a single transaction, therefore this // operation is fully atomic. @@ -179,7 +163,7 @@ func (d *DB) FetchOpenChannels(nodeID *wire.ShaHash) ([]*OpenChannel, error) { } oChannel, err := fetchOpenChannel(openChanBucket, - nodeChanBucket, chanID, d.cryptoSystem) + nodeChanBucket, chanID) if err != nil { return err } From bdb6566076a614c45a207c21ef3ac0e27cf51798 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:03:18 -0700 Subject: [PATCH 02/15] channeldb: only store public keys for multi-sig + commitment keys This commit removes the storage+encryption of private keys within channeldb. We no longer need to encrypt these secrets before storing as the base wallet is now expected to retain full control of these secrets rather than the database. As a result, we now only store public keys within the database. --- channeldb/channel.go | 32 +++++++++----------------------- channeldb/channel_test.go | 12 ++++++------ 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 6ec88ae0..742ce2ef 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -106,7 +106,7 @@ type OpenChannel struct { //ReserveAmount btcutil.Amount // Keys for both sides to be used for the commitment transactions. - OurCommitKey *btcec.PrivateKey + OurCommitKey *btcec.PublicKey TheirCommitKey *btcec.PublicKey // Tracking total channel capacity, and the amount of funds allocated @@ -123,7 +123,7 @@ type OpenChannel struct { // The outpoint of the final funding transaction. FundingOutpoint *wire.OutPoint - OurMultiSigKey *btcec.PrivateKey + OurMultiSigKey *btcec.PublicKey TheirMultiSigKey *btcec.PublicKey FundingRedeemScript []byte @@ -809,12 +809,7 @@ func putChanCommitKeys(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error return err } - encryptedPriv, err := ed.Encrypt(channel.OurCommitKey.Serialize()) - if err != nil { - return err - } - - if _, err := b.Write(encryptedPriv); err != nil { + if _, err := b.Write(channel.OurCommitKey.SerializeCompressed()); err != nil { return err } @@ -848,12 +843,7 @@ func fetchChanCommitKeys(nodeChanBucket *bolt.Bucket, channel *OpenChannel) erro return err } - decryptedPriv, err := ed.Decrypt(keyBytes[33:]) - if err != nil { - return err - } - - channel.OurCommitKey, _ = btcec.PrivKeyFromBytes(btcec.S256(), decryptedPriv) + channel.OurCommitKey, err = btcec.ParsePubKey(keyBytes[33:], btcec.S256()) if err != nil { return err } @@ -952,11 +942,8 @@ func putChanFundingInfo(nodeChanBucket *bolt.Bucket, channel *OpenChannel) error return err } - encryptedPriv, err := ed.Encrypt(channel.OurMultiSigKey.Serialize()) - if err != nil { - return err - } - if err := wire.WriteVarBytes(&b, 0, encryptedPriv); err != nil { + ourSerKey := channel.OurMultiSigKey.SerializeCompressed() + if err := wire.WriteVarBytes(&b, 0, ourSerKey); err != nil { return err } theirSerKey := channel.TheirMultiSigKey.SerializeCompressed() @@ -1002,17 +989,16 @@ func fetchChanFundingInfo(nodeChanBucket *bolt.Bucket, channel *OpenChannel) err return err } - encryptedPrivBytes, err := wire.ReadVarBytes(infoBytes, 0, 100, "") + ourKeyBytes, err := wire.ReadVarBytes(infoBytes, 0, 34, "") if err != nil { return err } - decryptedPriv, err := ed.Decrypt(encryptedPrivBytes) + channel.OurMultiSigKey, err = btcec.ParsePubKey(ourKeyBytes, btcec.S256()) if err != nil { return err } - channel.OurMultiSigKey, _ = btcec.PrivKeyFromBytes(btcec.S256(), decryptedPriv) - theirKeyBytes, err := wire.ReadVarBytes(infoBytes, 0, 33, "") + theirKeyBytes, err := wire.ReadVarBytes(infoBytes, 0, 34, "") if err != nil { return err } diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index c46efdef..b7ce9345 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -126,7 +126,7 @@ func TestOpenChannelPutGetDelete(t *testing.T) { TheirLNID: key, ChanID: id, MinFeePerKb: btcutil.Amount(5000), - OurCommitKey: privKey, + OurCommitKey: privKey.PubKey(), TheirCommitKey: pubKey, Capacity: btcutil.Amount(10000), OurBalance: btcutil.Amount(3000), @@ -136,7 +136,7 @@ func TestOpenChannelPutGetDelete(t *testing.T) { LocalElkrem: sender, RemoteElkrem: receiver, FundingOutpoint: testOutpoint, - OurMultiSigKey: privKey, + OurMultiSigKey: privKey.PubKey(), TheirMultiSigKey: privKey.PubKey(), FundingRedeemScript: script, TheirCurrentRevocation: privKey.PubKey(), @@ -177,8 +177,8 @@ func TestOpenChannelPutGetDelete(t *testing.T) { t.Fatalf("fee/kb doens't match") } - if !bytes.Equal(state.OurCommitKey.Serialize(), - newState.OurCommitKey.Serialize()) { + if !bytes.Equal(state.OurCommitKey.SerializeCompressed(), + newState.OurCommitKey.SerializeCompressed()) { t.Fatalf("our commit key dont't match") } if !bytes.Equal(state.TheirCommitKey.SerializeCompressed(), @@ -216,8 +216,8 @@ func TestOpenChannelPutGetDelete(t *testing.T) { t.Fatalf("funding outpoint doesn't match") } - if !bytes.Equal(state.OurMultiSigKey.Serialize(), - newState.OurMultiSigKey.Serialize()) { + if !bytes.Equal(state.OurMultiSigKey.SerializeCompressed(), + newState.OurMultiSigKey.SerializeCompressed()) { t.Fatalf("our multisig key doesn't match") } if !bytes.Equal(state.TheirMultiSigKey.SerializeCompressed(), From 3c4beddee14d276aeb64cc13526f33ccca5389aa Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:04:26 -0700 Subject: [PATCH 03/15] build: update glide files to pin against the master branch of roasbeef's btcwallet fork --- glide.lock | 14 +++++++------- glide.yaml | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/glide.lock b/glide.lock index 52221620..67be3561 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 348cab6c25a05211caed34dc711bd1cb5a51800bc87d3609127ff9271b206c42 -updated: 2016-09-02T08:34:25.843272647-04:00 +hash: d11bb1bf4ed6df842559ffff8fcf859509382395ba38fc29a0de546526ecbcd4 +updated: 2016-09-08T12:08:13.98576317-07:00 imports: - name: github.com/awalterschulze/gographviz version: cafbade2d58068c3992f12afe46742195c673d2b @@ -51,7 +51,7 @@ imports: - name: github.com/codahale/chacha20poly1305 version: f8a5c48301822c3d7dd26d78e68ea2968db0ab20 - name: github.com/davecgh/go-spew - version: 6cf5744a041a0022271cefed95ba843f6d87fd51 + version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 subpackages: - spew - name: github.com/golang/protobuf @@ -82,7 +82,7 @@ imports: - txsort - base58 - name: github.com/roasbeef/btcwallet - version: d7d402cc4135a53230ce068dcc51252c58a11a3d + version: 1fd2d6224698e14591d06f2a10b24e86494cc19f subpackages: - chain - waddrmgr @@ -102,7 +102,7 @@ imports: - name: github.com/urfave/cli version: a14d7d367bc02b1f57d88de97926727f2d936387 - name: golang.org/x/crypto - version: f160b6bf95857cd862817875dd958be022e587c4 + version: 9e590154d2353f3f5e1b24da7275686040dcf491 subpackages: - hkdf - nacl/secretbox @@ -113,7 +113,7 @@ imports: - pbkdf2 - ssh/terminal - name: golang.org/x/net - version: 1358eff22f0dd0c54fc521042cc607f6ff4b531a + version: 9313baa13d9262e49d07b20ed57dceafcd7240cc subpackages: - context - http2 @@ -122,7 +122,7 @@ imports: - lex/httplex - internal/timeseries - name: golang.org/x/sys - version: a646d33e2ee3172a661fc09bca23bb4889a41bc8 + version: 30de6d19a3bd89a5f38ae4028e23aaa5582648af subpackages: - unix - name: google.golang.org/grpc diff --git a/glide.yaml b/glide.yaml index eeb9e2a5..d616ba35 100644 --- a/glide.yaml +++ b/glide.yaml @@ -37,6 +37,7 @@ import: - hdkeychain - txsort - package: github.com/roasbeef/btcwallet + version: master subpackages: - chain - waddrmgr From a87ddeabdc17ea783171041fb20f8beec5bf221b Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:06:43 -0700 Subject: [PATCH 04/15] lnwallet: remove interaction with the btcutil.coinset package This commit removes the wrapper functions used to rely on the coinset package for coin selection. In a future commit the prior behavior will be replaced by a custom coin selection implementation which estimates the size of the funding transaction. --- lnwallet/coin_select.go | 73 ----------------------------------------- 1 file changed, 73 deletions(-) delete mode 100644 lnwallet/coin_select.go diff --git a/lnwallet/coin_select.go b/lnwallet/coin_select.go deleted file mode 100644 index e1d95491..00000000 --- a/lnwallet/coin_select.go +++ /dev/null @@ -1,73 +0,0 @@ -package lnwallet - -import ( - "encoding/hex" - - "github.com/roasbeef/btcd/btcjson" - "github.com/roasbeef/btcd/wire" - "github.com/roasbeef/btcutil" - "github.com/roasbeef/btcutil/coinset" -) - -// lnCoin represents a single unspet output. Its purpose is to convert a regular -// output to a struct adhering 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 creates a new "coin" from the passed output. Coins are required -// in order to perform coin selection upon. -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 converts a slice of transaction outputs to a coin-selectable -// slice of "Coins"s. -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 -} From 5449ba2b34e59214b16dff331ca51c4a79de3b9e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:24:22 -0700 Subject: [PATCH 05/15] lnwallet: revamp interfaces, add BlockChainIO and Singer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit revamps the previous WalletController interface, edging it closer to a more complete version. Additionally, this commit also introduces two new interfaces: BlockchainIO, and Singer along with a new factor driver struct, the WalletDriver. This BlockChainIO abstracts read-only access to the blockchain, while the Singer interface abstracts the signing of inputs from the base wallet paving the way to hardware wallets, air-gapped signing, etc. Finally, in order to provide an easy method for selecting a particular concrete implementation of a WalletController interface, the concept of registering “WalletDriver”s has been introduced. A wallet driver is essentially the encapsulation of a factory function capable of create a new instance of a Wallet Controller. --- lnwallet/interface.go | 267 +++++++++++++++++++++++++++++++++--------- 1 file changed, 212 insertions(+), 55 deletions(-) diff --git a/lnwallet/interface.go b/lnwallet/interface.go index 087556a7..a29dffcc 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -1,11 +1,43 @@ package lnwallet import ( + "errors" + "fmt" + "sync" + "github.com/roasbeef/btcd/btcec" + "github.com/roasbeef/btcd/txscript" "github.com/roasbeef/btcd/wire" "github.com/roasbeef/btcutil" ) +// ErrNotMine is an error denoting that a WalletController instance is unable +// to spend a specifid output. +var ErrNotMine = errors.New("the passed output doesn't belong to the wallet") + +// AddressType is a enum-like type which denotes the possible address types +// WalletController supports. +type AddressType uint8 + +const ( + // WitnessPubKey represents a p2wkh address. + WitnessPubKey AddressType = iota + + // NestedWitnessPubKey represents a p2sh output which is itself a + // nested p2wkh output. + NestedWitnessPubKey + + // PublicKey represents a regular p2pkh output. + PubKeyHash +) + +// Utxo is an unspent output denoted by its outpoint, and output value of the +// original output. +type Utxo struct { + Value btcutil.Amount + wire.OutPoint +} + // WalletController defines an abstract interface for controlling a local Pure // Go wallet, a local or remote wallet via an RPC mechanism, or possibly even // a daemon assisted hardware wallet. This interface serves the purpose of @@ -17,82 +49,71 @@ import ( // behavior of all interface methods in order to ensure identical behavior // across all concrete implementations. type WalletController interface { + // FetchInputInfo queries for the WalletController's knowledge of the + // passed outpoint. If the base wallet determines this output is under + // its control, then the original txout should be returned. Otherwise, + // a non-nil error value of ErrNotMine should be returned instead. + FetchInputInfo(prevOut *wire.OutPoint) (*wire.TxOut, error) + // ConfirmedBalance returns the sum of all the wallet's unspent outputs // that have at least confs confirmations. If confs is set to zero, // then all unspent outputs, including those currently in the mempool // will be included in the final sum. - ConfirmedBalance(confs int32) btcutil.Amount + ConfirmedBalance(confs int32, witness bool) (btcutil.Amount, error) - // NewAddress returns the next external address for the wallet. The - // type of address returned is dictated by the wallet's capabilities, - // and may be of type: p2sh, p2pkh, p2wkh, p2wsh, etc. - NewAddress(witness bool) (btcutil.Address, error) - - // NewChangeAddress returns a new change address for the wallet. If the - // underlying wallet supports hd key chains, then this address should be - // dervied from an internal branch. - NewChangeAddress(witness bool) (btcutil.Address, error) + // NewAddress returns the next external or internal address for the + // wallet dicatated by the value of the `change` paramter. If change is + // true, then an internal address should be used, otherwise an external + // address should be returned. The type of address returned is dictated + // by the wallet's capabilities, and may be of type: p2sh, p2pkh, + // p2wkh, p2wsh, etc. + NewAddress(addrType AddressType, change bool) (btcutil.Address, error) // GetPrivKey retrives the underlying private key associated with the // passed address. If the wallet is unable to locate this private key // due to the address not being under control of the wallet, then an // error should be returned. - GetPrivKey(a *btcutil.Address) (*btcec.PrivateKey, error) + // TODO(roasbeef): should instead take tadge's derivation scheme in + GetPrivKey(a btcutil.Address) (*btcec.PrivateKey, error) // NewRawKey returns a raw private key controlled by the wallet. These // keys are used for the 2-of-2 multi-sig outputs for funding // transactions, as well as the pub key used for commitment transactions. - // TODO(roasbeef): key pool due to cancelled reservations?? - NewRawKey() (*btcec.PrivateKey, error) + // TODO(roasbeef): may be scrapped, see above TODO + NewRawKey() (*btcec.PublicKey, error) - // FetchIdentityKey returns a private key which will be utilized as the - // wallet's Lightning Network identity for authentication purposes. - // TODO(roasbeef): rotate identity key? - FetchIdentityKey() (*btcec.PrivateKey, error) + // FetchRootKey returns a root key which will be used by the + // LightningWallet to deterministically generate secrets. The private + // key returned by this method should remain constant in-between + // WalletController restarts. + FetchRootKey() (*btcec.PrivateKey, error) - // FundTransaction creates a new unsigned transactions paying to the - // passed outputs, possibly using the specified change address. The - // includeFee parameter dictates if the wallet should also provide - // enough the funds necessary to create an adequate fee or not. - FundTransaction(outputs []*wire.TxOut, changeAddr btcutil.Address, - includeFee bool) (*wire.MsgTx, error) - - // SignTransaction performs potentially a sparse, or full signing of - // all inputs within the passed transaction that are spendable by the - // wallet. - SignTransaction(tx *wire.MsgTx) error - - // BroadcastTransaction performs cursory validation (dust checks, etc), - // then finally broadcasts the passed transaction to the Bitcoin network. - BroadcastTransaction(tx *wire.MsgTx) error - - // SendMany funds, signs, and broadcasts a Bitcoin transaction paying - // out to the specified outputs. In the case the wallet has insufficient - // funds, or the outputs are non-standard, and error should be returned. - SendMany(outputs []*wire.TxOut) (*wire.ShaHash, error) + // SendOutputs funds, signs, and broadcasts a Bitcoin transaction + // paying out to the specified outputs. In the case the wallet has + // insufficient funds, or the outputs are non-standard, and error + // should be returned. + SendOutputs(outputs []*wire.TxOut) (*wire.ShaHash, error) // ListUnspentWitness returns all unspent outputs which are version 0 // witness programs. The 'confirms' parameter indicates the minimum // number of confirmations an output needs in order to be returned by - // this method. Passing -1 as 'confirms' indicates that even unconfirmed - // outputs should be returned. - ListUnspentWitness(confirms int32) ([]*wire.OutPoint, error) + // this method. Passing -1 as 'confirms' indicates that even + // unconfirmed outputs should be returned. + ListUnspentWitness(confirms int32) ([]*Utxo, error) // LockOutpoint marks an outpoint as locked meaning it will no longer - // be deemed as eligble for coin selection. Locking outputs are utilized - // in order to avoid race conditions when selecting inputs for usage when - // funding a channel. + // be deemed as eligible for coin selection. Locking outputs are + // utilized in order to avoid race conditions when selecting inputs for + // usage when funding a channel. LockOutpoint(o wire.OutPoint) // UnlockOutpoint unlocks an previously locked output, marking it // eligible for coin seleciton. UnlockOutpoint(o wire.OutPoint) - // ImportScript imports the serialize public key script, or redeem - // script into the wallet's database. Scripts to be imported include - // the 2-of-2 script for funding transactions, commitment scripts, - // HTLCs scripts, and so on. - ImportScript(b []byte) error + // PublishTransaction performs cursory validation (dust checks, etc), + // then finally broadcasts the passed transaction to the Bitcoin network. + PublishTransaction(tx *wire.MsgTx) error // Start initializes the wallet, making any neccessary connections, // starting up required goroutines etc. @@ -101,11 +122,147 @@ type WalletController interface { // Stop signals the wallet for shutdown. Shutdown may entail closing // any active sockets, database handles, stopping goroutines, etc. Stop() error - - // WaitForShutdown blocks until the wallet finishes the shutdown - // procedure triggered by a prior call to Stop(). - WaitForShutdown() error - - // TODO(roasbeef): ImportPriv? - // * segwitty flag? +} + +// BlockChainIO is a dedicated source which will be used to obtain queries +// related to the current state of the blockchain. The data returned by each of +// the defined methods within this interface should always return the most up +// to date data possible. +// +// TODO(roasbeef): move to diff package perhaps? +type BlockChainIO interface { + // GetCurrentHeight returns the current height of the valid most-work + // chain the implementation is aware of. + GetCurrentHeight() (int32, error) + + // GetTxOut returns the original output referenced by the passed + // outpoint. + GetUtxo(txid *wire.ShaHash, index uint32) (*wire.TxOut, error) + + // GetTransaction returns the full transaction identified by the passed + // transaction ID. + GetTransaction(txid *wire.ShaHash) (*wire.MsgTx, error) +} + +// SignDescriptor houses the necessary information required to succesfully sign +// a given output. This struct is used by the Signer interface in order to gain +// access to critial data needed to generate a valid signature. +type SignDescriptor struct { + // Pubkey is the public key to which the signature should be generated + // over. The Signer should then generate a signature with the private + // key corresponding to this public key. + PubKey *btcec.PublicKey + + // RedeemScript is the full script required to properly redeem the + // output. This field will only be populated if a p2wsh or a p2sh + // output is being signed. + RedeemScript []byte + + // Output is the target output which should be signed. The PkScript and + // Value fields within the output should be properly populated, + // otherwise an invalid signature may be generated. + Output *wire.TxOut + + // HashType is the target sighash type that should be used when + // generating the final sighash, and signature. + HashType txscript.SigHashType + + // SigHashes is the pre-computed sighash midstate to be used when + // generating the final sighash for signing. + SigHashes *txscript.TxSigHashes + + // InputIndex is the target input within the transaction that should be + // signed. + InputIndex int +} + +// Signer represents an abstract object capable of generating raw signatures as +// well as full complete input scripts given a valid SignDescriptor and +// transaction. This interface fully abstracts away signing paving the way for +// Signer implementations such as hardware wallets, hardware tokens, HSM's, or +// simply a regular wallet. +type Signer interface { + // SignOutputRaw generates a signature for the passed transaction + // according to the data within the passed SignDescriptor. + // + // NOTE: The resulting signature should be void of a sighash byte. + SignOutputRaw(tx *wire.MsgTx, signDesc *SignDescriptor) ([]byte, error) + + // ComputeInputScript generates a complete InputIndex for the passed + // transaction with the signature as defined within the passed + // SignDescriptor. This method should be capable of generating the + // proper input script for both regular p2wkh output and p2wkh outputs + // nested within a regualr p2sh output. + ComputeInputScript(tx *wire.MsgTx, signDesc *SignDescriptor) (*InputScript, error) +} + +// WalletDriver represents a "driver" for a particular concrete +// WalletController implementation. A driver is indentified by a globally +// unique string identifier along with a 'New()' method which is responsible +// for initializing a particular WalletController concrete implementation. +type WalletDriver struct { + // WalletType is a string which uniquely identifes the WalletController + // that this driver, drives. + WalletType string + + // New creates a new instance of a concrete WalletController + // implementation given a variadic set up arguments. The function takes + // a varidaic number of interface paramters in order to provide + // initialization flexibility, thereby accomodating several potential + // WalletController implementations. + New func(args ...interface{}) (WalletController, error) +} + +var ( + wallets = make(map[string]*WalletDriver) + registerMtx sync.Mutex +) + +// RegisteredWallets returns a slice of all currently registered notifiers. +// +// NOTE: This function is safe for concurrent access. +func RegisteredWallets() []*WalletDriver { + registerMtx.Lock() + defer registerMtx.Unlock() + + registeredWallets := make([]*WalletDriver, 0, len(wallets)) + for _, wallet := range wallets { + registeredWallets = append(registeredWallets, wallet) + } + + return registeredWallets +} + +// RegisterWallet registers a WalletDriver which is capable of driving a +// concrete WalletController interface. In the case that this driver has +// already been registered, an error is returned. +// +// NOTE: This function is safe for concurrent access. +func RegisterWallet(driver *WalletDriver) error { + registerMtx.Lock() + defer registerMtx.Unlock() + + if _, ok := wallets[driver.WalletType]; ok { + return fmt.Errorf("wallet already registered") + } + + wallets[driver.WalletType] = driver + + return nil +} + +// SupportedWallets returns a slice of strings that represents the walelt +// drivers that have been registered and are therefore supported. +// +// NOTE: This function is safe for concurrent access. +func SupportedWallets() []string { + registerMtx.Lock() + defer registerMtx.Unlock() + + supportedWallets := make([]string, 0, len(wallets)) + for walletName := range wallets { + supportedWallets = append(supportedWallets, walletName) + } + + return supportedWallets } From 6a1d8d06820d126bbbbcd124fabf14d457840fb3 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:29:38 -0700 Subject: [PATCH 06/15] lnwallet: add a BtcWallet implementation of WalletController This commit adds the first concrete implementation of the WalletController interface: BtcWallet. This implementation is simply a series of wrapper functions are the base btcwallet struct. Additionally, for ease of use both the BlockChain IO and Signer interface are also implemented by BtcWallet. Finally a new WalletDriver implementation has been implemented, and will be register by the init() method within this new package. --- lnwallet/btcwallet/blockchain.go | 55 +++++ lnwallet/btcwallet/btcwallet.go | 380 +++++++++++++++++++++++++++++++ lnwallet/btcwallet/config.go | 94 ++++++++ lnwallet/btcwallet/driver.go | 45 ++++ lnwallet/btcwallet/signer.go | 174 ++++++++++++++ lnwallet/config.go | 67 +----- 6 files changed, 759 insertions(+), 56 deletions(-) create mode 100644 lnwallet/btcwallet/blockchain.go create mode 100644 lnwallet/btcwallet/btcwallet.go create mode 100644 lnwallet/btcwallet/config.go create mode 100644 lnwallet/btcwallet/driver.go create mode 100644 lnwallet/btcwallet/signer.go diff --git a/lnwallet/btcwallet/blockchain.go b/lnwallet/btcwallet/blockchain.go new file mode 100644 index 00000000..bc6237ec --- /dev/null +++ b/lnwallet/btcwallet/blockchain.go @@ -0,0 +1,55 @@ +package btcwallet + +import ( + "encoding/hex" + + "github.com/roasbeef/btcd/wire" +) + +// GetCurrentHeight returns the current height of the known block within the +// main chain. +// +// This method is a part of the lnwallet.BlockChainIO interface. +func (b *BtcWallet) GetCurrentHeight() (int32, error) { + _, height, err := b.rpc.GetBestBlock() + if err != nil { + return 0, err + } + + return height, nil +} + +// GetTxOut returns the original output referenced by the passed outpoint. +// +// This method is a part of the lnwallet.BlockChainIO interface. +func (b *BtcWallet) GetUtxo(txid *wire.ShaHash, index uint32) (*wire.TxOut, error) { + txout, err := b.rpc.GetTxOut(txid, index, false) + if err != nil { + return nil, err + } + + pkScript, err := hex.DecodeString(txout.ScriptPubKey.Hex) + if err != nil { + return nil, err + } + + return &wire.TxOut{ + // Sadly, gettxout returns the output value in BTC + // instead of satoshis. + Value: int64(txout.Value) * 1e8, + PkScript: pkScript, + }, nil +} + +// GetTransaction returns the full transaction identified by the passed +// transaction ID. +// +// This method is a part of the lnwallet.BlockChainIO interface. +func (b *BtcWallet) GetTransaction(txid *wire.ShaHash) (*wire.MsgTx, error) { + tx, err := b.rpc.GetRawTransaction(txid) + if err != nil { + return nil, err + } + + return tx.MsgTx(), nil +} diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go new file mode 100644 index 00000000..29b9f933 --- /dev/null +++ b/lnwallet/btcwallet/btcwallet.go @@ -0,0 +1,380 @@ +package btcwallet + +import ( + "encoding/hex" + "fmt" + "math" + "sync" + + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/roasbeef/btcd/btcec" + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcd/txscript" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" + "github.com/roasbeef/btcwallet/chain" + "github.com/roasbeef/btcwallet/waddrmgr" + base "github.com/roasbeef/btcwallet/wallet" + "github.com/roasbeef/btcwallet/walletdb" +) + +const ( + defaultAccount = uint32(waddrmgr.DefaultAccountNum) +) + +var ( + lnNamespace = []byte("ln") + rootKey = []byte("ln-root") +) + +// BtcWallet is an implementation of the lnwallet.WalletController interface +// backed by an active instance of btcwallet. At the time of the writing of +// this documentation, this implementation requires a full btcd node to +// operate. +type BtcWallet struct { + // wallet is an active instance of btcwallet. + wallet *base.Wallet + + // rpc is an an active RPC connection to btcd full-node. + rpc *chain.RPCClient + + // lnNamespace is a namespace within btcwallet's walletdb used to store + // persistent state required by the WalletController interface but not + // natively supported by btcwallet. + lnNamespace walletdb.Namespace + + netParams *chaincfg.Params + + // utxoCache is a cache used to speed up repeated calls to + // FetchInputInfo. + utxoCache map[wire.OutPoint]*wire.TxOut + cacheMtx sync.RWMutex +} + +// A compile time check to ensure that BtcWallet implements the +// WalletController interface. +var _ lnwallet.WalletController = (*BtcWallet)(nil) + +// New returns a new fully initialized instance of BtcWallet given a valid +// confirguration struct. +func New(cfg *Config) (*BtcWallet, error) { + // Ensure the wallet exists or create it when the create flag is set. + netDir := networkDir(cfg.DataDir, cfg.NetParams) + + var pubPass []byte + if cfg.PublicPass == nil { + pubPass = defaultPubPassphrase + } else { + pubPass = cfg.PublicPass + } + + loader := base.NewLoader(cfg.NetParams, netDir) + walletExists, err := loader.WalletExists() + if err != nil { + return nil, err + } + + var wallet *base.Wallet + if !walletExists { + // Wallet has never been created, perform initial set up. + wallet, err = loader.CreateNewWallet(pubPass, cfg.PrivatePass, + cfg.HdSeed) + if err != nil { + return nil, err + } + } else { + // Wallet has been created and been initialized at this point, open it + // along with all the required DB namepsaces, and the DB itself. + wallet, err = loader.OpenExistingWallet(pubPass, false) + if err != nil { + return nil, err + } + } + + if err := wallet.Manager.Unlock(cfg.PrivatePass); err != nil { + return nil, err + } + + // Create a special websockets rpc client for btcd which will be used + // by the wallet for notifications, calls, etc. + rpcc, err := chain.NewRPCClient(cfg.NetParams, cfg.RpcHost, + cfg.RpcUser, cfg.RpcPass, cfg.CACert, false, 20) + if err != nil { + return nil, err + } + + db := wallet.Database() + walletNamespace, err := db.Namespace(lnNamespace) + if err != nil { + return nil, err + } + + return &BtcWallet{ + wallet: wallet, + rpc: rpcc, + lnNamespace: walletNamespace, + netParams: cfg.NetParams, + utxoCache: make(map[wire.OutPoint]*wire.TxOut), + }, nil +} + +// Start initializes the underlying rpc connection, the wallet itself, and +// begins syncing to the current available blockchain state. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) Start() error { + // Establish an RPC connection in additino to starting the goroutines + // in the underlying wallet. + if err := b.rpc.Start(); err != nil { + return err + } + + // Start the underlying btcwallet core. + b.wallet.Start() + + // Pass the rpc client into the wallet so it can sync up to the + // current main chain. + b.wallet.SynchronizeRPC(b.rpc) + + return nil +} + +// Stop signals the wallet for shutdown. Shutdown may entail closing +// any active sockets, database handles, stopping goroutines, etc. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) Stop() error { + b.wallet.Stop() + + b.wallet.WaitForShutdown() + + b.rpc.Shutdown() + + return nil +} + +// ConfirmedBalance returns the sum of all the wallet's unspent outputs that +// have at least confs confirmations. If confs is set to zero, then all unspent +// outputs, including those currently in the mempool will be included in the +// final sum. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) ConfirmedBalance(confs int32, witness bool) (btcutil.Amount, error) { + var balance btcutil.Amount + + if witness { + witnessOutputs, err := b.ListUnspentWitness(confs) + if err != nil { + return 0, err + } + + for _, witnessOutput := range witnessOutputs { + balance += witnessOutput.Value + } + } else { + outputSum, err := b.wallet.CalculateBalance(confs) + if err != nil { + return 0, err + } + + balance = outputSum + } + + return balance, nil +} + +// NewAddress returns the next external or internal address for the wallet +// dicatated by the value of the `change` paramter. If change is true, then an +// internal address will be returned, otherwise an external address should be +// returned. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) NewAddress(t lnwallet.AddressType, change bool) (btcutil.Address, error) { + var addrType waddrmgr.AddressType + + switch t { + case lnwallet.WitnessPubKey: + addrType = waddrmgr.WitnessPubKey + case lnwallet.NestedWitnessPubKey: + addrType = waddrmgr.NestedWitnessPubKey + case lnwallet.PubKeyHash: + addrType = waddrmgr.PubKeyHash + default: + return nil, fmt.Errorf("unknown address type") + } + + if change { + return b.wallet.NewAddress(defaultAccount, addrType) + } else { + return b.wallet.NewChangeAddress(defaultAccount, addrType) + } +} + +// GetPrivKey retrives the underlying private key associated with the passed +// address. If the we're unable to locate the proper private key, then a +// non-nil error will be returned. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) GetPrivKey(a btcutil.Address) (*btcec.PrivateKey, error) { + // Using the ID address, request the private key coresponding to the + // address from the wallet's address manager. + walletAddr, err := b.wallet.Manager.Address(a) + if err != nil { + return nil, err + } + + return walletAddr.(waddrmgr.ManagedPubKeyAddress).PrivKey() +} + +// NewRawKey retrieves the next key within our HD key-chain for use within as a +// multi-sig key within the funding transaction, or within the commitment +// transaction's outputs. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) NewRawKey() (*btcec.PublicKey, error) { + nextAddr, err := b.wallet.Manager.NextExternalAddresses(defaultAccount, + 1, waddrmgr.WitnessPubKey) + if err != nil { + return nil, err + } + + pkAddr := nextAddr[0].(waddrmgr.ManagedPubKeyAddress) + + return pkAddr.PubKey(), nil +} + +// FetchRootKey returns a root key which is meanted to be used as an initial +// seed/salt to generate any Lightning specific secrets. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) FetchRootKey() (*btcec.PrivateKey, error) { + // Fetch the root address hash from the database, this is persisted + // locally within the database, then used to obtain the key from the + // wallet based on the address hash. + var rootAddrHash []byte + if err := b.lnNamespace.Update(func(tx walletdb.Tx) error { + rootBucket := tx.RootBucket() + + rootAddrHash = rootBucket.Get(rootKey) + return nil + }); err != nil { + return nil, err + } + + if rootAddrHash == nil { + // Otherwise, we need to generate a fresh address from the + // wallet, then stores it's hash160 within the database so we + // can look up the exact key later. + rootAddr, err := b.wallet.Manager.NextExternalAddresses(defaultAccount, + 1, waddrmgr.WitnessPubKey) + if err != nil { + return nil, err + } + + if err := b.lnNamespace.Update(func(tx walletdb.Tx) error { + rootBucket := tx.RootBucket() + + rootAddrHash = rootAddr[0].Address().ScriptAddress() + if err := rootBucket.Put(rootKey, rootAddrHash); err != nil { + return err + } + + return nil + }); err != nil { + return nil, err + } + } + + // With the root address hash obtained, generate the corresponding + // address, then retrieve the managed address from the wallet which + // will allow us to obtain the private key. + rootAddr, err := btcutil.NewAddressWitnessPubKeyHash(rootAddrHash, + b.netParams) + if err != nil { + return nil, err + } + walletAddr, err := b.wallet.Manager.Address(rootAddr) + if err != nil { + return nil, err + } + + return walletAddr.(waddrmgr.ManagedPubKeyAddress).PrivKey() +} + +// SendOutputs funds, signs, and broadcasts a Bitcoin transaction paying out to +// the specified outputs. In the case the wallet has insufficient funds, or the +// outputs are non-standard, a non-nil error will be be returned. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) SendOutputs(outputs []*wire.TxOut) (*wire.ShaHash, error) { + return b.wallet.SendOutputs(outputs, defaultAccount, 1) +} + +// LockOutpoint marks an outpoint as locked meaning it will no longer be deemed +// as eligible for coin selection. Locking outputs are utilized in order to +// avoid race conditions when selecting inputs for usage when funding a +// channel. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) LockOutpoint(o wire.OutPoint) { + b.wallet.LockOutpoint(o) +} + +// UnlockOutpoint unlocks an previously locked output, marking it eligible for +// coin seleciton. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) UnlockOutpoint(o wire.OutPoint) { + b.wallet.UnlockOutpoint(o) +} + +// ListUnspentWitness returns a slice of all the unspent outputs the wallet +// controls which pay to witness programs either directly or indirectly. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) ListUnspentWitness(minConfs int32) ([]*lnwallet.Utxo, error) { + // First, grab all the unfiltered currently unspent outputs. + maxConfs := int32(math.MaxInt32) + unspentOutputs, err := b.wallet.ListUnspent(minConfs, maxConfs, nil) + if err != nil { + return nil, err + } + + // Next, we'll run through all the regular outputs, only saving those + // which are p2wkh outputs or a p2wsh output nested within a p2sh output. + witnessOutputs := make([]*lnwallet.Utxo, 0, len(unspentOutputs)) + for _, output := range unspentOutputs { + pkScript, err := hex.DecodeString(output.ScriptPubKey) + if err != nil { + return nil, err + } + + // TODO(roasbeef): this assumes all p2sh outputs returned by + // the wallet are nested p2sh... + if txscript.IsPayToWitnessPubKeyHash(pkScript) || + txscript.IsPayToScriptHash(pkScript) { + txid, err := wire.NewShaHashFromStr(output.TxID) + if err != nil { + return nil, err + } + + utxo := &lnwallet.Utxo{ + Value: btcutil.Amount(output.Amount * 1e8), + OutPoint: wire.OutPoint{ + Hash: *txid, + Index: output.Vout, + }, + } + witnessOutputs = append(witnessOutputs, utxo) + } + + } + + return witnessOutputs, nil +} + +// PublishTransaction performs cursory validation (dust checks, etc), then +// finally broadcasts the passed transaction to the Bitcoin network. +func (b *BtcWallet) PublishTransaction(tx *wire.MsgTx) error { + return b.wallet.PublishTransaction(tx) +} diff --git a/lnwallet/btcwallet/config.go b/lnwallet/btcwallet/config.go new file mode 100644 index 00000000..5b19eafb --- /dev/null +++ b/lnwallet/btcwallet/config.go @@ -0,0 +1,94 @@ +package btcwallet + +import ( + "path/filepath" + + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" + _ "github.com/roasbeef/btcwallet/walletdb/bdb" +) + +var ( + lnwalletHomeDir = btcutil.AppDataDir("lnwallet", false) + defaultDataDir = lnwalletHomeDir + + defaultLogFilename = "lnwallet.log" + defaultLogDirname = "logs" + defaultLogDir = filepath.Join(lnwalletHomeDir, defaultLogDirname) + + btcdHomeDir = btcutil.AppDataDir("btcd", false) + btcdHomedirCAFile = filepath.Join(btcdHomeDir, "rpc.cert") + defaultRPCKeyFile = filepath.Join(lnwalletHomeDir, "rpc.key") + defaultRPCCertFile = filepath.Join(lnwalletHomeDir, "rpc.cert") + + // 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") + + walletDbName = "lnwallet.db" +) + +// Config is a struct which houses configuration paramters which modify the +// instance of BtcWallet generated by the New() function. +type Config struct { + // DataDir is the name of the directory where the wallet's persistent + // state should be sotred. + DataDir string + + // LogDir is the name of the directory which should be used to store + // generated log files. + LogDir string + + // DebugLevel is a string representing the level of verbosity the + // logger should use. + DebugLevel string + + // RpcHost is the host and port to use to reach the rpc sever. + RpcHost string // localhost:18334 + + // RpcUser is the username which should be used to authentiate with the + // rpc server. + RpcUser string + + // RpcPass is the password which should be used to authenticate the + // connection with the RPC server. + RpcPass string + + // RpcNoTLS denotes if a TLS connection should be attempted when + // connecting to the RPC server. + RpcNoTLS bool + + // RPCCert directory where the TLS certificate of the RPC sever is + // stored. If the RpcNoTLS is false, then this value will be unused. + RPCCert string + RPCKey string + + // CACert is the raw RPC cert for btcd. + CACert []byte + + PrivatePass []byte + PublicPass []byte + HdSeed []byte + + NetParams *chaincfg.Params +} + +// 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) +} diff --git a/lnwallet/btcwallet/driver.go b/lnwallet/btcwallet/driver.go new file mode 100644 index 00000000..828633ed --- /dev/null +++ b/lnwallet/btcwallet/driver.go @@ -0,0 +1,45 @@ +package btcwallet + +import ( + "fmt" + + "github.com/lightningnetwork/lnd/lnwallet" +) + +const ( + walletType = "btcwallet" +) + +// createNewWallet creates a new instance of BtcWallet given the proper list of +// initialization parameters. This function is the factory function required to +// properly create an instance of the lnwallet.WalletDriver struct for +// BtcWallet. +func createNewWallet(args ...interface{}) (lnwallet.WalletController, error) { + if len(args) != 1 { + return nil, fmt.Errorf("incorrect number of arguments to .New(...), "+ + "expected 1, instead passed %v", len(args)) + } + + config, ok := args[0].(*Config) + if !ok { + return nil, fmt.Errorf("first argument to btcdnotifier.New is " + + "incorrect, expected a *btcrpcclient.ConnConfig") + } + + return New(config) +} + +// init registers a driver for the BtcWallet concrete implementation of the +// lnwallet.WalletController interface. +func init() { + // Register the driver. + driver := &lnwallet.WalletDriver{ + WalletType: walletType, + New: createNewWallet, + } + + if err := lnwallet.RegisterWallet(driver); err != nil { + panic(fmt.Sprintf("failed to register wallet driver '%s': %v", + walletType, err)) + } +} diff --git a/lnwallet/btcwallet/signer.go b/lnwallet/btcwallet/signer.go new file mode 100644 index 00000000..67bd394d --- /dev/null +++ b/lnwallet/btcwallet/signer.go @@ -0,0 +1,174 @@ +package btcwallet + +import ( + "fmt" + + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/roasbeef/btcd/txscript" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" + "github.com/roasbeef/btcwallet/waddrmgr" +) + +// FetchInputInfo queries for the WalletController's knowledge of the passed +// outpoint. If the base wallet determines this output is under its control, +// then the original txout should be returned. Otherwise, a non-nil error value +// of ErrNotMine should be returned instead. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.TxOut, error) { + var ( + err error + output *wire.TxOut + ) + + // First check to see if the output is already within the utxo cache. + // If so we can return directly saving usk a disk access. + b.cacheMtx.RLock() + if output, ok := b.utxoCache[*prevOut]; ok { + b.cacheMtx.RUnlock() + return output, nil + } + b.cacheMtx.RUnlock() + + // Otherwse, we manually look up the output within the tx store. + txDetail, err := b.wallet.TxStore.TxDetails(&prevOut.Hash) + if err != nil { + return nil, err + } else if txDetail == nil { + return nil, lnwallet.ErrNotMine + } + + output = txDetail.TxRecord.MsgTx.TxOut[prevOut.Index] + + b.cacheMtx.Lock() + b.utxoCache[*prevOut] = output + b.cacheMtx.Unlock() + + return output, nil +} + +// fetchOutputKey attempts to fetch the managed address corresponding to the +// passed output script. This function is used to look up the proper key which +// should be used to sign a specified input. +func (b *BtcWallet) fetchOutputAddr(script []byte) (waddrmgr.ManagedAddress, error) { + _, addrs, _, err := txscript.ExtractPkScriptAddrs(script, b.netParams) + if err != nil { + return nil, err + } + + // If the case of a multi-sig output, several address may be extracted. + // Therefore, we simply select the key for the first address we know + // of. + for _, addr := range addrs { + wAddr, err := b.wallet.Manager.Address(addr) + if err == nil { + return wAddr, nil + } + } + + // TODO(roasbeef): use the errors.wrap package + return nil, fmt.Errorf("address not found") +} + +// SignOutputRaw generates a signature for the passed transaction according to +// the data within the passed SignDescriptor. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) SignOutputRaw(tx *wire.MsgTx, signDesc *lnwallet.SignDescriptor) ([]byte, error) { + redeemScript := signDesc.RedeemScript + walletAddr, err := b.fetchOutputAddr(redeemScript) + if err != nil { + return nil, err + } + + privKey, err := walletAddr.(waddrmgr.ManagedPubKeyAddress).PrivKey() + if err != nil { + return nil, err + } + + amt := signDesc.Output.Value + sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes, 0, + amt, redeemScript, txscript.SigHashAll, privKey) + if err != nil { + return nil, err + } + + // Chop off the sighash flag at the end of the signature. + return sig[:len(sig)-1], nil +} + +// ComputeInputScript generates a complete InputIndex for the passed +// transaction with the signature as defined within the passed SignDescriptor. +// This method is capable of generating the proper input script for both +// regular p2wkh output and p2wkh outputs nested within a regualr p2sh output. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) ComputeInputScript(tx *wire.MsgTx, + signDesc *lnwallet.SignDescriptor) (*lnwallet.InputScript, error) { + + outputScript := signDesc.Output.PkScript + walletAddr, err := b.fetchOutputAddr(outputScript) + if err != nil { + return nil, nil + } + + pka := walletAddr.(waddrmgr.ManagedPubKeyAddress) + privKey, err := pka.PrivKey() + if err != nil { + return nil, err + } + + var witnessProgram []byte + inputScript := &lnwallet.InputScript{} + + // If we're spending p2wkh output nested within a p2sh output, then + // we'll need to attach a sigScript in addition to witness data. + switch { + case pka.IsNestedWitness(): + pubKey := privKey.PubKey() + pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed()) + + // Next, we'll generate a valid sigScript that'll allow us to + // spend the p2sh output. The sigScript will contain only a + // single push of the p2wkh witness program corresponding to + // the matching public key of this address. + p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, + b.netParams) + if err != nil { + return nil, err + } + witnessProgram, err = txscript.PayToAddrScript(p2wkhAddr) + if err != nil { + return nil, err + } + + bldr := txscript.NewScriptBuilder() + bldr.AddData(witnessProgram) + sigScript, err := bldr.Script() + if err != nil { + return nil, err + } + + inputScript.ScriptSig = sigScript + // Otherwise, this is a regular p2wkh output, so we include the + // witness program itself as the subscript to generate the proper + // sighash digest. As part of the new sighash digest algorithm, the + // p2wkh witness program will be expanded into a regular p2kh + // script. + default: + witnessProgram = outputScript + } + + // Generate a valid witness stack for the input. + witnessScript, err := txscript.WitnessScript(tx, signDesc.SigHashes, + signDesc.InputIndex, signDesc.Output.Value, witnessProgram, + txscript.SigHashAll, privKey, true) + if err != nil { + return nil, err + } + + inputScript.Witness = witnessScript + + return inputScript, nil +} diff --git a/lnwallet/config.go b/lnwallet/config.go index 8547c1c8..f579fde5 100644 --- a/lnwallet/config.go +++ b/lnwallet/config.go @@ -1,60 +1,15 @@ package lnwallet -import ( - "path/filepath" - - "github.com/roasbeef/btcd/chaincfg" - "github.com/roasbeef/btcutil" -) - -var ( - // TODO(roasbeef): lnwallet config file - lnwalletHomeDir = btcutil.AppDataDir("lnwallet", false) - defaultDataDir = lnwalletHomeDir - - defaultLogFilename = "lnwallet.log" - defaultLogDirname = "logs" - defaultLogDir = filepath.Join(lnwalletHomeDir, defaultLogDirname) - - btcdHomeDir = btcutil.AppDataDir("btcd", false) - btcdHomedirCAFile = filepath.Join(btcdHomeDir, "rpc.cert") - defaultRPCKeyFile = filepath.Join(lnwalletHomeDir, "rpc.key") - defaultRPCCertFile = filepath.Join(lnwalletHomeDir, "rpc.cert") - - // 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") - - walletDbName = "lnwallet.db" -) - -// Config... +// Config.. type Config struct { - DataDir string - LogDir string - - DebugLevel string - - RpcHost string // localhost:18334 - RpcUser string - RpcPass string - RpcNoTLS bool - - RPCCert string - RPCKey string - - CACert []byte - - PrivatePass []byte - PublicPass []byte - HdSeed []byte - - // Which bitcoin network are we using? - NetParams *chaincfg.Params -} - -// setDefaults... -func setDefaults(confg *Config) { + // default csv time + // default cltv time + // default wait for funding time + // default wait for closure time + // min amount to accept channel + // min fee imformation + // * or possibly interface to predict fees + // max htlcs in flight? + // possible secret derivation functions + // } From 773c831561be76d904005df7dcca8433e76e7e28 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:35:33 -0700 Subject: [PATCH 07/15] lnwallet: replace naive coin selection a size+fee aware version --- lnwallet/wallet.go | 185 +++++++++++++++++++++++++++++++-------------- 1 file changed, 130 insertions(+), 55 deletions(-) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index c5b69a35..6afd07d0 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -502,7 +502,9 @@ func (l *LightningWallet) handleFundingReserveRequest(req *initFundingReserveMsg // don't need to perform any coin selection. Otherwise, attempt to // obtain enough coins to meet the required funding amount. if req.fundingAmount != 0 { - if err := l.selectCoinsAndChange(req.fundingAmount, + // TODO(roasbeef): consult model for proper fee rate + feeRate := uint64(10) + if err := l.selectCoinsAndChange(feeRate, req.fundingAmount, ourContribution); err != nil { req.err <- err req.resp <- nil @@ -1327,7 +1329,7 @@ func (l *LightningWallet) ListUnspentWitness(minConfs int32) ([]*btcjson.ListUns // the passed contribution's inputs. If necessary, a change address will also be // generated. // TODO(roasbeef): remove hardcoded fees and req'd confs for outputs. -func (l *LightningWallet) selectCoinsAndChange(numCoins btcutil.Amount, +func (l *LightningWallet) selectCoinsAndChange(feeRate uint64, amt btcutil.Amount, contribution *ChannelContribution) error { // We hold the coin select mutex while querying for outputs, and @@ -1337,22 +1339,10 @@ func (l *LightningWallet) selectCoinsAndChange(numCoins btcutil.Amount, // when we encounter an error condition. l.coinSelectMtx.Lock() - // TODO(roasbeef): check if balance is insufficient, if so then select - // on two channels, one is a time.After that will bail out with - // insuffcient funds, the other is a notification that the balance has - // been updated make(chan struct{}, 1). - // Find all unlocked unspent witness outputs with greater than 1 // confirmation. // TODO(roasbeef): make num confs a configuration paramter - unspentOutputs, err := l.ListUnspentWitness(1) - if err != nil { - l.coinSelectMtx.Unlock() - return err - } - - // Convert the outputs to coins for coin selection below. - coins, err := outputsToCoins(unspentOutputs) + coins, err := l.ListUnspentWitness(1) if err != nil { l.coinSelectMtx.Unlock() return err @@ -1360,17 +1350,8 @@ func (l *LightningWallet) selectCoinsAndChange(numCoins btcutil.Amount, // 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. - // NOTE: this current selection assumes "priority" is still a thing. - selector := &coinset.MaxValueAgeCoinSelector{ - MaxInputs: 10, - MinChangeAmount: 10000, - } - // TODO(roasbeef): don't hardcode fee... - totalWithFee := numCoins + 10000 - selectedCoins, err := selector.CoinSelect(totalWithFee, coins) + // TODO(roasbeef): take in sat/byte + selectedCoins, changeAmt, err := coinSelect(feeRate, amt, coins) if err != nil { l.coinSelectMtx.Unlock() return err @@ -1379,42 +1360,37 @@ func (l *LightningWallet) selectCoinsAndChange(numCoins btcutil.Amount, // 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. - contribution.Inputs = make([]*wire.TxIn, len(selectedCoins.Coins())) - for i, coin := range selectedCoins.Coins() { - txout := wire.NewOutPoint(coin.Hash(), coin.Index()) - l.LockOutpoint(*txout) + contribution.Inputs = make([]*wire.TxIn, len(selectedCoins)) + for i, coin := range selectedCoins { + l.lockedOutPoints[*coin] = struct{}{} + l.LockOutpoint(*coin) // 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()) - contribution.Inputs[i] = wire.NewTxIn(outPoint, nil, nil) + contribution.Inputs[i] = wire.NewTxIn(coin, nil, nil) + } + + // Record any change output(s) generated as a result of the coin + // selection. + if changeAmt != 0 { + changeAddr, err := l.NewAddress(WitnessPubKey, true) + if err != nil { + return err + } + changeScript, err := txscript.PayToAddrScript(changeAddr) + if err != nil { + return err + } + + contribution.ChangeOutputs = make([]*wire.TxOut, 1) + contribution.ChangeOutputs[0] = &wire.TxOut{ + Value: int64(changeAmt), + PkScript: changeScript, + } } l.coinSelectMtx.Unlock() - // Create some possibly neccessary change outputs. - selectedTotalValue := coinset.NewCoinSet(selectedCoins.Coins()).TotalValue() - if selectedTotalValue > totalWithFee { - // Change is necessary. Query for an available change address to - // send the remainder to. - contribution.ChangeOutputs = make([]*wire.TxOut, 1) - changeAddr, err := l.NewChangeAddress(waddrmgr.DefaultAccountNum, - waddrmgr.WitnessPubKey) - if err != nil { - return err - } - - changeAddrScript, err := txscript.PayToAddrScript(changeAddr) - if err != nil { - return err - } - - changeAmount := selectedTotalValue - totalWithFee - contribution.ChangeOutputs[0] = wire.NewTxOut(int64(changeAmount), - changeAddrScript) - } - - // TODO(roasbeef): re-calculate fees here to minFeePerKB, may need more inputs return nil } @@ -1428,8 +1404,107 @@ func (w *WaddrmgrEncryptorDecryptor) Encrypt(p []byte) ([]byte, error) { func (w *WaddrmgrEncryptorDecryptor) Decrypt(c []byte) ([]byte, error) { return w.M.Decrypt(waddrmgr.CKTPrivate, c) +// selectInputs selects a slice of inputs necessary to meet the specified +// selection amount. If input selectino is unable to suceed to to insuffcient +// funds, a non-nil error is returned. Additionally, the total amount of the +// selected coins are returned in order for the caller to properly handle +// change+fees. +func selectInputs(amt btcutil.Amount, coins []*Utxo) (btcutil.Amount, []*wire.OutPoint, error) { + var ( + selectedUtxos []*wire.OutPoint + satSelected btcutil.Amount + ) + + i := 0 + for satSelected < amt { + // If we're about to go past the number of available coins, + // then exit with an error. + if i > len(coins)-1 { + return 0, nil, ErrInsufficientFunds + } + + // Otherwise, collect this new coin as it may be used for final + // coin selection. + coin := coins[i] + utxo := &wire.OutPoint{ + Hash: coin.Hash, + Index: coin.Index, + } + + selectedUtxos = append(selectedUtxos, utxo) + satSelected += coin.Value + + i++ + } + + return satSelected, selectedUtxos, nil } func (w *WaddrmgrEncryptorDecryptor) OverheadSize() uint32 { return 24 +// coinSelect attemps to select a sufficient amount of coins, including a +// change output to fund amt satoshis, adhearing to the specified fee rate. The +// specified fee rate should be expressed in sat/byte for coin selection to +// function properly. +func coinSelect(feeRate uint64, amt btcutil.Amount, + coins []*Utxo) ([]*wire.OutPoint, btcutil.Amount, error) { + + const ( + // txOverhead is the overhead of a transaction residing within + // the version number and lock time. + txOverhead = 8 + + // p2wkhSpendSize an estimate of the number of bytes it takes + // to spend a p2wkh output. + // + // (p2wkh witness) + txid + index + varint script size + sequence + // TODO(roasbeef): div by 3 due to witness size? + p2wkhSpendSize = (1 + 73 + 1 + 33) + 32 + 4 + 1 + 4 + + // p2wkhOutputSize is an estimate of the size of a regualr + // p2wkh output. + // + // 8 (output) + 1 (var int script) + 22 (p2wkh output) + p2wkhOutputSize = 8 + 1 + 22 + + // p2wkhOutputSize is an estimate of the p2wsh funding uotput. + p2wshOutputSize = 8 + 1 + 34 + ) + + var estimatedSize int + + amtNeeded := amt + for { + // First perform an initial round of coin selection to estimate + // the required fee. + totalSat, selectedUtxos, err := selectInputs(amtNeeded, coins) + if err != nil { + return nil, 0, err + } + + // Based on the selected coins, estimate the size of the final + // fully signed transaction. + estimatedSize = ((len(selectedUtxos) * p2wkhSpendSize) + + p2wshOutputSize + txOverhead) + + // The difference bteween the selected amount and the amount + // requested will be used to pay fees, and generate a change + // output with the remaining. + overShootAmt := totalSat - amtNeeded + + // Based on the estimated size and fee rate, if the excess + // amount isn't enough to pay fees, then increase the requested + // coin amount by the estimate required fee, performing another + // round of coin selection. + requiredFee := btcutil.Amount(uint64(estimatedSize) * feeRate) + if overShootAmt < requiredFee { + amtNeeded += requiredFee + continue + } + + // If the fee is sufficient, then calculate the size of the change output. + changeAmt := overShootAmt - requiredFee + + return selectedUtxos, changeAmt, nil + } } From 044b65317ac4e5f5b2990331a9ecc0bf4b874658 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:36:39 -0700 Subject: [PATCH 08/15] lnwallet: remove setup.go This file is no longer needed as each implementation of the WalletController is expected to handle its own set up via an instance of the WalletDriver factory struct. --- lnwallet/setup.go | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 lnwallet/setup.go diff --git a/lnwallet/setup.go b/lnwallet/setup.go deleted file mode 100644 index b11ecb86..00000000 --- a/lnwallet/setup.go +++ /dev/null @@ -1,26 +0,0 @@ -package lnwallet - -import ( - "path/filepath" - - "github.com/roasbeef/btcd/chaincfg" - "github.com/roasbeef/btcd/wire" - _ "github.com/roasbeef/btcwallet/walletdb/bdb" -) - -// 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) -} From 564316a84677e0efa596f20962c4cde34b93c381 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:37:35 -0700 Subject: [PATCH 09/15] lnwallet: publicly export several functions within script_utils.go --- lnwallet/script_utils.go | 17 +++++++++-------- lnwallet/script_utils_test.go | 10 +++++----- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lnwallet/script_utils.go b/lnwallet/script_utils.go index 942c3958..2094239e 100644 --- a/lnwallet/script_utils.go +++ b/lnwallet/script_utils.go @@ -58,9 +58,9 @@ func genMultiSigScript(aPub, bPub []byte) ([]byte, error) { return bldr.Script() } -// genFundingPkScript creates a redeem script, and its matching p2wsh +// GenFundingPkScript creates a redeem script, and its matching p2wsh // output for the funding transaction. -func genFundingPkScript(aPub, bPub []byte, amt int64) ([]byte, *wire.TxOut, error) { +func GenFundingPkScript(aPub, bPub []byte, amt int64) ([]byte, *wire.TxOut, error) { // As a sanity check, ensure that the passed amount is above zero. if amt <= 0 { return nil, nil, fmt.Errorf("can't create FundTx script with " + @@ -85,7 +85,7 @@ func genFundingPkScript(aPub, bPub []byte, amt int64) ([]byte, *wire.TxOut, erro // spendMultiSig generates the witness stack required to redeem the 2-of-2 p2wsh // multi-sig output. -func spendMultiSig(redeemScript, pubA, sigA, pubB, sigB []byte) [][]byte { +func SpendMultiSig(redeemScript, pubA, sigA, pubB, sigB []byte) [][]byte { witness := make([][]byte, 4) // When spending a p2wsh multi-sig script, rather than an OP_0, we add @@ -114,7 +114,8 @@ func spendMultiSig(redeemScript, pubA, sigA, pubB, sigB []byte) [][]byte { // matching 'script'. Additionally, a boolean is returned indicating if // a matching output was found at all. // NOTE: The search stops after the first matching script is found. -func findScriptOutputIndex(tx *wire.MsgTx, script []byte) (bool, uint32) { +// TODO(roasbeef): shouldn't be public? +func FindScriptOutputIndex(tx *wire.MsgTx, script []byte) (bool, uint32) { found := false index := uint32(0) for i, txOut := range tx.TxOut { @@ -670,7 +671,7 @@ func commitSpendNoDelay(commitScript []byte, outputAmt btcutil.Amount, return wire.TxWitness(witness), nil } -// deriveRevocationPubkey derives the revocation public key given the +// DeriveRevocationPubkey derives the revocation public key given the // counter-party's commitment key, and revocation pre-image derived via a // pseudo-random-function. In the event that we (for some reason) broadcast a // revoked commitment transaction, then if the other party knows the revocation @@ -689,7 +690,7 @@ func commitSpendNoDelay(commitScript []byte, outputAmt btcutil.Amount, // revokePriv := commitPriv + revokePreimge mod N // // Where N is the order of the sub-group. -func deriveRevocationPubkey(commitPubKey *btcec.PublicKey, +func DeriveRevocationPubkey(commitPubKey *btcec.PublicKey, revokePreimage []byte) *btcec.PublicKey { // First we need to convert the revocation hash into a point on the @@ -703,7 +704,7 @@ func deriveRevocationPubkey(commitPubKey *btcec.PublicKey, return &btcec.PublicKey{X: revokeX, Y: revokeY} } -// deriveRevocationPrivKey derives the revocation private key given a node's +// DeriveRevocationPrivKey derives the revocation private key given a node's // commitment private key, and the pre-image to a previously seen revocation // hash. Using this derived private key, a node is able to claim the output // within the commitment transaction of a node in the case that they broadcast @@ -713,7 +714,7 @@ func deriveRevocationPubkey(commitPubKey *btcec.PublicKey, // revokePriv := commitPriv + revokePreimage mod N // // Where N is the order of the sub-group. -func deriveRevocationPrivKey(commitPrivKey *btcec.PrivateKey, +func DeriveRevocationPrivKey(commitPrivKey *btcec.PrivateKey, revokePreimage []byte) *btcec.PrivateKey { // Convert the revocation pre-image into a scalar value so we can diff --git a/lnwallet/script_utils_test.go b/lnwallet/script_utils_test.go index c750e9a9..1952d174 100644 --- a/lnwallet/script_utils_test.go +++ b/lnwallet/script_utils_test.go @@ -41,7 +41,7 @@ func TestCommitmentSpendValidation(t *testing.T) { channelBalance := btcutil.Amount(1 * 10e8) csvTimeout := uint32(5) revocationPreimage := testHdSeed[:] - revokePubKey := deriveRevocationPubkey(bobKeyPub, revocationPreimage) + revokePubKey := DeriveRevocationPubkey(bobKeyPub, revocationPreimage) // With all the test data set up, we create the commitment transaction. // We only focus on a single party's transactions, as the scripts are @@ -50,7 +50,7 @@ func TestCommitmentSpendValidation(t *testing.T) { // This is Alice's commitment transaction, so she must wait a CSV delay // of 5 blocks before sweeping the output, while bob can spend // immediately with either the revocation key, or his regular key. - commitmentTx, err := createCommitTx(fakeFundingTxIn, aliceKeyPub, + commitmentTx, err := CreateCommitTx(fakeFundingTxIn, aliceKeyPub, bobKeyPub, revokePubKey, csvTimeout, channelBalance, channelBalance) if err != nil { t.Fatalf("unable to create commitment transaction: %v", nil) @@ -96,7 +96,7 @@ func TestCommitmentSpendValidation(t *testing.T) { // Next, we'll test bob spending with the derived revocation key to // simulate the scenario when alice broadcasts this commitmen // transaction after it's been revoked. - revokePrivKey := deriveRevocationPrivKey(bobKeyPriv, revocationPreimage) + revokePrivKey := DeriveRevocationPrivKey(bobKeyPriv, revocationPreimage) bobWitnessSpend, err := commitSpendRevoke(delayScript, channelBalance, revokePrivKey, sweepTx) if err != nil { @@ -144,9 +144,9 @@ func TestRevocationKeyDerivation(t *testing.T) { priv, pub := btcec.PrivKeyFromBytes(btcec.S256(), testWalletPrivKey) - revocationPub := deriveRevocationPubkey(pub, revocationPreimage) + revocationPub := DeriveRevocationPubkey(pub, revocationPreimage) - revocationPriv := deriveRevocationPrivKey(priv, revocationPreimage) + revocationPriv := DeriveRevocationPrivKey(priv, revocationPreimage) x, y := btcec.S256().ScalarBaseMult(revocationPriv.D.Bytes()) derivedRevPub := &btcec.PublicKey{ Curve: btcec.S256(), From 99fdb3a3a91626e472065638d156378f287575de Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:43:16 -0700 Subject: [PATCH 10/15] lnwallet: modify elkrem root derivation, derive from root HD seed This commit modifies the elkrem root derivation for each newly created channel. First a master elkrem root is derived from the rood HD seed generated from private wallet data. Next, a HKDF is used with the secret being the master elkrem root. --- lnwallet/script_utils.go | 9 ++++---- lnwallet/wallet.go | 47 ++++++++++++++++++++++++++++++---------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/lnwallet/script_utils.go b/lnwallet/script_utils.go index 2094239e..01dff4e6 100644 --- a/lnwallet/script_utils.go +++ b/lnwallet/script_utils.go @@ -743,12 +743,13 @@ func DeriveRevocationPrivKey(commitPrivKey *btcec.PrivateKey, // // [1]: https://eprint.iacr.org/2010/264.pdf // [2]: https://tools.ietf.org/html/rfc5869 -func deriveElkremRoot(localMultiSigKey *btcec.PrivateKey, +func deriveElkremRoot(elkremDerivationRoot *btcec.PrivateKey, + localMultiSigKey *btcec.PublicKey, remoteMultiSigKey *btcec.PublicKey) wire.ShaHash { - secret := localMultiSigKey.Serialize() - salt := remoteMultiSigKey.SerializeCompressed() - info := []byte("elkrem") + secret := elkremDerivationRoot.Serialize() + salt := localMultiSigKey.SerializeCompressed() + info := remoteMultiSigKey.SerializeCompressed() rootReader := hkdf.New(sha256.New, secret, salt, info) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 6afd07d0..263e34e6 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -29,6 +29,10 @@ const ( // The size of the buffered queue of requests to the wallet from the // outside word. msgBufferSize = 100 + + // elkremRootIndex is the top level HD key index from which secrets + // used to generate elkrem roots should be derived from. + elkremRootIndex = hdkeychain.HardenedKeyStart + 1 ) var ( @@ -246,6 +250,9 @@ type LightningWallet struct { // * getrawtransaction -> verify proof of channel links // * gettxout -> verify inputs to funding tx exist and are unspent rpc *chain.RPCClient + // rootKey is the root HD key dervied from a WalletController private + // key. This rootKey is used to derive all LN specific secrets. + rootKey *hdkeychain.ExtendedKey // All messages to the wallet are to be sent accross this channel. msgChan chan interface{} @@ -294,6 +301,9 @@ func NewLightningWallet(config *Config, cdb *channeldb.DB, loader := btcwallet.NewLoader(config.NetParams, netDir) walletExists, err := loader.WalletExists() + // Fetch the root derivation key from the wallet's HD chain. We'll use + // this to generate specific Lightning related secrets on the fly. + rootKey, err := wallet.FetchRootKey() if err != nil { return nil, err } @@ -359,6 +369,7 @@ func NewLightningWallet(config *Config, cdb *channeldb.DB, cfg: config, fundingLimbo: make(map[uint64]*ChannelReservation), quit: make(chan struct{}), + rootKey: rootMasterKey, }, nil } @@ -769,11 +780,17 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { pendingReservation.partialState.RemoteElkrem = e pendingReservation.partialState.TheirCurrentRevocation = theirContribution.RevocationKey + masterElkremRoot, err := l.deriveMasterElkremRoot() + if err != nil { + req.err <- err + return + } + // Now that we have their commitment key, we can create the revocation // key for the first version of our commitment transaction. To do so, // we'll first create our elkrem root, then grab the first pre-iamge // from it. - elkremRoot := deriveElkremRoot(ourKey, theirKey) + elkremRoot := deriveElkremRoot(masterElkremRoot, ourKey, theirKey) elkremSender := elkrem.NewElkremSender(elkremRoot) pendingReservation.partialState.LocalElkrem = elkremSender firstPreimage, err := elkremSender.AtIndex(0) @@ -881,9 +898,15 @@ func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg } pendingReservation.partialState.FundingRedeemScript = redeemScript + masterElkremRoot, err := l.deriveMasterElkremRoot() + if err != nil { + req.err <- err + return + } + // Now that we know their commitment key, we can create the revocation // key for our version of the initial commitment transaction. - elkremRoot := deriveElkremRoot(ourKey, theirKey) + elkremRoot := deriveElkremRoot(masterElkremRoot, ourKey, theirKey) elkremSender := elkrem.NewElkremSender(elkremRoot) firstPreimage, err := elkremSender.AtIndex(0) if err != nil { @@ -1394,16 +1417,18 @@ func (l *LightningWallet) selectCoinsAndChange(feeRate uint64, amt btcutil.Amoun return nil } -type WaddrmgrEncryptorDecryptor struct { - M *waddrmgr.Manager +// deriveMasterElkremRoot derives the private key which serves as the master +// elkrem root. This master secret is used as the secret input to a HKDF to +// generate elkrem secrets based on random, but public data. +func (l *LightningWallet) deriveMasterElkremRoot() (*btcec.PrivateKey, error) { + masterElkremRoot, err := l.rootKey.Child(elkremRootIndex) + if err != nil { + return nil, err + } + + return masterElkremRoot.ECPrivKey() } -func (w *WaddrmgrEncryptorDecryptor) Encrypt(p []byte) ([]byte, error) { - return w.M.Encrypt(waddrmgr.CKTPrivate, p) -} - -func (w *WaddrmgrEncryptorDecryptor) Decrypt(c []byte) ([]byte, error) { - return w.M.Decrypt(waddrmgr.CKTPrivate, c) // selectInputs selects a slice of inputs necessary to meet the specified // selection amount. If input selectino is unable to suceed to to insuffcient // funds, a non-nil error is returned. Additionally, the total amount of the @@ -1440,8 +1465,6 @@ func selectInputs(amt btcutil.Amount, coins []*Utxo) (btcutil.Amount, []*wire.Ou return satSelected, selectedUtxos, nil } -func (w *WaddrmgrEncryptorDecryptor) OverheadSize() uint32 { - return 24 // coinSelect attemps to select a sufficient amount of coins, including a // change output to fund amt satoshis, adhearing to the specified fee rate. The // specified fee rate should be expressed in sat/byte for coin selection to From 671098325dc11f6ea406994b01dd5673758338de Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:50:47 -0700 Subject: [PATCH 11/15] lnwallet: refactor all wallet/channel interaction to use the WalletController. This commit performs a major refactor of the current wallet, reservation, and channel code in order to call into a WalletController implementation rather than directly into btcwallet. The current set of wallets tests have been modified in order to test against *all* registered WalletController implementations rather than only btcwallet. As a result, all future WalletControllers primary need to ensure that their implementation passes the current set of tests (which will be expanded into the future), providing an easy path of integration assurance. Rather than directly holding the private keys throughout funding and channel creation, the burden of securing keys has been shifted to the specified WalletController and Signer interfaces. All signing is done via the Signer interface rather than directly, increasing flexibility dramatically. During channel funding, rather than creating a txscript.Engine to verify commitment signatures, regular ECDSA sig verification is now used instead. This is faster and more efficient. Finally certain fields/methods within ChannelReservation and LightningChannel have been exposed publicly in order to restrict the amount of modifications the prior tests needed to undergo in order to support testing directly agains the WalletController interface. --- lnwallet/channel.go | 103 ++-- lnwallet/channel_test.go | 101 +++- .../{wallet_test.go => interface_test.go} | 343 ++++++----- lnwallet/reservation.go | 28 +- lnwallet/wallet.go | 538 +++++++----------- 5 files changed, 538 insertions(+), 575 deletions(-) rename lnwallet/{wallet_test.go => interface_test.go} (77%) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 982c5029..e6900f85 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -274,6 +274,9 @@ type LightningChannel struct { // wallet can add back. lnwallet *LightningWallet + signer Signer + signDesc *SignDescriptor + channelEvents chainntnfs.ChainNotifier sync.RWMutex @@ -281,7 +284,8 @@ type LightningChannel struct { ourLogCounter uint32 theirLogCounter uint32 - status channelState + status channelState + Capacity btcutil.Amount // currentHeight is the current height of our local commitment chain. // This is also the same as the number of updates to the channel we've @@ -337,10 +341,12 @@ type LightningChannel struct { ourLogIndex map[uint32]*list.Element theirLogIndex map[uint32]*list.Element - fundingTxIn *wire.TxIn - fundingP2WSH []byte + LocalDeliveryScript []byte + RemoteDeliveryScript []byte - channelDB *channeldb.DB + FundingRedeemScript []byte + fundingTxIn *wire.TxIn + fundingP2WSH []byte started int32 shutdown int32 @@ -354,11 +360,13 @@ type LightningChannel struct { // and the current settled channel state. Throughout state transitions, then // channel will automatically persist pertinent state to the database in an // efficient manner. -func NewLightningChannel(wallet *LightningWallet, events chainntnfs.ChainNotifier, - chanDB *channeldb.DB, state *channeldb.OpenChannel) (*LightningChannel, error) { +func NewLightningChannel(signer Signer, wallet *LightningWallet, + events chainntnfs.ChainNotifier, + state *channeldb.OpenChannel) (*LightningChannel, error) { // TODO(roasbeef): remove events+wallet lc := &LightningChannel{ + signer: signer, lnwallet: wallet, channelEvents: events, currentHeight: state.NumUpdates, @@ -370,7 +378,10 @@ func NewLightningChannel(wallet *LightningWallet, events chainntnfs.ChainNotifie theirUpdateLog: list.New(), ourLogIndex: make(map[uint32]*list.Element), theirLogIndex: make(map[uint32]*list.Element), - channelDB: chanDB, + Capacity: state.Capacity, + LocalDeliveryScript: state.OurDeliveryScript, + RemoteDeliveryScript: state.TheirDeliveryScript, + FundingRedeemScript: state.FundingRedeemScript, } // Initialize both of our chains the current un-revoked commitment for @@ -395,6 +406,17 @@ func NewLightningChannel(wallet *LightningWallet, events chainntnfs.ChainNotifie lc.fundingTxIn = wire.NewTxIn(state.FundingOutpoint, nil, nil) lc.fundingP2WSH = fundingPkScript + lc.signDesc = &SignDescriptor{ + PubKey: lc.channelState.OurMultiSigKey, + RedeemScript: lc.channelState.FundingRedeemScript, + Output: &wire.TxOut{ + PkScript: lc.fundingP2WSH, + Value: int64(lc.channelState.Capacity), + }, + HashType: txscript.SigHashAll, + InputIndex: 0, + } + return lc, nil } @@ -480,12 +502,12 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, var delayBalance, p2wkhBalance btcutil.Amount if remoteChain { selfKey = lc.channelState.TheirCommitKey - remoteKey = lc.channelState.OurCommitKey.PubKey() + remoteKey = lc.channelState.OurCommitKey delay = lc.channelState.RemoteCsvDelay delayBalance = theirBalance p2wkhBalance = ourBalance } else { - selfKey = lc.channelState.OurCommitKey.PubKey() + selfKey = lc.channelState.OurCommitKey remoteKey = lc.channelState.TheirCommitKey delay = lc.channelState.LocalCsvDelay delayBalance = ourBalance @@ -495,7 +517,7 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, // Generate a new commitment transaction with all the latest // unsettled/un-timed out HTLC's. ourCommitTx := !remoteChain - commitTx, err := createCommitTx(lc.fundingTxIn, selfKey, remoteKey, + commitTx, err := CreateCommitTx(lc.fundingTxIn, selfKey, remoteKey, revocationKey, delay, delayBalance, p2wkhBalance) if err != nil { return nil, err @@ -722,11 +744,8 @@ func (lc *LightningChannel) SignNextCommitment() ([]byte, uint32, error) { })) // Sign their version of the new commitment transaction. - hashCache := txscript.NewTxSigHashes(newCommitView.txn) - sig, err := txscript.RawTxInWitnessSignature(newCommitView.txn, - hashCache, 0, int64(lc.channelState.Capacity), - lc.channelState.FundingRedeemScript, txscript.SigHashAll, - lc.channelState.OurMultiSigKey) + lc.signDesc.SigHashes = txscript.NewTxSigHashes(newCommitView.txn) + sig, err := lc.signer.SignOutputRaw(newCommitView.txn, lc.signDesc) if err != nil { return nil, 0, err } @@ -744,7 +763,7 @@ func (lc *LightningChannel) SignNextCommitment() ([]byte, uint32, error) { // Strip off the sighash flag on the signature in order to send it over // the wire. - return sig[:len(sig)], lc.theirLogCounter, nil + return sig, lc.theirLogCounter, nil } // ReceiveNewCommitment processs a signature for a new commitment state sent by @@ -770,7 +789,7 @@ func (lc *LightningChannel) ReceiveNewCommitment(rawSig []byte, if err != nil { return err } - revocationKey := deriveRevocationPubkey(theirCommitKey, revocation[:]) + revocationKey := DeriveRevocationPubkey(theirCommitKey, revocation[:]) revocationHash := fastsha256.Sum256(revocation[:]) // With the revocation information calculated, construct the new @@ -857,7 +876,7 @@ func (lc *LightningChannel) RevokeCurrentCommitment() (*lnwire.CommitRevocation, if err != nil { return nil, err } - revocationMsg.NextRevocationKey = deriveRevocationPubkey(theirCommitKey, + revocationMsg.NextRevocationKey = DeriveRevocationPubkey(theirCommitKey, revocationEdge[:]) revocationMsg.NextRevocationHash = fastsha256.Sum256(revocationEdge[:]) @@ -921,8 +940,8 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.CommitRevocation) ( // Verify that the revocation public key we can derive using this // pre-image and our private key is identical to the revocation key we // were given for their current (prior) commitment transaction. - revocationPriv := deriveRevocationPrivKey(ourCommitKey, pendingRevocation[:]) - if !revocationPriv.PubKey().IsEqual(currentRevocationKey) { + revocationPub := DeriveRevocationPubkey(ourCommitKey, pendingRevocation[:]) + if !revocationPub.IsEqual(currentRevocationKey) { return nil, fmt.Errorf("revocation key mismatch") } @@ -1067,7 +1086,7 @@ func (lc *LightningChannel) ExtendRevocationWindow() (*lnwire.CommitRevocation, } theirCommitKey := lc.channelState.TheirCommitKey - revMsg.NextRevocationKey = deriveRevocationPubkey(theirCommitKey, + revMsg.NextRevocationKey = DeriveRevocationPubkey(theirCommitKey, revocation[:]) revMsg.NextRevocationHash = fastsha256.Sum256(revocation[:]) @@ -1201,7 +1220,7 @@ func (lc *LightningChannel) addHTLC(commitTx *wire.MsgTx, ourCommit bool, paymentDesc *PaymentDescriptor, revocation [32]byte, delay uint32, isIncoming bool) error { - localKey := lc.channelState.OurCommitKey.PubKey() + localKey := lc.channelState.OurCommitKey remoteKey := lc.channelState.TheirCommitKey timeout := paymentDesc.Timeout rHash := paymentDesc.RHash @@ -1288,7 +1307,7 @@ func (lc *LightningChannel) InitCooperativeClose() ([]byte, *wire.ShaHash, error lc.status = channelClosing // TODO(roasbeef): assumes initiator pays fees - closeTx := createCooperativeCloseTx(lc.fundingTxIn, + closeTx := CreateCooperativeCloseTx(lc.fundingTxIn, lc.channelState.OurBalance, lc.channelState.TheirBalance, lc.channelState.OurDeliveryScript, lc.channelState.TheirDeliveryScript, true) @@ -1298,11 +1317,8 @@ func (lc *LightningChannel) InitCooperativeClose() ([]byte, *wire.ShaHash, error // initiator we'll simply send our signature over the the remote party, // using the generated txid to be notified once the closure transaction // has been confirmed. - hashCache := txscript.NewTxSigHashes(closeTx) - closeSig, err := txscript.RawTxInWitnessSignature(closeTx, - hashCache, 0, int64(lc.channelState.Capacity), - lc.channelState.FundingRedeemScript, txscript.SigHashAll, - lc.channelState.OurMultiSigKey) + lc.signDesc.SigHashes = txscript.NewTxSigHashes(closeTx) + closeSig, err := lc.signer.SignOutputRaw(closeTx, lc.signDesc) if err != nil { return nil, nil, err } @@ -1315,6 +1331,9 @@ func (lc *LightningChannel) InitCooperativeClose() ([]byte, *wire.ShaHash, error // remote node initating a cooperative channel closure. A fully signed closure // transaction is returned. It is the duty of the responding node to broadcast // a signed+valid closure transaction to the network. +// +// NOTE: The passed remote sig is expected to the a fully complete signature +// including the proper sighash byte. func (lc *LightningChannel) CompleteCooperativeClose(remoteSig []byte) (*wire.MsgTx, error) { lc.Lock() defer lc.Unlock() @@ -1330,35 +1349,34 @@ func (lc *LightningChannel) CompleteCooperativeClose(remoteSig []byte) (*wire.Ms // Create the transaction used to return the current settled balance // on this active channel back to both parties. In this current model, // the initiator pays full fees for the cooperative close transaction. - closeTx := createCooperativeCloseTx(lc.fundingTxIn, + closeTx := CreateCooperativeCloseTx(lc.fundingTxIn, lc.channelState.OurBalance, lc.channelState.TheirBalance, lc.channelState.OurDeliveryScript, lc.channelState.TheirDeliveryScript, false) // With the transaction created, we can finally generate our half of // the 2-of-2 multi-sig needed to redeem the funding output. - redeemScript := lc.channelState.FundingRedeemScript hashCache := txscript.NewTxSigHashes(closeTx) - capacity := int64(lc.channelState.Capacity) - closeSig, err := txscript.RawTxInWitnessSignature(closeTx, - hashCache, 0, capacity, redeemScript, txscript.SigHashAll, - lc.channelState.OurMultiSigKey) + lc.signDesc.SigHashes = hashCache + closeSig, err := lc.signer.SignOutputRaw(closeTx, lc.signDesc) if err != nil { return nil, err } // Finally, construct the witness stack minding the order of the // pubkeys+sigs on the stack. - ourKey := lc.channelState.OurMultiSigKey.PubKey().SerializeCompressed() + ourKey := lc.channelState.OurMultiSigKey.SerializeCompressed() theirKey := lc.channelState.TheirMultiSigKey.SerializeCompressed() - witness := spendMultiSig(redeemScript, ourKey, closeSig, + ourSig := append(closeSig, byte(txscript.SigHashAll)) + witness := SpendMultiSig(lc.signDesc.RedeemScript, ourKey, ourSig, theirKey, remoteSig) closeTx.TxIn[0].Witness = witness // Validate the finalized transaction to ensure the output script is // properly met, and that the remote peer supplied a valid signature. vm, err := txscript.NewEngine(lc.fundingP2WSH, closeTx, 0, - txscript.StandardVerifyFlags, nil, hashCache, capacity) + txscript.StandardVerifyFlags, nil, hashCache, + int64(lc.channelState.Capacity)) if err != nil { return nil, err } @@ -1376,7 +1394,8 @@ func (lc *LightningChannel) DeleteState() error { return lc.channelState.CloseChannel() } -// StateSnapshot returns a snapshot b +// StateSnapshot returns a snapshot of the current fully committed state within +// the channel. func (lc *LightningChannel) StateSnapshot() *channeldb.ChannelSnapshot { lc.stateMtx.RLock() defer lc.stateMtx.RUnlock() @@ -1384,12 +1403,12 @@ func (lc *LightningChannel) StateSnapshot() *channeldb.ChannelSnapshot { return lc.channelState.Snapshot() } -// createCommitTx creates a commitment transaction, spending from specified +// CreateCommitTx creates a commitment transaction, spending from specified // funding output. The commitment transaction contains two outputs: one paying // to the "owner" of the commitment transaction which can be spent after a // relative block delay or revocation event, and the other paying the the // counter-party within the channel, which can be spent immediately. -func createCommitTx(fundingOutput *wire.TxIn, selfKey, theirKey *btcec.PublicKey, +func CreateCommitTx(fundingOutput *wire.TxIn, selfKey, theirKey *btcec.PublicKey, revokeKey *btcec.PublicKey, csvTimeout uint32, amountToSelf, amountToThem btcutil.Amount) (*wire.MsgTx, error) { @@ -1433,13 +1452,13 @@ func createCommitTx(fundingOutput *wire.TxIn, selfKey, theirKey *btcec.PublicKey return commitTx, nil } -// createCooperativeCloseTx creates a transaction which if signed by both +// CreateCooperativeCloseTx creates a transaction which if signed by both // parties, then broadcast cooperatively closes an active channel. The creation // of the closure transaction is modified by a boolean indicating if the party // constructing the channel is the initiator of the closure. Currently it is // expected that the initiator pays the transaction fees for the closing // transaction in full. -func createCooperativeCloseTx(fundingTxIn *wire.TxIn, +func CreateCooperativeCloseTx(fundingTxIn *wire.TxIn, ourBalance, theirBalance btcutil.Amount, ourDeliveryScript, theirDeliveryScript []byte, initiator bool) *wire.MsgTx { diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 8a778b01..588ecbfd 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -13,27 +13,64 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcd/txscript" "github.com/roasbeef/btcd/wire" "github.com/roasbeef/btcutil" ) -// MockEncryptorDecryptor is a mock implementation of EncryptorDecryptor that -// simply returns the passed bytes without encrypting or decrypting. This is -// used for testing purposes to be able to create a channldb instance which -// doesn't use encryption. -type MockEncryptorDecryptor struct { +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. + testHdSeed = [32]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, + } + + // The number of confirmations required to consider any created channel + // open. + numReqConfs = uint16(1) +) + +type mockSigner struct { + key *btcec.PrivateKey } -func (m *MockEncryptorDecryptor) Encrypt(n []byte) ([]byte, error) { - return n, nil +func (m *mockSigner) SignOutputRaw(tx *wire.MsgTx, signDesc *SignDescriptor) ([]byte, error) { + amt := signDesc.Output.Value + redeemScript := signDesc.RedeemScript + privKey := m.key + + sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes, + signDesc.InputIndex, amt, redeemScript, txscript.SigHashAll, privKey) + if err != nil { + return nil, err + } + + return sig[:len(sig)-1], nil } -func (m *MockEncryptorDecryptor) Decrypt(n []byte) ([]byte, error) { - return n, nil -} - -func (m *MockEncryptorDecryptor) OverheadSize() uint32 { - return 0 +// ComputeInputScript... +func (m *mockSigner) ComputeInputScript(tx *wire.MsgTx, signDesc *SignDescriptor) (*InputScript, error) { + return nil, nil } // createTestChannels creates two test channels funded with 10 BTC, with 5 BTC @@ -49,7 +86,7 @@ func createTestChannels() (*LightningChannel, *LightningChannel, func(), error) csvTimeoutAlice := uint32(5) csvTimeoutBob := uint32(4) - redeemScript, _, err := genFundingPkScript(aliceKeyPub.SerializeCompressed(), + redeemScript, _, err := GenFundingPkScript(aliceKeyPub.SerializeCompressed(), bobKeyPub.SerializeCompressed(), int64(channelCapacity)) if err != nil { return nil, nil, nil, err @@ -61,26 +98,26 @@ func createTestChannels() (*LightningChannel, *LightningChannel, func(), error) } fundingTxIn := wire.NewTxIn(prevOut, nil, nil) - bobElkrem := elkrem.NewElkremSender(deriveElkremRoot(bobKeyPriv, aliceKeyPub)) + bobElkrem := elkrem.NewElkremSender(deriveElkremRoot(bobKeyPriv, bobKeyPub, aliceKeyPub)) bobFirstRevoke, err := bobElkrem.AtIndex(0) if err != nil { return nil, nil, nil, err } - bobRevokeKey := deriveRevocationPubkey(aliceKeyPub, bobFirstRevoke[:]) + bobRevokeKey := DeriveRevocationPubkey(aliceKeyPub, bobFirstRevoke[:]) - aliceElkrem := elkrem.NewElkremSender(deriveElkremRoot(aliceKeyPriv, bobKeyPub)) + aliceElkrem := elkrem.NewElkremSender(deriveElkremRoot(aliceKeyPriv, aliceKeyPub, bobKeyPub)) aliceFirstRevoke, err := aliceElkrem.AtIndex(0) if err != nil { return nil, nil, nil, err } - aliceRevokeKey := deriveRevocationPubkey(bobKeyPub, aliceFirstRevoke[:]) + aliceRevokeKey := DeriveRevocationPubkey(bobKeyPub, aliceFirstRevoke[:]) - aliceCommitTx, err := createCommitTx(fundingTxIn, aliceKeyPub, + aliceCommitTx, err := CreateCommitTx(fundingTxIn, aliceKeyPub, bobKeyPub, aliceRevokeKey, csvTimeoutAlice, channelBal, channelBal) if err != nil { return nil, nil, nil, err } - bobCommitTx, err := createCommitTx(fundingTxIn, bobKeyPub, + bobCommitTx, err := CreateCommitTx(fundingTxIn, bobKeyPub, aliceKeyPub, bobRevokeKey, csvTimeoutBob, channelBal, channelBal) if err != nil { return nil, nil, nil, err @@ -91,25 +128,24 @@ func createTestChannels() (*LightningChannel, *LightningChannel, func(), error) if err != nil { return nil, nil, nil, err } - dbAlice.RegisterCryptoSystem(&MockEncryptorDecryptor{}) + bobPath, err := ioutil.TempDir("", "bobdb") dbBob, err := channeldb.Open(bobPath, &chaincfg.TestNet3Params) if err != nil { return nil, nil, nil, err } - dbBob.RegisterCryptoSystem(&MockEncryptorDecryptor{}) aliceChannelState := &channeldb.OpenChannel{ TheirLNID: testHdSeed, ChanID: prevOut, - OurCommitKey: aliceKeyPriv, + OurCommitKey: aliceKeyPub, TheirCommitKey: bobKeyPub, Capacity: channelCapacity, OurBalance: channelBal, TheirBalance: channelBal, OurCommitTx: aliceCommitTx, FundingOutpoint: prevOut, - OurMultiSigKey: aliceKeyPriv, + OurMultiSigKey: aliceKeyPub, TheirMultiSigKey: bobKeyPub, FundingRedeemScript: redeemScript, LocalCsvDelay: csvTimeoutAlice, @@ -122,14 +158,14 @@ func createTestChannels() (*LightningChannel, *LightningChannel, func(), error) bobChannelState := &channeldb.OpenChannel{ TheirLNID: testHdSeed, ChanID: prevOut, - OurCommitKey: bobKeyPriv, + OurCommitKey: bobKeyPub, TheirCommitKey: aliceKeyPub, Capacity: channelCapacity, OurBalance: channelBal, TheirBalance: channelBal, OurCommitTx: bobCommitTx, FundingOutpoint: prevOut, - OurMultiSigKey: bobKeyPriv, + OurMultiSigKey: bobKeyPub, TheirMultiSigKey: aliceKeyPub, FundingRedeemScript: redeemScript, LocalCsvDelay: csvTimeoutBob, @@ -145,11 +181,14 @@ func createTestChannels() (*LightningChannel, *LightningChannel, func(), error) os.RemoveAll(alicePath) } - channelAlice, err := NewLightningChannel(nil, nil, dbAlice, aliceChannelState) + aliceSigner := &mockSigner{aliceKeyPriv} + bobSigner := &mockSigner{bobKeyPriv} + + channelAlice, err := NewLightningChannel(aliceSigner, nil, nil, aliceChannelState) if err != nil { return nil, nil, nil, err } - channelBob, err := NewLightningChannel(nil, nil, dbBob, bobChannelState) + channelBob, err := NewLightningChannel(bobSigner, nil, nil, bobChannelState) if err != nil { return nil, nil, nil, err } @@ -456,7 +495,8 @@ func TestCooperativeChannelClosure(t *testing.T) { if err != nil { t.Fatalf("unable to initiate alice cooperative close: %v", err) } - closeTx, err := bobChannel.CompleteCooperativeClose(sig) + finalSig := append(sig, byte(txscript.SigHashAll)) + closeTx, err := bobChannel.CompleteCooperativeClose(finalSig) if err != nil { t.Fatalf("unable to complete alice cooperative close: %v", err) } @@ -475,7 +515,8 @@ func TestCooperativeChannelClosure(t *testing.T) { if err != nil { t.Fatalf("unable to initiate bob cooperative close: %v", err) } - closeTx, err = aliceChannel.CompleteCooperativeClose(sig) + finalSig = append(sig, byte(txscript.SigHashAll)) + closeTx, err = aliceChannel.CompleteCooperativeClose(finalSig) if err != nil { t.Fatalf("unable to complete bob cooperative close: %v", err) } diff --git a/lnwallet/wallet_test.go b/lnwallet/interface_test.go similarity index 77% rename from lnwallet/wallet_test.go rename to lnwallet/interface_test.go index 330401c9..87890801 100644 --- a/lnwallet/wallet_test.go +++ b/lnwallet/interface_test.go @@ -1,4 +1,4 @@ -package lnwallet +package lnwallet_test import ( "bytes" @@ -11,18 +11,20 @@ import ( "time" "github.com/boltdb/bolt" + "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/roasbeef/btcd/chaincfg" "github.com/roasbeef/btcutil/txsort" + _ "github.com/roasbeef/btcwallet/walletdb/bdb" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/rpctest" "github.com/roasbeef/btcd/txscript" "github.com/roasbeef/btcd/wire" "github.com/roasbeef/btcutil" - "github.com/roasbeef/btcutil/coinset" - "github.com/roasbeef/btcwallet/waddrmgr" ) var ( @@ -60,8 +62,8 @@ var ( // assertProperBalance asserts than the total value of the unspent outputs // within the wallet are *exactly* amount. If unable to retrieve the current // balance, or the assertion fails, the test will halt with a fatal error. -func assertProperBalance(t *testing.T, lw *LightningWallet, numConfirms int32, amount int64) { - balance, err := lw.CalculateBalance(numConfirms) +func assertProperBalance(t *testing.T, lw *lnwallet.LightningWallet, numConfirms int32, amount int64) { + balance, err := lw.ConfirmedBalance(numConfirms, false) if err != nil { t.Fatalf("unable to query for balance: %v", err) } @@ -72,7 +74,7 @@ func assertProperBalance(t *testing.T, lw *LightningWallet, numConfirms int32, a } func assertChannelOpen(t *testing.T, miner *rpctest.Harness, numConfs uint32, - c <-chan *LightningChannel) *LightningChannel { + c <-chan *lnwallet.LightningChannel) *lnwallet.LightningChannel { // Mine a single block. After this block is mined, the channel should // be considered fully open. if _, err := miner.Node.Generate(1); err != nil { @@ -108,9 +110,9 @@ type bobNode struct { // Contribution returns bobNode's contribution necessary to open a payment // channel with Alice. -func (b *bobNode) Contribution(aliceCommitKey *btcec.PublicKey) *ChannelContribution { - revokeKey := deriveRevocationPubkey(aliceCommitKey, b.revocation[:]) - return &ChannelContribution{ +func (b *bobNode) Contribution(aliceCommitKey *btcec.PublicKey) *lnwallet.ChannelContribution { + revokeKey := lnwallet.DeriveRevocationPubkey(aliceCommitKey, b.revocation[:]) + return &lnwallet.ChannelContribution{ FundingAmount: b.fundingAmt, Inputs: b.availableOutputs, ChangeOutputs: b.changeOutputs, @@ -124,9 +126,9 @@ func (b *bobNode) Contribution(aliceCommitKey *btcec.PublicKey) *ChannelContribu // SingleContribution returns bobNode's contribution to a single funded // channel. This contribution contains no inputs nor change outputs. -func (b *bobNode) SingleContribution(aliceCommitKey *btcec.PublicKey) *ChannelContribution { - revokeKey := deriveRevocationPubkey(aliceCommitKey, b.revocation[:]) - return &ChannelContribution{ +func (b *bobNode) SingleContribution(aliceCommitKey *btcec.PublicKey) *lnwallet.ChannelContribution { + revokeKey := lnwallet.DeriveRevocationPubkey(aliceCommitKey, b.revocation[:]) + return &lnwallet.ChannelContribution{ FundingAmount: b.fundingAmt, MultiSigKey: b.channelKey, CommitKey: b.channelKey, @@ -139,8 +141,8 @@ func (b *bobNode) SingleContribution(aliceCommitKey *btcec.PublicKey) *ChannelCo // signFundingTx generates signatures for all the inputs in the funding tx // belonging to Bob. // NOTE: This generates the full witness stack. -func (b *bobNode) signFundingTx(fundingTx *wire.MsgTx) ([]*InputScript, error) { - bobInputScripts := make([]*InputScript, 0, len(b.availableOutputs)) +func (b *bobNode) signFundingTx(fundingTx *wire.MsgTx) ([]*lnwallet.InputScript, error) { + bobInputScripts := make([]*lnwallet.InputScript, 0, len(b.availableOutputs)) bobPkScript := b.changeOutputs[0].PkScript inputValue := int64(7e8) @@ -158,7 +160,7 @@ func (b *bobNode) signFundingTx(fundingTx *wire.MsgTx) ([]*InputScript, error) { return nil, err } - inputScript := &InputScript{Witness: witness} + inputScript := &lnwallet.InputScript{Witness: witness} bobInputScripts = append(bobInputScripts, inputScript) } @@ -217,7 +219,7 @@ func newBobNode(miner *rpctest.Harness, amt btcutil.Amount) (*bobNode, error) { if err != nil { return nil, err } - found, index := findScriptOutputIndex(tx.MsgTx(), bobAddrScript) + found, index := lnwallet.FindScriptOutputIndex(tx.MsgTx(), bobAddrScript) if !found { return nil, fmt.Errorf("output to bob never created") } @@ -251,14 +253,14 @@ func newBobNode(miner *rpctest.Harness, amt btcutil.Amount) (*bobNode, error) { }, nil } -func loadTestCredits(miner *rpctest.Harness, w *LightningWallet, numOutputs, btcPerOutput int) error { +func loadTestCredits(miner *rpctest.Harness, w *lnwallet.LightningWallet, numOutputs, btcPerOutput int) error { // Using the mining node, spend from a coinbase output numOutputs to // give us btcPerOutput with each output. satoshiPerOutput := int64(btcPerOutput * 1e8) addrs := make([]btcutil.Address, 0, numOutputs) for i := 0; i < numOutputs; i++ { // Grab a fresh address from the wallet to house this output. - walletAddr, err := w.NewAddress(waddrmgr.DefaultAccountNum, waddrmgr.WitnessPubKey) + walletAddr, err := w.NewAddress(lnwallet.WitnessPubKey, false) if err != nil { return err } @@ -285,87 +287,59 @@ func loadTestCredits(miner *rpctest.Harness, w *LightningWallet, numOutputs, btc return err } - _, bestHeight, err := miner.Node.GetBestBlock() - if err != nil { - return err - } - // Wait until the wallet has finished syncing up to the main chain. ticker := time.NewTicker(100 * time.Millisecond) + expectedBalance := btcutil.Amount(satoshiPerOutput * int64(numOutputs)) out: for { select { case <-ticker.C: - if w.Manager.SyncedTo().Height == bestHeight { + balance, err := w.ConfirmedBalance(1, false) + if err != nil { + return err + } + if balance == expectedBalance { break out } } } ticker.Stop() - // Trigger a re-scan to ensure the wallet knows of the newly created - // outputs it can spend. - if err := w.Rescan(addrs, nil); err != nil { - return err - } - return nil } // createTestWallet creates a test LightningWallet will a total of 20BTC // available for funding channels. -func createTestWallet(miningNode *rpctest.Harness, netParams *chaincfg.Params) (string, *LightningWallet, error) { - privPass := []byte("private-test") - tempTestDir, err := ioutil.TempDir("", "lnwallet") - if err != nil { - return "", nil, nil - } - - rpcConfig := miningNode.RPCConfig() - config := &Config{ - PrivatePass: privPass, - HdSeed: testHdSeed[:], - DataDir: tempTestDir, - NetParams: netParams, - RpcHost: rpcConfig.Host, - RpcUser: rpcConfig.User, - RpcPass: rpcConfig.Pass, - CACert: rpcConfig.Certificates, - } +func createTestWallet(tempTestDir string, miningNode *rpctest.Harness, + netParams *chaincfg.Params, notifier chainntnfs.ChainNotifier, + wc lnwallet.WalletController, signer lnwallet.Signer, + bio lnwallet.BlockChainIO) (*lnwallet.LightningWallet, error) { dbDir := filepath.Join(tempTestDir, "cdb") cdb, err := channeldb.Open(dbDir, &chaincfg.SegNet4Params) if err != nil { - return "", nil, err + return nil, err } - chainNotifier, err := btcdnotify.New(&rpcConfig) + wallet, err := lnwallet.NewLightningWallet(cdb, notifier, wc, signer, + bio, netParams) if err != nil { - return "", nil, err - } - if err := chainNotifier.Start(); err != nil { - return "", nil, err + return nil, err } - wallet, err := NewLightningWallet(config, cdb, chainNotifier) - if err != nil { - return "", nil, err - } if err := wallet.Startup(); err != nil { - return "", nil, err + return nil, err } - cdb.RegisterCryptoSystem(&WaddrmgrEncryptorDecryptor{wallet.Manager}) - - // Load our test wallet with 10 outputs each holding 4BTC. - if err := loadTestCredits(miningNode, wallet, 10, 4); err != nil { - return "", nil, err + // Load our test wallet with 20 outputs each holding 4BTC. + if err := loadTestCredits(miningNode, wallet, 20, 4); err != nil { + return nil, err } - return tempTestDir, wallet, nil + return wallet, nil } -func testDualFundingReservationWorkflow(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testDualFundingReservationWorkflow(miner *rpctest.Harness, wallet *lnwallet.LightningWallet, t *testing.T) { // Create the bob-test wallet which will be the other side of our funding // channel. fundingAmount := btcutil.Amount(5 * 1e8) @@ -376,7 +350,7 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, lnwallet *Lightn // Bob initiates a channel funded with 5 BTC for each side, so 10 // BTC total. He also generates 2 BTC in change. - chanReservation, err := lnwallet.InitChannelReservation(fundingAmount*2, + chanReservation, err := wallet.InitChannelReservation(fundingAmount*2, fundingAmount, bobNode.id, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation: %v", err) @@ -426,10 +400,13 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, lnwallet *Lightn if ourContribution.RevocationKey == nil { t.Fatalf("alice's revocation key not found") } + // Additionally, the funding tx should have been populated. - if chanReservation.fundingTx == nil { + fundingTx := chanReservation.FinalFundingTx() + if fundingTx == nil { t.Fatalf("funding transaction never created!") } + // Their funds should also be filled in. if len(theirContribution.Inputs) != 1 { t.Fatalf("bob's outputs for funding tx not properly selected, have %v "+ @@ -455,13 +432,13 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, lnwallet *Lightn // Alice responds with her output, change addr, multi-sig key and signatures. // Bob then responds with his signatures. - bobsSigs, err := bobNode.signFundingTx(chanReservation.fundingTx) + bobsSigs, err := bobNode.signFundingTx(fundingTx) if err != nil { t.Fatalf("unable to sign inputs for bob: %v", err) } commitSig, err := bobNode.signCommitTx( - chanReservation.partialState.OurCommitTx, - chanReservation.partialState.FundingRedeemScript, + chanReservation.LocalCommitTx(), + chanReservation.FundingRedeemScript(), 10e8) if err != nil { t.Fatalf("bob is unable to sign alice's commit tx: %v", err) @@ -474,10 +451,9 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, lnwallet *Lightn // txn hits a "comfortable" depth. // The resulting active channel state should have been persisted to the DB. - fundingTx := chanReservation.FinalFundingTx() fundingSha := fundingTx.TxSha() nodeID := wire.ShaHash(bobNode.id) - channels, err := lnwallet.channelDB.FetchOpenChannels(&nodeID) + channels, err := wallet.ChannelDB.FetchOpenChannels(&nodeID) if err != nil { t.Fatalf("unable to retrieve channel from DB: %v", err) } @@ -495,46 +471,49 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, lnwallet *Lightn if err != nil { t.Fatalf("unable to init cooperative closure: %v", err) } + aliceCloseSig = append(aliceCloseSig, byte(txscript.SigHashAll)) + + chanInfo := lnc.StateSnapshot() // Obtain bob's signature for the closure transaction. - redeemScript := lnc.channelState.FundingRedeemScript + redeemScript := lnc.FundingRedeemScript fundingOut := lnc.ChannelPoint() fundingTxIn := wire.NewTxIn(fundingOut, nil, nil) - bobCloseTx := createCooperativeCloseTx(fundingTxIn, - lnc.channelState.TheirBalance, lnc.channelState.OurBalance, - lnc.channelState.TheirDeliveryScript, lnc.channelState.OurDeliveryScript, + bobCloseTx := lnwallet.CreateCooperativeCloseTx(fundingTxIn, + chanInfo.RemoteBalance, chanInfo.LocalBalance, + lnc.RemoteDeliveryScript, lnc.LocalDeliveryScript, false) - bobSig, err := bobNode.signCommitTx(bobCloseTx, - redeemScript, - int64(lnc.channelState.Capacity)) + bobSig, err := bobNode.signCommitTx(bobCloseTx, redeemScript, int64(lnc.Capacity)) if err != nil { t.Fatalf("unable to generate bob's signature for closing tx: %v", err) } // Broadcast the transaction to the network. This transaction should // be accepted, and found in the next mined block. - ourKey := lnc.channelState.OurMultiSigKey.PubKey().SerializeCompressed() - theirKey := lnc.channelState.TheirMultiSigKey.SerializeCompressed() - witness := spendMultiSig(redeemScript, ourKey, aliceCloseSig, + ourKey := chanReservation.OurContribution().MultiSigKey.SerializeCompressed() + theirKey := chanReservation.TheirContribution().MultiSigKey.SerializeCompressed() + witness := lnwallet.SpendMultiSig(redeemScript, ourKey, aliceCloseSig, theirKey, bobSig) bobCloseTx.TxIn[0].Witness = witness - if err := lnwallet.PublishTransaction(bobCloseTx); err != nil { + if err := wallet.PublishTransaction(bobCloseTx); err != nil { t.Fatalf("broadcast of close tx rejected: %v", err) } } -func testFundingTransactionLockedOutputs(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testFundingTransactionLockedOutputs(miner *rpctest.Harness, + wallet *lnwallet.LightningWallet, t *testing.T) { + // 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, + chanReservation1, err := wallet.InitChannelReservation(fundingAmount, fundingAmount, testHdSeed, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation 1: %v", err) } - chanReservation2, err := lnwallet.InitChannelReservation(fundingAmount, + chanReservation2, err := wallet.InitChannelReservation(fundingAmount, fundingAmount, testHdSeed, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation 2: %v", err) @@ -563,12 +542,12 @@ func testFundingTransactionLockedOutputs(miner *rpctest.Harness, lnwallet *Light // 90 BTC. We only have around 24BTC worth of outpoints that aren't locked, so // this should fail. amt := btcutil.Amount(90 * 1e8) - failedReservation, err := lnwallet.InitChannelReservation(amt, amt, + failedReservation, err := wallet.InitChannelReservation(amt, amt, testHdSeed, numReqConfs, 4) if err == nil { t.Fatalf("not error returned, should fail on coin selection") } - if err != coinset.ErrCoinsNoSelectionAvailable { + if err != lnwallet.ErrInsufficientFunds { t.Fatalf("error not coinselect error: %v", err) } if failedReservation != nil { @@ -576,28 +555,30 @@ func testFundingTransactionLockedOutputs(miner *rpctest.Harness, lnwallet *Light } } -func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { - // Create a reservation for 22 BTC. - fundingAmount := btcutil.Amount(22 * 1e8) - chanReservation, err := lnwallet.InitChannelReservation(fundingAmount, +func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness, + wallet *lnwallet.LightningWallet, t *testing.T) { + + // Create a reservation for 44 BTC. + fundingAmount := btcutil.Amount(44 * 1e8) + chanReservation, err := wallet.InitChannelReservation(fundingAmount, fundingAmount, testHdSeed, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation: %v", err) } - // There should be three locked outpoints. - lockedOutPoints := lnwallet.LockedOutpoints() - if len(lockedOutPoints) != 6 { + // There should be 12 locked outpoints. + lockedOutPoints := wallet.LockedOutpoints() + if len(lockedOutPoints) != 12 { t.Fatalf("two outpoints should now be locked, instead %v are", len(lockedOutPoints)) } - // Attempt to create another channel with 22 BTC, this should fail. - failedReservation, err := lnwallet.InitChannelReservation(fundingAmount, + // Attempt to create another channel with 44 BTC, this should fail. + _, err = wallet.InitChannelReservation(fundingAmount, fundingAmount, testHdSeed, numReqConfs, 4) - if err != coinset.ErrCoinsNoSelectionAvailable { - t.Fatalf("coin selection succeded should have insufficient funds: %+v", - failedReservation) + if err != lnwallet.ErrInsufficientFunds { + t.Fatalf("coin selection succeded should have insufficient funds: %v", + err) } // Now cancel that old reservation. @@ -606,15 +587,16 @@ func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness, lnwallet *Lig } // Those outpoints should no longer be locked. - lockedOutPoints = lnwallet.LockedOutpoints() + lockedOutPoints = 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") + // Reservation ID should no longer be tracked. + numReservations := wallet.ActiveReservations() + if len(wallet.ActiveReservations()) != 0 { + t.Fatalf("should have 0 reservations, instead have %v", + numReservations) } // TODO(roasbeef): create method like Balance that ignores locked @@ -622,16 +604,18 @@ func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness, lnwallet *Lig // attempting coin selection. // Request to fund a new channel should now succeeed. - _, err = lnwallet.InitChannelReservation(fundingAmount, fundingAmount, + _, err = wallet.InitChannelReservation(fundingAmount, fundingAmount, testHdSeed, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation: %v", err) } } -func testCancelNonExistantReservation(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testCancelNonExistantReservation(miner *rpctest.Harness, + wallet *lnwallet.LightningWallet, t *testing.T) { + // Create our own reservation, give it some ID. - res := newChannelReservation(1000, 1000, 5000, lnwallet, 22, numReqConfs) + res := lnwallet.NewChannelReservation(1000, 1000, 5000, wallet, 22, numReqConfs) // Attempt to cancel this reservation. This should fail, we know // nothing of it. @@ -640,7 +624,9 @@ func testCancelNonExistantReservation(miner *rpctest.Harness, lnwallet *Lightnin } } -func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, + lnwallet *lnwallet.LightningWallet, t *testing.T) { + // For this scenario, we (lnwallet) will be the channel initiator while bob // will be the recipient. @@ -702,7 +688,7 @@ func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, lnwall t.Fatalf("commitment sig not found") } // Additionally, the funding tx should have been populated. - if chanReservation.fundingTx == nil { + if chanReservation.FinalFundingTx() == nil { t.Fatalf("funding transaction never created!") } // Their funds should also be filled in. @@ -737,8 +723,8 @@ func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, lnwall // Now Bob can generate a signature for our version of the commitment // transaction, allowing us to complete the reservation. bobCommitSig, err := bobNode.signCommitTx( - chanReservation.partialState.OurCommitTx, - chanReservation.partialState.FundingRedeemScript, + chanReservation.LocalCommitTx(), + chanReservation.FundingRedeemScript(), int64(fundingAmt)) if err != nil { t.Fatalf("bob is unable to sign alice's commit tx: %v", err) @@ -755,7 +741,7 @@ func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, lnwall fundingTx := chanReservation.FinalFundingTx() fundingSha := fundingTx.TxSha() nodeID := wire.ShaHash(bobNode.id) - channels, err := lnwallet.channelDB.FetchOpenChannels(&nodeID) + channels, err := lnwallet.ChannelDB.FetchOpenChannels(&nodeID) if err != nil { t.Fatalf("unable to retrieve channel from DB: %v", err) } @@ -768,7 +754,9 @@ func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, lnwall assertChannelOpen(t, miner, uint32(numReqConfs), chanReservation.DispatchChan()) } -func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, + wallet *lnwallet.LightningWallet, t *testing.T) { + // For this scenario, bob will initiate the channel, while we simply act as // the responder. capacity := btcutil.Amount(4 * 1e8) @@ -783,7 +771,7 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwall // Bob sends over a single funding request, so we allocate our // contribution and the necessary resources. fundingAmt := btcutil.Amount(0) - chanReservation, err := lnwallet.InitChannelReservation(capacity, + chanReservation, err := wallet.InitChannelReservation(capacity, fundingAmt, bobNode.id, numReqConfs, 4) if err != nil { t.Fatalf("unable to init channel reservation: %v", err) @@ -821,7 +809,7 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwall if err := chanReservation.ProcessSingleContribution(bobContribution); err != nil { t.Fatalf("unable to process bob's contribution: %v", err) } - if chanReservation.fundingTx != nil { + if chanReservation.FinalFundingTx() != nil { t.Fatalf("funding transaction populated!") } if len(bobContribution.Inputs) != 1 { @@ -848,7 +836,7 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwall t.Fatalf("bob's revocaiton key not found") } - fundingRedeemScript, multiOut, err := genFundingPkScript( + fundingRedeemScript, multiOut, err := lnwallet.GenFundingPkScript( ourContribution.MultiSigKey.SerializeCompressed(), bobContribution.MultiSigKey.SerializeCompressed(), int64(capacity)) @@ -872,11 +860,11 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwall // wallet so it can finalize the transaction by signing bob's commitment // transaction. fundingTxID := fundingTx.TxSha() - _, multiSigIndex := findScriptOutputIndex(fundingTx, multiOut.PkScript) + _, multiSigIndex := lnwallet.FindScriptOutputIndex(fundingTx, multiOut.PkScript) fundingOutpoint := wire.NewOutPoint(&fundingTxID, multiSigIndex) fundingTxIn := wire.NewTxIn(fundingOutpoint, nil, nil) - aliceCommitTx, err := createCommitTx(fundingTxIn, ourContribution.CommitKey, + aliceCommitTx, err := lnwallet.CreateCommitTx(fundingTxIn, ourContribution.CommitKey, bobContribution.CommitKey, ourContribution.RevocationKey, ourContribution.CsvDelay, 0, capacity) if err != nil { @@ -897,10 +885,9 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwall } // Alice should have saved the funding output. - if chanReservation.partialState.FundingOutpoint != fundingOutpoint { + if chanReservation.FundingOutpoint() != fundingOutpoint { t.Fatalf("funding outputs don't match: %#v vs %#v", - chanReservation.partialState.FundingOutpoint, - fundingOutpoint) + chanReservation.FundingOutpoint(), fundingOutpoint) } // Some period of time later, Bob presents us with an SPV proof @@ -913,13 +900,13 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwall // TODO(roasbeef): bob verify alice's sig } -func testFundingReservationInvalidCounterpartySigs(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testFundingReservationInvalidCounterpartySigs(miner *rpctest.Harness, lnwallet *lnwallet.LightningWallet, t *testing.T) { } -func testFundingTransactionTxFees(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testFundingTransactionTxFees(miner *rpctest.Harness, lnwallet *lnwallet.LightningWallet, t *testing.T) { } -var walletTests = []func(miner *rpctest.Harness, w *LightningWallet, test *testing.T){ +var walletTests = []func(miner *rpctest.Harness, w *lnwallet.LightningWallet, test *testing.T){ testDualFundingReservationWorkflow, testSingleFunderReservationWorkflowInitiator, testSingleFunderReservationWorkflowResponder, @@ -934,20 +921,29 @@ var walletTests = []func(miner *rpctest.Harness, w *LightningWallet, test *testi } type testLnWallet struct { - lnwallet *LightningWallet + lnwallet *lnwallet.LightningWallet cleanUpFunc func() } -func clearWalletState(w *LightningWallet) error { - w.nextFundingID = 0 - w.fundingLimbo = make(map[uint64]*ChannelReservation) - w.ResetLockedOutpoints() - +func clearWalletState(w *lnwallet.LightningWallet) error { // TODO(roasbeef): should also restore outputs to original state. + w.ResetReservations() - return w.channelDB.Wipe() + return w.ChannelDB.Wipe() } +// TestInterfaces tests all registered interfaces with a unified set of tests +// which excersie each of the required methods found within the WalletController +// interface. +// +// NOTE: In the future, when additional implementations of the WalletController +// interface have been implemented, in order to ensure the new concrete +// implementation is automatically tested, two steps must be undertaken. First, +// one needs add a "non-captured" (_) import from the new sub-package. This +// import should trigger an init() method within the package which registeres +// the interface. Second, an additional case in the switch within the main loop +// below needs to be added which properly initializes the interface. +// // TODO(roasbeef): purge bobNode in favor of dual lnwallet's func TestLightningWallet(t *testing.T) { netParams := &chaincfg.SimNetParams @@ -965,27 +961,72 @@ func TestLightningWallet(t *testing.T) { t.Fatalf("unable to set up mining node: %v", err) } - // Funding via 10 outputs with 4BTC each. - testDir, lnwallet, err := createTestWallet(miningNode, netParams) + rpcConfig := miningNode.RPCConfig() + + chainNotifier, err := btcdnotify.New(&rpcConfig) if err != nil { - t.Fatalf("unable to create test ln wallet: %v", err) + t.Fatalf("unable to create notifier: %v", err) + } + if err := chainNotifier.Start(); err != nil { + t.Fatalf("unable to start notifier: %v", err) } - defer os.RemoveAll(testDir) - defer lnwallet.Shutdown() - // The wallet should now have 40BTC available for spending. - assertProperBalance(t, lnwallet, 1, 40) - - // Execute every test, clearing possibly mutated wallet state after - // each step. - for _, walletTest := range walletTests { - walletTest(miningNode, lnwallet, t) - - // TODO(roasbeef): possible reset mining node's chainstate to - // initial level, cleanly wipe buckets - if err := clearWalletState(lnwallet); err != nil && - err != bolt.ErrBucketNotFound { - t.Fatalf("unable to wipe wallet state: %v", err) + var bio lnwallet.BlockChainIO + var signer lnwallet.Signer + var wc lnwallet.WalletController + for _, walletDriver := range lnwallet.RegisteredWallets() { + tempTestDir, err := ioutil.TempDir("", "lnwallet") + if err != nil { + t.Fatalf("unable to create temp directory: %v", err) } + defer os.RemoveAll(tempTestDir) + + walletType := walletDriver.WalletType + switch walletType { + case "btcwallet": + btcwalletConfig := &btcwallet.Config{ + PrivatePass: privPass, + HdSeed: testHdSeed[:], + DataDir: tempTestDir, + NetParams: netParams, + RpcHost: rpcConfig.Host, + RpcUser: rpcConfig.User, + RpcPass: rpcConfig.Pass, + CACert: rpcConfig.Certificates, + } + wc, err = walletDriver.New(btcwalletConfig) + if err != nil { + t.Fatalf("unable to create btcwallet: %v", err) + } + signer = wc.(*btcwallet.BtcWallet) + bio = wc.(*btcwallet.BtcWallet) + default: + t.Fatalf("unknown wallet driver: %v", walletType) + } + + // Funding via 20 outputs with 4BTC each. + lnwallet, err := createTestWallet(tempTestDir, miningNode, netParams, + chainNotifier, wc, signer, bio) + if err != nil { + t.Fatalf("unable to create test ln wallet: %v", err) + } + + // The wallet should now have 80BTC available for spending. + assertProperBalance(t, lnwallet, 1, 80) + + // Execute every test, clearing possibly mutated wallet state after + // each step. + for _, walletTest := range walletTests { + walletTest(miningNode, lnwallet, t) + + // TODO(roasbeef): possible reset mining node's chainstate to + // initial level, cleanly wipe buckets + if err := clearWalletState(lnwallet); err != nil && + err != bolt.ErrBucketNotFound { + t.Fatalf("unable to wipe wallet state: %v", err) + } + } + + lnwallet.Shutdown() } } diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index c905e9e2..e59dbf97 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -88,7 +88,6 @@ type InputScript struct { // as a signature to our version of the commitment transaction. // * We then verify the validity of all signatures before considering the // channel "open". -// TODO(roasbeef): update with single funder description type ChannelReservation struct { // This mutex MUST be held when either reading or modifying any of the // fields below. @@ -131,11 +130,11 @@ type ChannelReservation struct { wallet *LightningWallet } -// newChannelReservation creates a new channel reservation. This function is +// NewChannelReservation creates a new channel reservation. This function is // used only internally by lnwallet. In order to concurrent safety, the creation // of all channel reservations should be carried out via the // lnwallet.InitChannelReservation interface. -func newChannelReservation(capacity, fundingAmt btcutil.Amount, minFeeRate btcutil.Amount, +func NewChannelReservation(capacity, fundingAmt btcutil.Amount, minFeeRate btcutil.Amount, wallet *LightningWallet, id uint64, numConfs uint16) *ChannelReservation { var ourBalance btcutil.Amount var theirBalance btcutil.Amount @@ -160,7 +159,7 @@ func newChannelReservation(capacity, fundingAmt btcutil.Amount, minFeeRate btcut OurBalance: ourBalance, TheirBalance: theirBalance, MinFeePerKb: minFeeRate, - Db: wallet.channelDB, + Db: wallet.ChannelDB, }, numConfsToOpen: numConfs, reservationID: id, @@ -315,6 +314,26 @@ func (r *ChannelReservation) FinalFundingTx() *wire.MsgTx { return r.fundingTx } +// FundingRedeemScript returns the fully populated funding redeem script. +// +// NOTE: This method will only return a non-nil value after either +// ProcesContribution or ProcessSingleContribution have been executed and +// returned without error. +func (r *ChannelReservation) FundingRedeemScript() []byte { + r.RLock() + defer r.RUnlock() + return r.partialState.FundingRedeemScript +} + +// LocalCommitTx returns the commitment transaction for the local node involved +// in this funding reservation. +func (r *ChannelReservation) LocalCommitTx() *wire.MsgTx { + r.RLock() + defer r.RUnlock() + + return r.partialState.OurCommitTx +} + // FundingOutpoint returns the outpoint of the funding transaction. // // NOTE: The pointer returned will only be set once the .ProcesContribution() @@ -346,6 +365,7 @@ func (r *ChannelReservation) Cancel() error { // transaction for this pending payment channel obtains the configured number // of confirmations. Once confirmations have been obtained, a fully initialized // LightningChannel instance is returned, allowing for channel updates. +// // NOTE: If this method is called before .CompleteReservation(), it will block // indefinitely. func (r *ChannelReservation) DispatchChan() <-chan *LightningChannel { diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 263e34e6..41c1894e 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -4,7 +4,6 @@ import ( "encoding/hex" "errors" "fmt" - "math" "sync" "sync/atomic" @@ -12,17 +11,14 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/elkrem" - "github.com/roasbeef/btcd/btcjson" + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcutil/hdkeychain" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/txscript" "github.com/roasbeef/btcd/wire" "github.com/roasbeef/btcutil" - "github.com/roasbeef/btcutil/coinset" "github.com/roasbeef/btcutil/txsort" - "github.com/roasbeef/btcwallet/chain" - "github.com/roasbeef/btcwallet/waddrmgr" - btcwallet "github.com/roasbeef/btcwallet/wallet" ) const ( @@ -33,6 +29,13 @@ const ( // elkremRootIndex is the top level HD key index from which secrets // used to generate elkrem roots should be derived from. elkremRootIndex = hdkeychain.HardenedKeyStart + 1 + + // identityKeyIndex is the top level HD key index which is used to + // generate/rotate identity keys. + // + // TODO(roasbeef): should instead be child to make room for future + // rotations, etc. + identityKeyIndex = hdkeychain.HardenedKeyStart + 2 ) var ( @@ -221,7 +224,7 @@ type channelOpenMsg struct { type LightningWallet struct { // This mutex is to be held when generating external keys to be used // as multi-sig, and commitment keys within the channel. - KeyGenMtx sync.RWMutex + keyGenMtx sync.RWMutex // This mutex MUST be held when performing coin selection in order to // avoid inadvertently creating multiple funding transaction which @@ -231,25 +234,27 @@ type LightningWallet struct { // A wrapper around a namespace within boltdb reserved for ln-based // wallet meta-data. See the 'channeldb' package for further // information. - channelDB *channeldb.DB + ChannelDB *channeldb.DB // Used by in order to obtain notifications about funding transaction // reaching a specified confirmation depth, and to catch // counterparty's broadcasting revoked commitment states. chainNotifier chainntnfs.ChainNotifier - // The core wallet, all non Lightning Network specific interaction is - // proxied to the internal wallet. - *btcwallet.Wallet + // wallet is the the core wallet, all non Lightning Network specific + // interaction is proxied to the internal wallet. + WalletController + + // Signer is the wallet's current Signer implementation. This Signer is + // used to generate signature for all inputs to potential funding + // transactions, as well as for spends from the funding transaction to + // update the commitment state. + Signer Signer + + // chainIO is an instance of the BlockChainIO interface. chainIO is + // used to lookup the existance of outputs within the utxo set. + chainIO BlockChainIO - // An active RPC connection to a full-node. In the case of a btcd node, - // websockets are used for notifications. If using Bitcoin Core, - // notifications are either generated via long-polling or the usage of - // ZeroMQ. - // TODO(roasbeef): make into interface need: getrawtransaction + gettxout - // * getrawtransaction -> verify proof of channel links - // * gettxout -> verify inputs to funding tx exist and are unspent - rpc *chain.RPCClient // rootKey is the root HD key dervied from a WalletController private // key. This rootKey is used to derive all LN specific secrets. rootKey *hdkeychain.ExtendedKey @@ -269,7 +274,12 @@ type LightningWallet struct { // TODO(roasbeef): zombie garbage collection routine to solve // lost-object/starvation problem/attack. - cfg *Config + // lockedOutPoints is a set of the currently locked outpoint. This + // information is kept in order to provide an easy way to unlock all + // the currently locked outpoints. + lockedOutPoints map[wire.OutPoint]struct{} + + netParams *chaincfg.Params started int32 shutdown int32 @@ -286,21 +296,12 @@ type LightningWallet struct { // // NOTE: The passed channeldb, and ChainNotifier should already be fully // initialized/started before being passed as a function arugment. -func NewLightningWallet(config *Config, cdb *channeldb.DB, - notifier chainntnfs.ChainNotifier) (*LightningWallet, error) { +func NewLightningWallet(cdb *channeldb.DB, notifier chainntnfs.ChainNotifier, + wallet WalletController, signer Signer, bio BlockChainIO, + netParams *chaincfg.Params) (*LightningWallet, error) { - // Ensure the wallet exists or create it when the create flag is set. - netDir := networkDir(config.DataDir, config.NetParams) + // TODO(roasbeef): need a another wallet level config - var pubPass []byte - if config.PublicPass == nil { - pubPass = defaultPubPassphrase - } else { - pubPass = config.PublicPass - } - - loader := btcwallet.NewLoader(config.NetParams, netDir) - walletExists, err := loader.WalletExists() // Fetch the root derivation key from the wallet's HD chain. We'll use // this to generate specific Lightning related secrets on the fly. rootKey, err := wallet.FetchRootKey() @@ -308,68 +309,25 @@ func NewLightningWallet(config *Config, cdb *channeldb.DB, return nil, err } - var createID bool - var wallet *btcwallet.Wallet - if !walletExists { - // Wallet has never been created, perform initial set up. - wallet, err = loader.CreateNewWallet(pubPass, config.PrivatePass, - config.HdSeed) - if err != nil { - return nil, err - } - - createID = true - } else { - // Wallet has been created and been initialized at this point, open it - // along with all the required DB namepsaces, and the DB itself. - wallet, err = loader.OpenExistingWallet(pubPass, false) - if err != nil { - return nil, err - } - } - - if err := wallet.Manager.Unlock(config.PrivatePass); err != nil { - return nil, err - } - - // If we just created the wallet, then reserve, and store a key for - // our ID within the Lightning Network. - if createID { - account := uint32(waddrmgr.DefaultAccountNum) - adrs, err := wallet.Manager.NextInternalAddresses(account, 1, waddrmgr.WitnessPubKey) - if err != nil { - return nil, err - } - - idPubkeyHash := adrs[0].Address().ScriptAddress() - if err := cdb.PutIdKey(idPubkeyHash); err != nil { - return nil, err - } - walletLog.Infof("stored identity key pubkey hash in channeldb") - } - - // Create a special websockets rpc client for btcd which will be used - // by the wallet for notifications, calls, etc. - rpcc, err := chain.NewRPCClient(config.NetParams, config.RpcHost, - config.RpcUser, config.RpcPass, config.CACert, false, 20) + // TODO(roasbeef): always re-derive on the fly? + rootKeyRaw := rootKey.Serialize() + rootMasterKey, err := hdkeychain.NewMaster(rootKeyRaw, netParams) if err != nil { return nil, err } return &LightningWallet{ - chainNotifier: notifier, - rpc: rpcc, - Wallet: wallet, - channelDB: cdb, - 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, - cfg: config, - fundingLimbo: make(map[uint64]*ChannelReservation), - quit: make(chan struct{}), rootKey: rootMasterKey, + chainNotifier: notifier, + Signer: signer, + WalletController: wallet, + chainIO: bio, + ChannelDB: cdb, + msgChan: make(chan interface{}, msgBufferSize), + nextFundingID: 0, + fundingLimbo: make(map[uint64]*ChannelReservation), + lockedOutPoints: make(map[wire.OutPoint]struct{}), + quit: make(chan struct{}), }, nil } @@ -381,16 +339,10 @@ func (l *LightningWallet) Startup() error { return nil } - // Establish an RPC connection in additino to starting the goroutines - // in the underlying wallet. - if err := l.rpc.Start(); err != nil { + // Start the underlying wallet controller. + if err := l.Start(); err != nil { return err } - l.Start() - - // Pass the rpc client into the wallet so it can sync up to the - // current main chain. - l.SynchronizeRPC(l.rpc) l.wg.Add(1) // TODO(roasbeef): multiple request handlers? @@ -407,16 +359,59 @@ func (l *LightningWallet) Shutdown() error { // Signal the underlying wallet controller to shutdown, waiting until // all active goroutines have been shutdown. - l.Stop() - l.WaitForShutdown() - - l.rpc.Shutdown() + if err := l.Stop(); err != nil { + return err + } close(l.quit) l.wg.Wait() return nil } +// LockOutpoints returns a list of all currently locked outpoint. +func (l *LightningWallet) LockedOutpoints() []*wire.OutPoint { + outPoints := make([]*wire.OutPoint, 0, len(l.lockedOutPoints)) + for outPoint := range l.lockedOutPoints { + outPoints = append(outPoints, &outPoint) + } + + return outPoints +} + +// ResetReservations reset the volatile wallet state which trakcs all currently +// active reservations. +func (l *LightningWallet) ResetReservations() { + l.nextFundingID = 0 + l.fundingLimbo = make(map[uint64]*ChannelReservation) + + for outpoint := range l.lockedOutPoints { + l.UnlockOutpoint(outpoint) + } + l.lockedOutPoints = make(map[wire.OutPoint]struct{}) +} + +// ActiveReservations returns a slice of all the currently active +// (non-cancalled) reservations. +func (l *LightningWallet) ActiveReservations() []*ChannelReservation { + reservations := make([]*ChannelReservation, 0, len(l.fundingLimbo)) + for _, reservation := range l.fundingLimbo { + reservations = append(reservations, reservation) + } + + return reservations +} + +// GetIdentitykey returns the identity private key of the wallet. +// TODO(roasbeef): should be moved elsewhere +func (l *LightningWallet) GetIdentitykey() (*btcec.PrivateKey, error) { + identityKey, err := l.rootKey.Child(identityKeyIndex) + if err != nil { + return nil, err + } + + return identityKey.ECPrivKey() +} + // requestHandler is the primary goroutine(s) resposible for handling, and // dispatching relies to all messages. func (l *LightningWallet) requestHandler() { @@ -489,16 +484,9 @@ func (l *LightningWallet) InitChannelReservation(capacity, // handleFundingReserveRequest processes a message intending to create, and // validate a funding reservation request. func (l *LightningWallet) handleFundingReserveRequest(req *initFundingReserveMsg) { - // Create a limbo and record entry for this newly pending funding request. - l.limboMtx.Lock() - - id := l.nextFundingID - reservation := newChannelReservation(req.capacity, req.fundingAmount, + id := atomic.AddUint64(&l.nextFundingID, 1) + reservation := NewChannelReservation(req.capacity, req.fundingAmount, req.minFeeRate, l, id, req.numConfs) - l.nextFundingID++ - l.fundingLimbo[id] = reservation - - l.limboMtx.Unlock() // Grab the mutex on the ChannelReservation to ensure thead-safety reservation.Lock() @@ -526,27 +514,26 @@ func (l *LightningWallet) handleFundingReserveRequest(req *initFundingReserveMsg // Grab two fresh keys from our HD chain, one will be used for the // multi-sig funding transaction, and the other for the commitment // transaction. - multiSigKey, err := l.getNextRawKey() + multiSigKey, err := l.NewRawKey() if err != nil { req.err <- err req.resp <- nil return } - commitKey, err := l.getNextRawKey() + commitKey, err := l.NewRawKey() if err != nil { req.err <- err req.resp <- nil return } reservation.partialState.OurMultiSigKey = multiSigKey - ourContribution.MultiSigKey = multiSigKey.PubKey() + ourContribution.MultiSigKey = multiSigKey reservation.partialState.OurCommitKey = commitKey - ourContribution.CommitKey = commitKey.PubKey() + ourContribution.CommitKey = commitKey // Generate a fresh address to be used in the case of a cooperative // channel close. - deliveryAddress, err := l.NewAddress(waddrmgr.DefaultAccountNum, - waddrmgr.WitnessPubKey) + deliveryAddress, err := l.NewAddress(WitnessPubKey, false) if err != nil { req.err <- err req.resp <- nil @@ -561,6 +548,12 @@ func (l *LightningWallet) handleFundingReserveRequest(req *initFundingReserveMsg reservation.partialState.OurDeliveryScript = deliveryScript ourContribution.DeliveryAddress = deliveryAddress + // Create a limbo and record entry for this newly pending funding + // request. + l.limboMtx.Lock() + l.fundingLimbo[id] = reservation + l.limboMtx.Unlock() + // Funding reservation request succesfully handled. The funding inputs // will be marked as unavailable until the reservation is either // completed, or cancecled. @@ -591,6 +584,7 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs // Mark all previously locked outpoints as usuable for future funding // requests. for _, unusedInput := range pendingReservation.ourContribution.Inputs { + delete(l.lockedOutPoints, unusedInput.PreviousOutPoint) l.UnlockOutpoint(unusedInput.PreviousOutPoint) } @@ -652,7 +646,7 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // Finally, add the 2-of-2 multi-sig output which will set up the lightning // channel. channelCapacity := int64(pendingReservation.partialState.Capacity) - redeemScript, multiSigOut, err := genFundingPkScript(ourKey.PubKey().SerializeCompressed(), + redeemScript, multiSigOut, err := GenFundingPkScript(ourKey.SerializeCompressed(), theirKey.SerializeCompressed(), channelCapacity) if err != nil { req.err <- err @@ -660,21 +654,6 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { } pendingReservation.partialState.FundingRedeemScript = redeemScript - // Register intent for notifications related to the funding output. - // This'll allow us to properly track the number of confirmations the - // funding tx has once it has been broadcasted. - // TODO(roasbeef): remove - lastBlock := l.Manager.SyncedTo() - scriptAddr, err := l.Manager.ImportScript(redeemScript, &lastBlock) - if err != nil { - req.err <- err - return - } - if err := l.rpc.NotifyReceived([]btcutil.Address{scriptAddr.Address()}); err != nil { - req.err <- err - return - } - // 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. @@ -684,81 +663,30 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // Next, sign all inputs that are ours, collecting the signatures in // order of the inputs. pendingReservation.ourFundingInputScripts = make([]*InputScript, 0, len(ourContribution.Inputs)) - hashCache := txscript.NewTxSigHashes(fundingTx) + signDesc := SignDescriptor{ + HashType: txscript.SigHashAll, + SigHashes: txscript.NewTxSigHashes(fundingTx), + } for i, txIn := range fundingTx.TxIn { - // Does the wallet know about the txin? - txDetail, _ := l.TxStore.TxDetails(&txIn.PreviousOutPoint.Hash) - if txDetail == nil { + info, err := l.FetchInputInfo(&txIn.PreviousOutPoint) + if err == ErrNotMine { continue - } - - // Is this our txin? - prevIndex := txIn.PreviousOutPoint.Index - prevOut := txDetail.TxRecord.MsgTx.TxOut[prevIndex] - _, addrs, _, _ := txscript.ExtractPkScriptAddrs(prevOut.PkScript, l.cfg.NetParams) - apkh := addrs[0] - - ai, err := l.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) + } else if err != nil { + req.err <- err return } - var witnessProgram []byte - inputScript := &InputScript{} + signDesc.Output = info + signDesc.InputIndex = i - // If we're spending p2wkh output nested within a p2sh output, - // then we'll need to attach a sigScript in addition to witness - // data. - if pka.IsNestedWitness() { - pubKey := privKey.PubKey() - pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed()) - - // Next, we'll generate a valid sigScript that'll allow us to spend - // the p2sh output. The sigScript will contain only a single push of - // the p2wkh witness program corresponding to the matching public key - // of this address. - p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, - l.cfg.NetParams) - if err != nil { - req.err <- fmt.Errorf("unable to create p2wkh addr: %v", err) - return - } - witnessProgram, err = txscript.PayToAddrScript(p2wkhAddr) - if err != nil { - req.err <- fmt.Errorf("unable to create witness program: %v", err) - return - } - bldr := txscript.NewScriptBuilder() - bldr.AddData(witnessProgram) - sigScript, err := bldr.Script() - if err != nil { - req.err <- fmt.Errorf("unable to create scriptsig: %v", err) - return - } - txIn.SignatureScript = sigScript - inputScript.ScriptSig = sigScript - } else { - witnessProgram = prevOut.PkScript - } - - // Generate a valid witness stack for the input. - inputValue := prevOut.Value - witnessScript, err := txscript.WitnessScript(fundingTx, hashCache, i, - inputValue, witnessProgram, txscript.SigHashAll, privKey, true) + inputScript, err := l.Signer.ComputeInputScript(fundingTx, &signDesc) if err != nil { - req.err <- fmt.Errorf("cannot create witnessscript: %s", err) + req.err <- err return } - txIn.Witness = witnessScript - inputScript.Witness = witnessScript + txIn.SignatureScript = inputScript.ScriptSig + txIn.Witness = inputScript.Witness pendingReservation.ourFundingInputScripts = append( pendingReservation.ourFundingInputScripts, inputScript, @@ -766,10 +694,10 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { } // Locate the index of the multi-sig outpoint in order to record it - // since the outputs are cannonically sorted. If this is a sigle funder + // since the outputs are cannonically sorted. If this is a single funder // workflow, then we'll also need to send this to the remote node. fundingTxID := fundingTx.TxSha() - _, multiSigIndex := findScriptOutputIndex(fundingTx, multiSigOut.PkScript) + _, multiSigIndex := FindScriptOutputIndex(fundingTx, multiSigOut.PkScript) fundingOutpoint := wire.NewOutPoint(&fundingTxID, multiSigIndex) pendingReservation.partialState.FundingOutpoint = fundingOutpoint @@ -799,7 +727,7 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { return } theirCommitKey := theirContribution.CommitKey - ourRevokeKey := deriveRevocationPubkey(theirCommitKey, firstPreimage[:]) + ourRevokeKey := DeriveRevocationPubkey(theirCommitKey, firstPreimage[:]) // Create the txIn to our commitment transaction; required to construct // the commitment transactions. @@ -811,14 +739,14 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { ourBalance := ourContribution.FundingAmount theirBalance := theirContribution.FundingAmount ourCommitKey := ourContribution.CommitKey - ourCommitTx, err := createCommitTx(fundingTxIn, ourCommitKey, theirCommitKey, + ourCommitTx, err := CreateCommitTx(fundingTxIn, ourCommitKey, theirCommitKey, ourRevokeKey, ourContribution.CsvDelay, ourBalance, theirBalance) if err != nil { req.err <- err return } - theirCommitTx, err := createCommitTx(fundingTxIn, theirCommitKey, ourCommitKey, + theirCommitTx, err := CreateCommitTx(fundingTxIn, theirCommitKey, ourCommitKey, theirContribution.RevocationKey, theirContribution.CsvDelay, theirBalance, ourBalance) if err != nil { @@ -849,10 +777,15 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // Generate a signature for their version of the initial commitment // transaction. - hashCache = txscript.NewTxSigHashes(theirCommitTx) - channelBalance := pendingReservation.partialState.Capacity - sigTheirCommit, err := txscript.RawTxInWitnessSignature(theirCommitTx, hashCache, 0, - int64(channelBalance), redeemScript, txscript.SigHashAll, ourKey) + signDesc = SignDescriptor{ + RedeemScript: redeemScript, + PubKey: ourKey, + Output: multiSigOut, + HashType: txscript.SigHashAll, + SigHashes: txscript.NewTxSigHashes(theirCommitTx), + InputIndex: 0, + } + sigTheirCommit, err := l.Signer.SignOutputRaw(theirCommitTx, &signDesc) if err != nil { req.err <- err return @@ -890,7 +823,7 @@ func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg ourKey := pendingReservation.partialState.OurMultiSigKey theirKey := theirContribution.MultiSigKey channelCapacity := int64(pendingReservation.partialState.Capacity) - redeemScript, _, err := genFundingPkScript(ourKey.PubKey().SerializeCompressed(), + redeemScript, _, err := GenFundingPkScript(ourKey.SerializeCompressed(), theirKey.SerializeCompressed(), channelCapacity) if err != nil { req.err <- err @@ -915,7 +848,7 @@ func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg } pendingReservation.partialState.LocalElkrem = elkremSender theirCommitKey := theirContribution.CommitKey - ourRevokeKey := deriveRevocationPubkey(theirCommitKey, firstPreimage[:]) + ourRevokeKey := DeriveRevocationPubkey(theirCommitKey, firstPreimage[:]) // Initialize an empty sha-chain for them, tracking the current pending // revocation hash (we don't yet know the pre-image so we can't add it @@ -976,25 +909,16 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs // 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) + output, err := l.chainIO.GetUtxo(&prevOut.Hash, prevOut.Index) if output == nil { msg.err <- fmt.Errorf("input to funding tx does not exist: %v", err) return } - pkScript, err := hex.DecodeString(output.ScriptPubKey.Hex) - if err != nil { - msg.err <- err - return - } - // Sadly, gettxout returns the output value in BTC - // instead of satoshis. - inputValue := int64(output.Value) * 1e8 - // Ensure that the witness+sigScript combo is valid. - vm, err := txscript.NewEngine(pkScript, + vm, err := txscript.NewEngine(output.PkScript, fundingTx, i, txscript.StandardVerifyFlags, nil, - fundingHashCache, inputValue) + fundingHashCache, output.Value) if err != nil { // TODO(roasbeef): cancel at this stage if invalid sigs? msg.err <- fmt.Errorf("cannot create script engine: %s", err) @@ -1014,54 +938,37 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs pendingReservation.theirCommitmentSig = msg.theirCommitmentSig commitTx := pendingReservation.partialState.OurCommitTx theirKey := pendingReservation.theirContribution.MultiSigKey - ourKey := pendingReservation.partialState.OurMultiSigKey // Re-generate both the redeemScript and p2sh output. We sign the // redeemScript script, but include the p2sh output as the subscript // for verification. redeemScript := pendingReservation.partialState.FundingRedeemScript - p2wsh, err := witnessScriptHash(redeemScript) - if err != nil { - msg.err <- err - return - } - - // First, we sign our copy of the commitment transaction ourselves. - channelValue := int64(pendingReservation.partialState.Capacity) - hashCache := txscript.NewTxSigHashes(commitTx) - ourCommitSig, err := txscript.RawTxInWitnessSignature(commitTx, hashCache, 0, - channelValue, redeemScript, txscript.SigHashAll, ourKey) - if err != nil { - msg.err <- err - return - } // Next, create the spending scriptSig, and then verify that the script // is complete, allowing us to spend from the funding transaction. theirCommitSig := msg.theirCommitmentSig - ourKeySer := ourKey.PubKey().SerializeCompressed() - theirKeySer := theirKey.SerializeCompressed() - witness := spendMultiSig(redeemScript, ourKeySer, ourCommitSig, - theirKeySer, theirCommitSig) - - // Finally, create an instance of a Script VM, and ensure that the - // Script executes succesfully. - commitTx.TxIn[0].Witness = witness - vm, err := txscript.NewEngine(p2wsh, - commitTx, 0, txscript.StandardVerifyFlags, nil, - nil, channelValue) + channelValue := int64(pendingReservation.partialState.Capacity) + hashCache := txscript.NewTxSigHashes(commitTx) + sigHash, err := txscript.CalcWitnessSigHash(redeemScript, hashCache, + txscript.SigHashAll, commitTx, 0, channelValue) if err != nil { - msg.err <- err - return - } - if err := vm.Execute(); err != nil { msg.err <- fmt.Errorf("counterparty's commitment signature is invalid: %v", err) return } - // Strip and store the signature to ensure that our commitment - // transaction doesn't stay hot. - commitTx.TxIn[0].Witness = nil + walletLog.Infof("sighash verify: %v", hex.EncodeToString(sigHash)) + walletLog.Infof("initer verifying tx: %v", spew.Sdump(commitTx)) + + // Verify that we've received a valid signature from the remote party + // for our version of the commitment transaction. + sig, err := btcec.ParseSignature(theirCommitSig, btcec.S256()) + if err != nil { + msg.err <- err + return + } else if !sig.Verify(sigHash, theirKey) { + msg.err <- fmt.Errorf("counterparty's commitment signature is invalid") + return + } pendingReservation.partialState.OurCommitSig = theirCommitSig // Funding complete, this entry can be removed from limbo. @@ -1126,14 +1033,14 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { theirCommitKey := pendingReservation.theirContribution.CommitKey ourBalance := pendingReservation.ourContribution.FundingAmount theirBalance := pendingReservation.theirContribution.FundingAmount - ourCommitTx, err := createCommitTx(fundingTxIn, ourCommitKey, theirCommitKey, + ourCommitTx, err := CreateCommitTx(fundingTxIn, ourCommitKey, theirCommitKey, pendingReservation.ourContribution.RevocationKey, pendingReservation.ourContribution.CsvDelay, ourBalance, theirBalance) if err != nil { req.err <- err return } - theirCommitTx, err := createCommitTx(fundingTxIn, theirCommitKey, ourCommitKey, + theirCommitTx, err := CreateCommitTx(fundingTxIn, theirCommitKey, ourCommitKey, req.revokeKey, pendingReservation.theirContribution.CsvDelay, theirBalance, ourBalance) if err != nil { @@ -1148,65 +1055,51 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { pendingReservation.partialState.OurCommitTx = ourCommitTx txsort.InPlaceSort(theirCommitTx) - // Verify that their signature of valid for our current commitment - // transaction. Re-generate both the redeemScript and p2sh output. We - // sign the redeemScript script, but include the p2sh output as the - // subscript for verification. - // TODO(roasbeef): replace with regular sighash calculation once the PR - // is merged. redeemScript := pendingReservation.partialState.FundingRedeemScript - p2wsh, err := witnessScriptHash(redeemScript) - if err != nil { - req.err <- err - return - } - // TODO(roasbeef): remove all duplication after merge. - - // First, we sign our copy of the commitment transaction ourselves. channelValue := int64(pendingReservation.partialState.Capacity) hashCache := txscript.NewTxSigHashes(ourCommitTx) theirKey := pendingReservation.theirContribution.MultiSigKey ourKey := pendingReservation.partialState.OurMultiSigKey - ourCommitSig, err := txscript.RawTxInWitnessSignature(ourCommitTx, hashCache, 0, - channelValue, redeemScript, txscript.SigHashAll, ourKey) + + sigHash, err := txscript.CalcWitnessSigHash(redeemScript, hashCache, + txscript.SigHashAll, ourCommitTx, 0, channelValue) if err != nil { req.err <- err return } - // Next, create the spending scriptSig, and then verify that the script - // is complete, allowing us to spend from the funding transaction. - ourKeySer := ourKey.PubKey().SerializeCompressed() - theirKeySer := theirKey.SerializeCompressed() - witness := spendMultiSig(redeemScript, ourKeySer, ourCommitSig, - theirKeySer, req.theirCommitmentSig) - - // Finally, create an instance of a Script VM, and ensure that the - // Script executes succesfully. - ourCommitTx.TxIn[0].Witness = witness - // TODO(roasbeef): replace engine with plain sighash check - vm, err := txscript.NewEngine(p2wsh, ourCommitTx, 0, - txscript.StandardVerifyFlags, nil, nil, channelValue) + // Verify that we've received a valid signature from the remote party + // for our version of the commitment transaction. + sig, err := btcec.ParseSignature(req.theirCommitmentSig, btcec.S256()) if err != nil { req.err <- err return - } - if err := vm.Execute(); err != nil { - req.err <- fmt.Errorf("counterparty's commitment signature is invalid: %v", err) + } else if !sig.Verify(sigHash, theirKey) { + req.err <- fmt.Errorf("counterparty's commitment signature is invalid") return } - - // Strip and store the signature to ensure that our commitment - // transaction doesn't stay hot. - ourCommitTx.TxIn[0].Witness = nil pendingReservation.partialState.OurCommitSig = req.theirCommitmentSig // With their signature for our version of the commitment transactions // verified, we can now generate a signature for their version, // allowing the funding transaction to be safely broadcast. - hashCache = txscript.NewTxSigHashes(theirCommitTx) - sigTheirCommit, err := txscript.RawTxInWitnessSignature(theirCommitTx, hashCache, 0, - channelValue, redeemScript, txscript.SigHashAll, ourKey) + p2wsh, err := witnessScriptHash(redeemScript) + if err != nil { + req.err <- err + return + } + signDesc := SignDescriptor{ + RedeemScript: redeemScript, + PubKey: ourKey, + Output: &wire.TxOut{ + PkScript: p2wsh, + Value: channelValue, + }, + HashType: txscript.SigHashAll, + SigHashes: txscript.NewTxSigHashes(theirCommitTx), + InputIndex: 0, + } + sigTheirCommit, err := l.Signer.SignOutputRaw(theirCommitTx, &signDesc) if err != nil { req.err <- err return @@ -1249,8 +1142,7 @@ func (l *LightningWallet) handleChannelOpen(req *channelOpenMsg) { // Finally, create and officially open the payment channel! // TODO(roasbeef): CreationTime once tx is 'open' - channel, _ := NewLightningChannel(l, l.chainNotifier, l.channelDB, - res.partialState) + channel, _ := NewLightningChannel(l.Signer, l, l.chainNotifier, res.partialState) res.chanOpen <- channel req.err <- nil @@ -1291,61 +1183,11 @@ out: // Finally, create and officially open the payment channel! // TODO(roasbeef): CreationTime once tx is 'open' - channel, _ := NewLightningChannel(l, l.chainNotifier, l.channelDB, + channel, _ := NewLightningChannel(l.Signer, l, l.chainNotifier, res.partialState) res.chanOpen <- channel } -// getNextRawKey retrieves the next key within our HD key-chain for use within -// as a multi-sig key within the funding transaction, or within the commitment -// transaction's outputs. -// TODO(roasbeef): on shutdown, write state of pending keys, then read back? -func (l *LightningWallet) getNextRawKey() (*btcec.PrivateKey, error) { - l.KeyGenMtx.Lock() - defer l.KeyGenMtx.Unlock() - - nextAddr, err := l.Manager.NextExternalAddresses(waddrmgr.DefaultAccountNum, - 1, waddrmgr.WitnessPubKey) - if err != nil { - return nil, err - } - - pkAddr := nextAddr[0].(waddrmgr.ManagedPubKeyAddress) - - return pkAddr.PrivKey() -} - -// ListUnspentWitness returns a slice of all the unspent outputs the wallet -// controls which pay to witness programs either directly or indirectly. -func (l *LightningWallet) ListUnspentWitness(minConfs int32) ([]*btcjson.ListUnspentResult, error) { - // First, grab all the unfiltered currently unspent outputs. - maxConfs := int32(math.MaxInt32) - unspentOutputs, err := l.ListUnspent(minConfs, maxConfs, nil) - if err != nil { - return nil, err - } - - // Next, we'll run through all the regular outputs, only saving those - // which are p2wkh outputs or a p2wsh output nested within a p2sh output. - witnessOutputs := make([]*btcjson.ListUnspentResult, 0, len(unspentOutputs)) - for _, output := range unspentOutputs { - pkScript, err := hex.DecodeString(output.ScriptPubKey) - if err != nil { - return nil, err - } - - // TODO(roasbeef): this assumes all p2sh outputs returned by - // the wallet are nested p2sh... - if txscript.IsPayToWitnessPubKeyHash(pkScript) || - txscript.IsPayToScriptHash(pkScript) { - witnessOutputs = append(witnessOutputs, output) - } - - } - - return witnessOutputs, nil -} - // selectCoinsAndChange performs coin selection in order to obtain witness // outputs which sum to at least 'numCoins' amount of satoshis. If coin // selection is succesful/possible, then the selected coins are available within From a28cf3fdf8e9243c92a8eb5a353f1d086ef642ce Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:51:53 -0700 Subject: [PATCH 12/15] lnd: obtain the identity key via the wallet's GetIdentityKey method --- server.go | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/server.go b/server.go index 4fab56c0..65420575 100644 --- a/server.go +++ b/server.go @@ -18,7 +18,6 @@ import ( "github.com/BitfuryLightning/tools/routing" "github.com/BitfuryLightning/tools/rt/graph" - "github.com/roasbeef/btcwallet/waddrmgr" ) // server is the main server of the Lightning Network Daemon. The server @@ -66,7 +65,7 @@ type server struct { func newServer(listenAddrs []string, notifier chainntnfs.ChainNotifier, wallet *lnwallet.LightningWallet, chanDB *channeldb.DB) (*server, error) { - privKey, err := getIdentityPrivKey(chanDB, wallet) + privKey, err := wallet.GetIdentitykey() if err != nil { return nil, err } @@ -493,33 +492,3 @@ func (s *server) listener(l net.Listener) { s.wg.Done() } - -// getIdentityPrivKey gets the identity private key out of the wallet DB. -func getIdentityPrivKey(c *channeldb.DB, - w *lnwallet.LightningWallet) (*btcec.PrivateKey, error) { - - // First retrieve the current identity address for this peer. - adr, err := c.GetIdAdr() - if err != nil { - return nil, err - } - - // Using the ID address, request the private key coresponding to the - // address from the wallet's address manager. - adr2, err := w.Manager.Address(adr) - if err != nil { - return nil, err - } - - serializedKey := adr2.(waddrmgr.ManagedPubKeyAddress).PubKey().SerializeCompressed() - keyEncoded := hex.EncodeToString(serializedKey) - ltndLog.Infof("identity address: %v", adr) - ltndLog.Infof("identity pubkey retrieved: %v", keyEncoded) - - priv, err := adr2.(waddrmgr.ManagedPubKeyAddress).PrivKey() - if err != nil { - return nil, err - } - - return priv, nil -} From abf658a719ccabc5161b49b7dc413500b4dcb7b4 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:53:18 -0700 Subject: [PATCH 13/15] lnd: modify the rpcServer's methods to use the new wallet API's --- rpcserver.go | 46 +++++++++++----------------------------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/rpcserver.go b/rpcserver.go index 621958af..a897b6ad 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -10,6 +10,7 @@ import ( "github.com/lightningnetwork/lnd/lndc" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/roasbeef/btcd/txscript" "github.com/roasbeef/btcd/wire" @@ -96,7 +97,7 @@ func (r *rpcServer) sendCoinsOnChain(paymentMap map[string]int64) (*wire.ShaHash return nil, err } - return r.server.lnwallet.SendOutputs(outputs, defaultAccount, 1) + return r.server.lnwallet.SendOutputs(outputs) } // SendCoins executes a request to send coins to a particular address. Unlike @@ -136,23 +137,19 @@ func (r *rpcServer) SendMany(ctx context.Context, func (r *rpcServer) NewAddress(ctx context.Context, in *lnrpc.NewAddressRequest) (*lnrpc.NewAddressResponse, error) { - r.server.lnwallet.KeyGenMtx.Lock() - defer r.server.lnwallet.KeyGenMtx.Unlock() - // Translate the gRPC proto address type to the wallet controller's // available address types. - var addrType waddrmgr.AddressType + var addrType lnwallet.AddressType switch in.Type { case lnrpc.NewAddressRequest_WITNESS_PUBKEY_HASH: - addrType = waddrmgr.WitnessPubKey + addrType = lnwallet.WitnessPubKey case lnrpc.NewAddressRequest_NESTED_PUBKEY_HASH: - addrType = waddrmgr.NestedWitnessPubKey + addrType = lnwallet.NestedWitnessPubKey case lnrpc.NewAddressRequest_PUBKEY_HASH: - addrType = waddrmgr.PubKeyHash + addrType = lnwallet.PubKeyHash } - addr, err := r.server.lnwallet.NewAddress(defaultAccount, - addrType) + addr, err := r.server.lnwallet.NewAddress(addrType, false) if err != nil { return nil, err } @@ -378,35 +375,14 @@ func (r *rpcServer) ListPeers(ctx context.Context, func (r *rpcServer) WalletBalance(ctx context.Context, in *lnrpc.WalletBalanceRequest) (*lnrpc.WalletBalanceResponse, error) { - var balance float64 - - if in.WitnessOnly { - witnessOutputs, err := r.server.lnwallet.ListUnspentWitness(1) - if err != nil { - return nil, err - } - - // We need to convert from BTC to satoshi here otherwise, and - // incorrect sum will be returned. - var outputSum btcutil.Amount - for _, witnessOutput := range witnessOutputs { - outputSum += btcutil.Amount(witnessOutput.Amount * 1e8) - } - - balance = outputSum.ToBTC() - } else { - // TODO(roasbeef): make num confs a param - outputSum, err := r.server.lnwallet.CalculateBalance(1) - if err != nil { - return nil, err - } - - balance = outputSum.ToBTC() + balance, err := r.server.lnwallet.ConfirmedBalance(1, in.WitnessOnly) + if err != nil { + return nil, err } rpcsLog.Debugf("[walletbalance] balance=%v", balance) - return &lnrpc.WalletBalanceResponse{balance}, nil + return &lnrpc.WalletBalanceResponse{float64(balance)}, nil } // PendingChannels returns a list of all the channels that are currently From 0d871dabb38c5b0d92b099f189e230b6b0edc54f Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 12 Aug 2016 15:56:57 -0700 Subject: [PATCH 14/15] lnd: modify the daemon's initialization to use new wallet API's --- fundingmanager.go | 3 +-- lnd.go | 22 +++++++++++++++------- peer.go | 4 ++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/fundingmanager.go b/fundingmanager.go index 3748f77a..c38f13f8 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -469,7 +469,6 @@ func (f *fundingManager) handleFundingComplete(fmsg *fundingCompleteMsg) { // Append a sighash type of SigHashAll to the signature as it's the // sighash type used implicitly within this type of channel for // commitment transactions. - commitSig = append(commitSig, byte(txscript.SigHashAll)) revokeKey := fmsg.msg.RevocationKey if err := resCtx.reservation.CompleteReservationSingle(revokeKey, fundingOut, commitSig); err != nil { // TODO(roasbeef): better error logging: peerID, channelID, etc. @@ -521,7 +520,7 @@ func (f *fundingManager) handleFundingSignComplete(fmsg *fundingSignCompleteMsg) // The remote peer has responded with a signature for our commitment // transaction. We'll verify the signature for validity, then commit // the state to disk as we can now open the channel. - commitSig := append(fmsg.msg.CommitSignature.Serialize(), byte(txscript.SigHashAll)) + commitSig := fmsg.msg.CommitSignature.Serialize() if err := resCtx.reservation.CompleteReservation(nil, commitSig); err != nil { fndgLog.Errorf("unable to complete reservation sign complete: %v", err) fmsg.peer.Disconnect() diff --git a/lnd.go b/lnd.go index efcb9a1d..74b801cf 100644 --- a/lnd.go +++ b/lnd.go @@ -18,6 +18,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/roasbeef/btcrpcclient" ) @@ -116,9 +117,8 @@ func lndMain() error { return err } - // Create, and start the lnwallet, which handles the core payment - // channel logic, and exposes control via proxy state machines. - walletConfig := &lnwallet.Config{ + // TODO(roasbeef): paarse config here select chosen WalletController + walletConfig := &btcwallet.Config{ PrivatePass: []byte("hello"), DataDir: filepath.Join(loadedConfig.DataDir, "lnwallet"), RpcHost: fmt.Sprintf("%v:%v", rpcIP[0], activeNetParams.rpcPort), @@ -127,7 +127,18 @@ func lndMain() error { CACert: rpcCert, NetParams: activeNetParams.Params, } - wallet, err := lnwallet.NewLightningWallet(walletConfig, chanDB, notifier) + wc, err := btcwallet.New(walletConfig) + if err != nil { + fmt.Printf("unable to create wallet controller: %v\n", err) + return err + } + signer := wc + bio := wc + + // Create, and start the lnwallet, which handles the core payment + // channel logic, and exposes control via proxy state machines. + wallet, err := lnwallet.NewLightningWallet(chanDB, notifier, + wc, signer, bio, activeNetParams.Params) if err != nil { fmt.Printf("unable to create wallet: %v\n", err) return err @@ -138,9 +149,6 @@ func lndMain() error { } ltndLog.Info("LightningWallet opened") - ec := &lnwallet.WaddrmgrEncryptorDecryptor{wallet.Manager} - chanDB.RegisterCryptoSystem(ec) - // Set up the core server which will listen for incoming peer // connections. defaultListenAddrs := []string{ diff --git a/peer.go b/peer.go index a37fdf90..db8b2106 100644 --- a/peer.go +++ b/peer.go @@ -218,8 +218,8 @@ func newPeer(conn net.Conn, server *server, btcNet wire.BitcoinNet, inbound bool func (p *peer) loadActiveChannels(chans []*channeldb.OpenChannel) error { for _, dbChan := range chans { chanID := dbChan.ChanID - lnChan, err := lnwallet.NewLightningChannel(p.server.lnwallet, - p.server.chainNotifier, p.server.chanDB, dbChan) + lnChan, err := lnwallet.NewLightningChannel(p.server.lnwallet.Signer, + p.server.lnwallet, p.server.chainNotifier, dbChan) if err != nil { return err } From bccb1b920348e646495e30dc385b9a34ab74937f Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 6 Sep 2016 12:44:05 -0700 Subject: [PATCH 15/15] rpc: ensure wallet balances are returned in units of BTC --- rpcserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcserver.go b/rpcserver.go index a897b6ad..f67821cd 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -382,7 +382,7 @@ func (r *rpcServer) WalletBalance(ctx context.Context, rpcsLog.Debugf("[walletbalance] balance=%v", balance) - return &lnrpc.WalletBalanceResponse{float64(balance)}, nil + return &lnrpc.WalletBalanceResponse{balance.ToBTC()}, nil } // PendingChannels returns a list of all the channels that are currently