diff --git a/channeldb/channel.go b/channeldb/channel.go index 945f1f62..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 @@ -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 @@ -810,12 +809,7 @@ func putChanCommitKeys(nodeChanBucket *bolt.Bucket, channel *OpenChannel, 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 } @@ -829,8 +823,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 @@ -850,12 +843,7 @@ func fetchChanCommitKeys(nodeChanBucket *bolt.Bucket, channel *OpenChannel, 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 } @@ -939,9 +927,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 @@ -956,11 +942,8 @@ func putChanFundingInfo(nodeChanBucket *bolt.Bucket, channel *OpenChannel, 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() @@ -989,9 +972,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 @@ -1008,17 +989,16 @@ func fetchChanFundingInfo(nodeChanBucket *bolt.Bucket, channel *OpenChannel, 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 3d2c62e5..b7ce9345 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[:]) @@ -144,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), @@ -154,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(), @@ -195,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(), @@ -234,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(), 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 } 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/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 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/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/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/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 -} 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 + // } 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 } 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/script_utils.go b/lnwallet/script_utils.go index 942c3958..01dff4e6 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 @@ -742,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/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(), 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) -} diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index c5b69a35..41c1894e 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -4,7 +4,6 @@ import ( "encoding/hex" "errors" "fmt" - "math" "sync" "sync/atomic" @@ -12,23 +11,31 @@ 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 ( // 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 + + // 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 ( @@ -217,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 @@ -227,25 +234,30 @@ 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 - // 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 + // 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 + + // 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{} @@ -262,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 @@ -279,86 +296,38 @@ 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() if err != nil { 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 } @@ -370,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? @@ -396,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() { @@ -478,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() @@ -502,7 +501,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 @@ -513,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 @@ -548,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. @@ -578,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) } @@ -639,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 @@ -647,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. @@ -671,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, @@ -753,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 @@ -767,11 +708,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) @@ -780,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. @@ -792,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 { @@ -830,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 @@ -871,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 @@ -879,9 +831,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 { @@ -890,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 @@ -951,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) @@ -989,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. @@ -1101,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 { @@ -1123,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 @@ -1224,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 @@ -1266,68 +1183,18 @@ 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 // 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 +1204,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 +1215,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,57 +1225,151 @@ 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 } -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) +// 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) Decrypt(c []byte) ([]byte, error) { - return w.M.Decrypt(waddrmgr.CKTPrivate, c) -} +// 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) { -func (w *WaddrmgrEncryptorDecryptor) OverheadSize() uint32 { - return 24 + 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 + } } 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 } diff --git a/rpcserver.go b/rpcserver.go index 621958af..f67821cd 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{balance.ToBTC()}, nil } // PendingChannels returns a list of all the channels that are currently 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 -}