diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 982c5029..e6900f85 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -274,6 +274,9 @@ type LightningChannel struct { // wallet can add back. lnwallet *LightningWallet + signer Signer + signDesc *SignDescriptor + channelEvents chainntnfs.ChainNotifier sync.RWMutex @@ -281,7 +284,8 @@ type LightningChannel struct { ourLogCounter uint32 theirLogCounter uint32 - status channelState + status channelState + Capacity btcutil.Amount // currentHeight is the current height of our local commitment chain. // This is also the same as the number of updates to the channel we've @@ -337,10 +341,12 @@ type LightningChannel struct { ourLogIndex map[uint32]*list.Element theirLogIndex map[uint32]*list.Element - fundingTxIn *wire.TxIn - fundingP2WSH []byte + LocalDeliveryScript []byte + RemoteDeliveryScript []byte - channelDB *channeldb.DB + FundingRedeemScript []byte + fundingTxIn *wire.TxIn + fundingP2WSH []byte started int32 shutdown int32 @@ -354,11 +360,13 @@ type LightningChannel struct { // and the current settled channel state. Throughout state transitions, then // channel will automatically persist pertinent state to the database in an // efficient manner. -func NewLightningChannel(wallet *LightningWallet, events chainntnfs.ChainNotifier, - chanDB *channeldb.DB, state *channeldb.OpenChannel) (*LightningChannel, error) { +func NewLightningChannel(signer Signer, wallet *LightningWallet, + events chainntnfs.ChainNotifier, + state *channeldb.OpenChannel) (*LightningChannel, error) { // TODO(roasbeef): remove events+wallet lc := &LightningChannel{ + signer: signer, lnwallet: wallet, channelEvents: events, currentHeight: state.NumUpdates, @@ -370,7 +378,10 @@ func NewLightningChannel(wallet *LightningWallet, events chainntnfs.ChainNotifie theirUpdateLog: list.New(), ourLogIndex: make(map[uint32]*list.Element), theirLogIndex: make(map[uint32]*list.Element), - channelDB: chanDB, + Capacity: state.Capacity, + LocalDeliveryScript: state.OurDeliveryScript, + RemoteDeliveryScript: state.TheirDeliveryScript, + FundingRedeemScript: state.FundingRedeemScript, } // Initialize both of our chains the current un-revoked commitment for @@ -395,6 +406,17 @@ func NewLightningChannel(wallet *LightningWallet, events chainntnfs.ChainNotifie lc.fundingTxIn = wire.NewTxIn(state.FundingOutpoint, nil, nil) lc.fundingP2WSH = fundingPkScript + lc.signDesc = &SignDescriptor{ + PubKey: lc.channelState.OurMultiSigKey, + RedeemScript: lc.channelState.FundingRedeemScript, + Output: &wire.TxOut{ + PkScript: lc.fundingP2WSH, + Value: int64(lc.channelState.Capacity), + }, + HashType: txscript.SigHashAll, + InputIndex: 0, + } + return lc, nil } @@ -480,12 +502,12 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, var delayBalance, p2wkhBalance btcutil.Amount if remoteChain { selfKey = lc.channelState.TheirCommitKey - remoteKey = lc.channelState.OurCommitKey.PubKey() + remoteKey = lc.channelState.OurCommitKey delay = lc.channelState.RemoteCsvDelay delayBalance = theirBalance p2wkhBalance = ourBalance } else { - selfKey = lc.channelState.OurCommitKey.PubKey() + selfKey = lc.channelState.OurCommitKey remoteKey = lc.channelState.TheirCommitKey delay = lc.channelState.LocalCsvDelay delayBalance = ourBalance @@ -495,7 +517,7 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, // Generate a new commitment transaction with all the latest // unsettled/un-timed out HTLC's. ourCommitTx := !remoteChain - commitTx, err := createCommitTx(lc.fundingTxIn, selfKey, remoteKey, + commitTx, err := CreateCommitTx(lc.fundingTxIn, selfKey, remoteKey, revocationKey, delay, delayBalance, p2wkhBalance) if err != nil { return nil, err @@ -722,11 +744,8 @@ func (lc *LightningChannel) SignNextCommitment() ([]byte, uint32, error) { })) // Sign their version of the new commitment transaction. - hashCache := txscript.NewTxSigHashes(newCommitView.txn) - sig, err := txscript.RawTxInWitnessSignature(newCommitView.txn, - hashCache, 0, int64(lc.channelState.Capacity), - lc.channelState.FundingRedeemScript, txscript.SigHashAll, - lc.channelState.OurMultiSigKey) + lc.signDesc.SigHashes = txscript.NewTxSigHashes(newCommitView.txn) + sig, err := lc.signer.SignOutputRaw(newCommitView.txn, lc.signDesc) if err != nil { return nil, 0, err } @@ -744,7 +763,7 @@ func (lc *LightningChannel) SignNextCommitment() ([]byte, uint32, error) { // Strip off the sighash flag on the signature in order to send it over // the wire. - return sig[:len(sig)], lc.theirLogCounter, nil + return sig, lc.theirLogCounter, nil } // ReceiveNewCommitment processs a signature for a new commitment state sent by @@ -770,7 +789,7 @@ func (lc *LightningChannel) ReceiveNewCommitment(rawSig []byte, if err != nil { return err } - revocationKey := deriveRevocationPubkey(theirCommitKey, revocation[:]) + revocationKey := DeriveRevocationPubkey(theirCommitKey, revocation[:]) revocationHash := fastsha256.Sum256(revocation[:]) // With the revocation information calculated, construct the new @@ -857,7 +876,7 @@ func (lc *LightningChannel) RevokeCurrentCommitment() (*lnwire.CommitRevocation, if err != nil { return nil, err } - revocationMsg.NextRevocationKey = deriveRevocationPubkey(theirCommitKey, + revocationMsg.NextRevocationKey = DeriveRevocationPubkey(theirCommitKey, revocationEdge[:]) revocationMsg.NextRevocationHash = fastsha256.Sum256(revocationEdge[:]) @@ -921,8 +940,8 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.CommitRevocation) ( // Verify that the revocation public key we can derive using this // pre-image and our private key is identical to the revocation key we // were given for their current (prior) commitment transaction. - revocationPriv := deriveRevocationPrivKey(ourCommitKey, pendingRevocation[:]) - if !revocationPriv.PubKey().IsEqual(currentRevocationKey) { + revocationPub := DeriveRevocationPubkey(ourCommitKey, pendingRevocation[:]) + if !revocationPub.IsEqual(currentRevocationKey) { return nil, fmt.Errorf("revocation key mismatch") } @@ -1067,7 +1086,7 @@ func (lc *LightningChannel) ExtendRevocationWindow() (*lnwire.CommitRevocation, } theirCommitKey := lc.channelState.TheirCommitKey - revMsg.NextRevocationKey = deriveRevocationPubkey(theirCommitKey, + revMsg.NextRevocationKey = DeriveRevocationPubkey(theirCommitKey, revocation[:]) revMsg.NextRevocationHash = fastsha256.Sum256(revocation[:]) @@ -1201,7 +1220,7 @@ func (lc *LightningChannel) addHTLC(commitTx *wire.MsgTx, ourCommit bool, paymentDesc *PaymentDescriptor, revocation [32]byte, delay uint32, isIncoming bool) error { - localKey := lc.channelState.OurCommitKey.PubKey() + localKey := lc.channelState.OurCommitKey remoteKey := lc.channelState.TheirCommitKey timeout := paymentDesc.Timeout rHash := paymentDesc.RHash @@ -1288,7 +1307,7 @@ func (lc *LightningChannel) InitCooperativeClose() ([]byte, *wire.ShaHash, error lc.status = channelClosing // TODO(roasbeef): assumes initiator pays fees - closeTx := createCooperativeCloseTx(lc.fundingTxIn, + closeTx := CreateCooperativeCloseTx(lc.fundingTxIn, lc.channelState.OurBalance, lc.channelState.TheirBalance, lc.channelState.OurDeliveryScript, lc.channelState.TheirDeliveryScript, true) @@ -1298,11 +1317,8 @@ func (lc *LightningChannel) InitCooperativeClose() ([]byte, *wire.ShaHash, error // initiator we'll simply send our signature over the the remote party, // using the generated txid to be notified once the closure transaction // has been confirmed. - hashCache := txscript.NewTxSigHashes(closeTx) - closeSig, err := txscript.RawTxInWitnessSignature(closeTx, - hashCache, 0, int64(lc.channelState.Capacity), - lc.channelState.FundingRedeemScript, txscript.SigHashAll, - lc.channelState.OurMultiSigKey) + lc.signDesc.SigHashes = txscript.NewTxSigHashes(closeTx) + closeSig, err := lc.signer.SignOutputRaw(closeTx, lc.signDesc) if err != nil { return nil, nil, err } @@ -1315,6 +1331,9 @@ func (lc *LightningChannel) InitCooperativeClose() ([]byte, *wire.ShaHash, error // remote node initating a cooperative channel closure. A fully signed closure // transaction is returned. It is the duty of the responding node to broadcast // a signed+valid closure transaction to the network. +// +// NOTE: The passed remote sig is expected to the a fully complete signature +// including the proper sighash byte. func (lc *LightningChannel) CompleteCooperativeClose(remoteSig []byte) (*wire.MsgTx, error) { lc.Lock() defer lc.Unlock() @@ -1330,35 +1349,34 @@ func (lc *LightningChannel) CompleteCooperativeClose(remoteSig []byte) (*wire.Ms // Create the transaction used to return the current settled balance // on this active channel back to both parties. In this current model, // the initiator pays full fees for the cooperative close transaction. - closeTx := createCooperativeCloseTx(lc.fundingTxIn, + closeTx := CreateCooperativeCloseTx(lc.fundingTxIn, lc.channelState.OurBalance, lc.channelState.TheirBalance, lc.channelState.OurDeliveryScript, lc.channelState.TheirDeliveryScript, false) // With the transaction created, we can finally generate our half of // the 2-of-2 multi-sig needed to redeem the funding output. - redeemScript := lc.channelState.FundingRedeemScript hashCache := txscript.NewTxSigHashes(closeTx) - capacity := int64(lc.channelState.Capacity) - closeSig, err := txscript.RawTxInWitnessSignature(closeTx, - hashCache, 0, capacity, redeemScript, txscript.SigHashAll, - lc.channelState.OurMultiSigKey) + lc.signDesc.SigHashes = hashCache + closeSig, err := lc.signer.SignOutputRaw(closeTx, lc.signDesc) if err != nil { return nil, err } // Finally, construct the witness stack minding the order of the // pubkeys+sigs on the stack. - ourKey := lc.channelState.OurMultiSigKey.PubKey().SerializeCompressed() + ourKey := lc.channelState.OurMultiSigKey.SerializeCompressed() theirKey := lc.channelState.TheirMultiSigKey.SerializeCompressed() - witness := spendMultiSig(redeemScript, ourKey, closeSig, + ourSig := append(closeSig, byte(txscript.SigHashAll)) + witness := SpendMultiSig(lc.signDesc.RedeemScript, ourKey, ourSig, theirKey, remoteSig) closeTx.TxIn[0].Witness = witness // Validate the finalized transaction to ensure the output script is // properly met, and that the remote peer supplied a valid signature. vm, err := txscript.NewEngine(lc.fundingP2WSH, closeTx, 0, - txscript.StandardVerifyFlags, nil, hashCache, capacity) + txscript.StandardVerifyFlags, nil, hashCache, + int64(lc.channelState.Capacity)) if err != nil { return nil, err } @@ -1376,7 +1394,8 @@ func (lc *LightningChannel) DeleteState() error { return lc.channelState.CloseChannel() } -// StateSnapshot returns a snapshot b +// StateSnapshot returns a snapshot of the current fully committed state within +// the channel. func (lc *LightningChannel) StateSnapshot() *channeldb.ChannelSnapshot { lc.stateMtx.RLock() defer lc.stateMtx.RUnlock() @@ -1384,12 +1403,12 @@ func (lc *LightningChannel) StateSnapshot() *channeldb.ChannelSnapshot { return lc.channelState.Snapshot() } -// createCommitTx creates a commitment transaction, spending from specified +// CreateCommitTx creates a commitment transaction, spending from specified // funding output. The commitment transaction contains two outputs: one paying // to the "owner" of the commitment transaction which can be spent after a // relative block delay or revocation event, and the other paying the the // counter-party within the channel, which can be spent immediately. -func createCommitTx(fundingOutput *wire.TxIn, selfKey, theirKey *btcec.PublicKey, +func CreateCommitTx(fundingOutput *wire.TxIn, selfKey, theirKey *btcec.PublicKey, revokeKey *btcec.PublicKey, csvTimeout uint32, amountToSelf, amountToThem btcutil.Amount) (*wire.MsgTx, error) { @@ -1433,13 +1452,13 @@ func createCommitTx(fundingOutput *wire.TxIn, selfKey, theirKey *btcec.PublicKey return commitTx, nil } -// createCooperativeCloseTx creates a transaction which if signed by both +// CreateCooperativeCloseTx creates a transaction which if signed by both // parties, then broadcast cooperatively closes an active channel. The creation // of the closure transaction is modified by a boolean indicating if the party // constructing the channel is the initiator of the closure. Currently it is // expected that the initiator pays the transaction fees for the closing // transaction in full. -func createCooperativeCloseTx(fundingTxIn *wire.TxIn, +func CreateCooperativeCloseTx(fundingTxIn *wire.TxIn, ourBalance, theirBalance btcutil.Amount, ourDeliveryScript, theirDeliveryScript []byte, initiator bool) *wire.MsgTx { diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 8a778b01..588ecbfd 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -13,27 +13,64 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcd/txscript" "github.com/roasbeef/btcd/wire" "github.com/roasbeef/btcutil" ) -// MockEncryptorDecryptor is a mock implementation of EncryptorDecryptor that -// simply returns the passed bytes without encrypting or decrypting. This is -// used for testing purposes to be able to create a channldb instance which -// doesn't use encryption. -type MockEncryptorDecryptor struct { +var ( + privPass = []byte("private-test") + + // For simplicity a single priv key controls all of our test outputs. + testWalletPrivKey = []byte{ + 0x2b, 0xd8, 0x06, 0xc9, 0x7f, 0x0e, 0x00, 0xaf, + 0x1a, 0x1f, 0xc3, 0x32, 0x8f, 0xa7, 0x63, 0xa9, + 0x26, 0x97, 0x23, 0xc8, 0xdb, 0x8f, 0xac, 0x4f, + 0x93, 0xaf, 0x71, 0xdb, 0x18, 0x6d, 0x6e, 0x90, + } + + // We're alice :) + bobsPrivKey = []byte{ + 0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda, + 0x63, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17, + 0xd, 0xe7, 0x95, 0xe4, 0xb7, 0x25, 0xb8, 0x4d, + 0x1e, 0xb, 0x4c, 0xfd, 0x9e, 0xc5, 0x8c, 0xe9, + } + + // Use a hard-coded HD seed. + testHdSeed = [32]byte{ + 0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab, + 0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4, + 0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9, + 0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53, + } + + // The number of confirmations required to consider any created channel + // open. + numReqConfs = uint16(1) +) + +type mockSigner struct { + key *btcec.PrivateKey } -func (m *MockEncryptorDecryptor) Encrypt(n []byte) ([]byte, error) { - return n, nil +func (m *mockSigner) SignOutputRaw(tx *wire.MsgTx, signDesc *SignDescriptor) ([]byte, error) { + amt := signDesc.Output.Value + redeemScript := signDesc.RedeemScript + privKey := m.key + + sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes, + signDesc.InputIndex, amt, redeemScript, txscript.SigHashAll, privKey) + if err != nil { + return nil, err + } + + return sig[:len(sig)-1], nil } -func (m *MockEncryptorDecryptor) Decrypt(n []byte) ([]byte, error) { - return n, nil -} - -func (m *MockEncryptorDecryptor) OverheadSize() uint32 { - return 0 +// ComputeInputScript... +func (m *mockSigner) ComputeInputScript(tx *wire.MsgTx, signDesc *SignDescriptor) (*InputScript, error) { + return nil, nil } // createTestChannels creates two test channels funded with 10 BTC, with 5 BTC @@ -49,7 +86,7 @@ func createTestChannels() (*LightningChannel, *LightningChannel, func(), error) csvTimeoutAlice := uint32(5) csvTimeoutBob := uint32(4) - redeemScript, _, err := genFundingPkScript(aliceKeyPub.SerializeCompressed(), + redeemScript, _, err := GenFundingPkScript(aliceKeyPub.SerializeCompressed(), bobKeyPub.SerializeCompressed(), int64(channelCapacity)) if err != nil { return nil, nil, nil, err @@ -61,26 +98,26 @@ func createTestChannels() (*LightningChannel, *LightningChannel, func(), error) } fundingTxIn := wire.NewTxIn(prevOut, nil, nil) - bobElkrem := elkrem.NewElkremSender(deriveElkremRoot(bobKeyPriv, aliceKeyPub)) + bobElkrem := elkrem.NewElkremSender(deriveElkremRoot(bobKeyPriv, bobKeyPub, aliceKeyPub)) bobFirstRevoke, err := bobElkrem.AtIndex(0) if err != nil { return nil, nil, nil, err } - bobRevokeKey := deriveRevocationPubkey(aliceKeyPub, bobFirstRevoke[:]) + bobRevokeKey := DeriveRevocationPubkey(aliceKeyPub, bobFirstRevoke[:]) - aliceElkrem := elkrem.NewElkremSender(deriveElkremRoot(aliceKeyPriv, bobKeyPub)) + aliceElkrem := elkrem.NewElkremSender(deriveElkremRoot(aliceKeyPriv, aliceKeyPub, bobKeyPub)) aliceFirstRevoke, err := aliceElkrem.AtIndex(0) if err != nil { return nil, nil, nil, err } - aliceRevokeKey := deriveRevocationPubkey(bobKeyPub, aliceFirstRevoke[:]) + aliceRevokeKey := DeriveRevocationPubkey(bobKeyPub, aliceFirstRevoke[:]) - aliceCommitTx, err := createCommitTx(fundingTxIn, aliceKeyPub, + aliceCommitTx, err := CreateCommitTx(fundingTxIn, aliceKeyPub, bobKeyPub, aliceRevokeKey, csvTimeoutAlice, channelBal, channelBal) if err != nil { return nil, nil, nil, err } - bobCommitTx, err := createCommitTx(fundingTxIn, bobKeyPub, + bobCommitTx, err := CreateCommitTx(fundingTxIn, bobKeyPub, aliceKeyPub, bobRevokeKey, csvTimeoutBob, channelBal, channelBal) if err != nil { return nil, nil, nil, err @@ -91,25 +128,24 @@ func createTestChannels() (*LightningChannel, *LightningChannel, func(), error) if err != nil { return nil, nil, nil, err } - dbAlice.RegisterCryptoSystem(&MockEncryptorDecryptor{}) + bobPath, err := ioutil.TempDir("", "bobdb") dbBob, err := channeldb.Open(bobPath, &chaincfg.TestNet3Params) if err != nil { return nil, nil, nil, err } - dbBob.RegisterCryptoSystem(&MockEncryptorDecryptor{}) aliceChannelState := &channeldb.OpenChannel{ TheirLNID: testHdSeed, ChanID: prevOut, - OurCommitKey: aliceKeyPriv, + OurCommitKey: aliceKeyPub, TheirCommitKey: bobKeyPub, Capacity: channelCapacity, OurBalance: channelBal, TheirBalance: channelBal, OurCommitTx: aliceCommitTx, FundingOutpoint: prevOut, - OurMultiSigKey: aliceKeyPriv, + OurMultiSigKey: aliceKeyPub, TheirMultiSigKey: bobKeyPub, FundingRedeemScript: redeemScript, LocalCsvDelay: csvTimeoutAlice, @@ -122,14 +158,14 @@ func createTestChannels() (*LightningChannel, *LightningChannel, func(), error) bobChannelState := &channeldb.OpenChannel{ TheirLNID: testHdSeed, ChanID: prevOut, - OurCommitKey: bobKeyPriv, + OurCommitKey: bobKeyPub, TheirCommitKey: aliceKeyPub, Capacity: channelCapacity, OurBalance: channelBal, TheirBalance: channelBal, OurCommitTx: bobCommitTx, FundingOutpoint: prevOut, - OurMultiSigKey: bobKeyPriv, + OurMultiSigKey: bobKeyPub, TheirMultiSigKey: aliceKeyPub, FundingRedeemScript: redeemScript, LocalCsvDelay: csvTimeoutBob, @@ -145,11 +181,14 @@ func createTestChannels() (*LightningChannel, *LightningChannel, func(), error) os.RemoveAll(alicePath) } - channelAlice, err := NewLightningChannel(nil, nil, dbAlice, aliceChannelState) + aliceSigner := &mockSigner{aliceKeyPriv} + bobSigner := &mockSigner{bobKeyPriv} + + channelAlice, err := NewLightningChannel(aliceSigner, nil, nil, aliceChannelState) if err != nil { return nil, nil, nil, err } - channelBob, err := NewLightningChannel(nil, nil, dbBob, bobChannelState) + channelBob, err := NewLightningChannel(bobSigner, nil, nil, bobChannelState) if err != nil { return nil, nil, nil, err } @@ -456,7 +495,8 @@ func TestCooperativeChannelClosure(t *testing.T) { if err != nil { t.Fatalf("unable to initiate alice cooperative close: %v", err) } - closeTx, err := bobChannel.CompleteCooperativeClose(sig) + finalSig := append(sig, byte(txscript.SigHashAll)) + closeTx, err := bobChannel.CompleteCooperativeClose(finalSig) if err != nil { t.Fatalf("unable to complete alice cooperative close: %v", err) } @@ -475,7 +515,8 @@ func TestCooperativeChannelClosure(t *testing.T) { if err != nil { t.Fatalf("unable to initiate bob cooperative close: %v", err) } - closeTx, err = aliceChannel.CompleteCooperativeClose(sig) + finalSig = append(sig, byte(txscript.SigHashAll)) + closeTx, err = aliceChannel.CompleteCooperativeClose(finalSig) if err != nil { t.Fatalf("unable to complete bob cooperative close: %v", err) } diff --git a/lnwallet/wallet_test.go b/lnwallet/interface_test.go similarity index 77% rename from lnwallet/wallet_test.go rename to lnwallet/interface_test.go index 330401c9..87890801 100644 --- a/lnwallet/wallet_test.go +++ b/lnwallet/interface_test.go @@ -1,4 +1,4 @@ -package lnwallet +package lnwallet_test import ( "bytes" @@ -11,18 +11,20 @@ import ( "time" "github.com/boltdb/bolt" + "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/roasbeef/btcd/chaincfg" "github.com/roasbeef/btcutil/txsort" + _ "github.com/roasbeef/btcwallet/walletdb/bdb" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/rpctest" "github.com/roasbeef/btcd/txscript" "github.com/roasbeef/btcd/wire" "github.com/roasbeef/btcutil" - "github.com/roasbeef/btcutil/coinset" - "github.com/roasbeef/btcwallet/waddrmgr" ) var ( @@ -60,8 +62,8 @@ var ( // assertProperBalance asserts than the total value of the unspent outputs // within the wallet are *exactly* amount. If unable to retrieve the current // balance, or the assertion fails, the test will halt with a fatal error. -func assertProperBalance(t *testing.T, lw *LightningWallet, numConfirms int32, amount int64) { - balance, err := lw.CalculateBalance(numConfirms) +func assertProperBalance(t *testing.T, lw *lnwallet.LightningWallet, numConfirms int32, amount int64) { + balance, err := lw.ConfirmedBalance(numConfirms, false) if err != nil { t.Fatalf("unable to query for balance: %v", err) } @@ -72,7 +74,7 @@ func assertProperBalance(t *testing.T, lw *LightningWallet, numConfirms int32, a } func assertChannelOpen(t *testing.T, miner *rpctest.Harness, numConfs uint32, - c <-chan *LightningChannel) *LightningChannel { + c <-chan *lnwallet.LightningChannel) *lnwallet.LightningChannel { // Mine a single block. After this block is mined, the channel should // be considered fully open. if _, err := miner.Node.Generate(1); err != nil { @@ -108,9 +110,9 @@ type bobNode struct { // Contribution returns bobNode's contribution necessary to open a payment // channel with Alice. -func (b *bobNode) Contribution(aliceCommitKey *btcec.PublicKey) *ChannelContribution { - revokeKey := deriveRevocationPubkey(aliceCommitKey, b.revocation[:]) - return &ChannelContribution{ +func (b *bobNode) Contribution(aliceCommitKey *btcec.PublicKey) *lnwallet.ChannelContribution { + revokeKey := lnwallet.DeriveRevocationPubkey(aliceCommitKey, b.revocation[:]) + return &lnwallet.ChannelContribution{ FundingAmount: b.fundingAmt, Inputs: b.availableOutputs, ChangeOutputs: b.changeOutputs, @@ -124,9 +126,9 @@ func (b *bobNode) Contribution(aliceCommitKey *btcec.PublicKey) *ChannelContribu // SingleContribution returns bobNode's contribution to a single funded // channel. This contribution contains no inputs nor change outputs. -func (b *bobNode) SingleContribution(aliceCommitKey *btcec.PublicKey) *ChannelContribution { - revokeKey := deriveRevocationPubkey(aliceCommitKey, b.revocation[:]) - return &ChannelContribution{ +func (b *bobNode) SingleContribution(aliceCommitKey *btcec.PublicKey) *lnwallet.ChannelContribution { + revokeKey := lnwallet.DeriveRevocationPubkey(aliceCommitKey, b.revocation[:]) + return &lnwallet.ChannelContribution{ FundingAmount: b.fundingAmt, MultiSigKey: b.channelKey, CommitKey: b.channelKey, @@ -139,8 +141,8 @@ func (b *bobNode) SingleContribution(aliceCommitKey *btcec.PublicKey) *ChannelCo // signFundingTx generates signatures for all the inputs in the funding tx // belonging to Bob. // NOTE: This generates the full witness stack. -func (b *bobNode) signFundingTx(fundingTx *wire.MsgTx) ([]*InputScript, error) { - bobInputScripts := make([]*InputScript, 0, len(b.availableOutputs)) +func (b *bobNode) signFundingTx(fundingTx *wire.MsgTx) ([]*lnwallet.InputScript, error) { + bobInputScripts := make([]*lnwallet.InputScript, 0, len(b.availableOutputs)) bobPkScript := b.changeOutputs[0].PkScript inputValue := int64(7e8) @@ -158,7 +160,7 @@ func (b *bobNode) signFundingTx(fundingTx *wire.MsgTx) ([]*InputScript, error) { return nil, err } - inputScript := &InputScript{Witness: witness} + inputScript := &lnwallet.InputScript{Witness: witness} bobInputScripts = append(bobInputScripts, inputScript) } @@ -217,7 +219,7 @@ func newBobNode(miner *rpctest.Harness, amt btcutil.Amount) (*bobNode, error) { if err != nil { return nil, err } - found, index := findScriptOutputIndex(tx.MsgTx(), bobAddrScript) + found, index := lnwallet.FindScriptOutputIndex(tx.MsgTx(), bobAddrScript) if !found { return nil, fmt.Errorf("output to bob never created") } @@ -251,14 +253,14 @@ func newBobNode(miner *rpctest.Harness, amt btcutil.Amount) (*bobNode, error) { }, nil } -func loadTestCredits(miner *rpctest.Harness, w *LightningWallet, numOutputs, btcPerOutput int) error { +func loadTestCredits(miner *rpctest.Harness, w *lnwallet.LightningWallet, numOutputs, btcPerOutput int) error { // Using the mining node, spend from a coinbase output numOutputs to // give us btcPerOutput with each output. satoshiPerOutput := int64(btcPerOutput * 1e8) addrs := make([]btcutil.Address, 0, numOutputs) for i := 0; i < numOutputs; i++ { // Grab a fresh address from the wallet to house this output. - walletAddr, err := w.NewAddress(waddrmgr.DefaultAccountNum, waddrmgr.WitnessPubKey) + walletAddr, err := w.NewAddress(lnwallet.WitnessPubKey, false) if err != nil { return err } @@ -285,87 +287,59 @@ func loadTestCredits(miner *rpctest.Harness, w *LightningWallet, numOutputs, btc return err } - _, bestHeight, err := miner.Node.GetBestBlock() - if err != nil { - return err - } - // Wait until the wallet has finished syncing up to the main chain. ticker := time.NewTicker(100 * time.Millisecond) + expectedBalance := btcutil.Amount(satoshiPerOutput * int64(numOutputs)) out: for { select { case <-ticker.C: - if w.Manager.SyncedTo().Height == bestHeight { + balance, err := w.ConfirmedBalance(1, false) + if err != nil { + return err + } + if balance == expectedBalance { break out } } } ticker.Stop() - // Trigger a re-scan to ensure the wallet knows of the newly created - // outputs it can spend. - if err := w.Rescan(addrs, nil); err != nil { - return err - } - return nil } // createTestWallet creates a test LightningWallet will a total of 20BTC // available for funding channels. -func createTestWallet(miningNode *rpctest.Harness, netParams *chaincfg.Params) (string, *LightningWallet, error) { - privPass := []byte("private-test") - tempTestDir, err := ioutil.TempDir("", "lnwallet") - if err != nil { - return "", nil, nil - } - - rpcConfig := miningNode.RPCConfig() - config := &Config{ - PrivatePass: privPass, - HdSeed: testHdSeed[:], - DataDir: tempTestDir, - NetParams: netParams, - RpcHost: rpcConfig.Host, - RpcUser: rpcConfig.User, - RpcPass: rpcConfig.Pass, - CACert: rpcConfig.Certificates, - } +func createTestWallet(tempTestDir string, miningNode *rpctest.Harness, + netParams *chaincfg.Params, notifier chainntnfs.ChainNotifier, + wc lnwallet.WalletController, signer lnwallet.Signer, + bio lnwallet.BlockChainIO) (*lnwallet.LightningWallet, error) { dbDir := filepath.Join(tempTestDir, "cdb") cdb, err := channeldb.Open(dbDir, &chaincfg.SegNet4Params) if err != nil { - return "", nil, err + return nil, err } - chainNotifier, err := btcdnotify.New(&rpcConfig) + wallet, err := lnwallet.NewLightningWallet(cdb, notifier, wc, signer, + bio, netParams) if err != nil { - return "", nil, err - } - if err := chainNotifier.Start(); err != nil { - return "", nil, err + return nil, err } - wallet, err := NewLightningWallet(config, cdb, chainNotifier) - if err != nil { - return "", nil, err - } if err := wallet.Startup(); err != nil { - return "", nil, err + return nil, err } - cdb.RegisterCryptoSystem(&WaddrmgrEncryptorDecryptor{wallet.Manager}) - - // Load our test wallet with 10 outputs each holding 4BTC. - if err := loadTestCredits(miningNode, wallet, 10, 4); err != nil { - return "", nil, err + // Load our test wallet with 20 outputs each holding 4BTC. + if err := loadTestCredits(miningNode, wallet, 20, 4); err != nil { + return nil, err } - return tempTestDir, wallet, nil + return wallet, nil } -func testDualFundingReservationWorkflow(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testDualFundingReservationWorkflow(miner *rpctest.Harness, wallet *lnwallet.LightningWallet, t *testing.T) { // Create the bob-test wallet which will be the other side of our funding // channel. fundingAmount := btcutil.Amount(5 * 1e8) @@ -376,7 +350,7 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, lnwallet *Lightn // Bob initiates a channel funded with 5 BTC for each side, so 10 // BTC total. He also generates 2 BTC in change. - chanReservation, err := lnwallet.InitChannelReservation(fundingAmount*2, + chanReservation, err := wallet.InitChannelReservation(fundingAmount*2, fundingAmount, bobNode.id, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation: %v", err) @@ -426,10 +400,13 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, lnwallet *Lightn if ourContribution.RevocationKey == nil { t.Fatalf("alice's revocation key not found") } + // Additionally, the funding tx should have been populated. - if chanReservation.fundingTx == nil { + fundingTx := chanReservation.FinalFundingTx() + if fundingTx == nil { t.Fatalf("funding transaction never created!") } + // Their funds should also be filled in. if len(theirContribution.Inputs) != 1 { t.Fatalf("bob's outputs for funding tx not properly selected, have %v "+ @@ -455,13 +432,13 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, lnwallet *Lightn // Alice responds with her output, change addr, multi-sig key and signatures. // Bob then responds with his signatures. - bobsSigs, err := bobNode.signFundingTx(chanReservation.fundingTx) + bobsSigs, err := bobNode.signFundingTx(fundingTx) if err != nil { t.Fatalf("unable to sign inputs for bob: %v", err) } commitSig, err := bobNode.signCommitTx( - chanReservation.partialState.OurCommitTx, - chanReservation.partialState.FundingRedeemScript, + chanReservation.LocalCommitTx(), + chanReservation.FundingRedeemScript(), 10e8) if err != nil { t.Fatalf("bob is unable to sign alice's commit tx: %v", err) @@ -474,10 +451,9 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, lnwallet *Lightn // txn hits a "comfortable" depth. // The resulting active channel state should have been persisted to the DB. - fundingTx := chanReservation.FinalFundingTx() fundingSha := fundingTx.TxSha() nodeID := wire.ShaHash(bobNode.id) - channels, err := lnwallet.channelDB.FetchOpenChannels(&nodeID) + channels, err := wallet.ChannelDB.FetchOpenChannels(&nodeID) if err != nil { t.Fatalf("unable to retrieve channel from DB: %v", err) } @@ -495,46 +471,49 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness, lnwallet *Lightn if err != nil { t.Fatalf("unable to init cooperative closure: %v", err) } + aliceCloseSig = append(aliceCloseSig, byte(txscript.SigHashAll)) + + chanInfo := lnc.StateSnapshot() // Obtain bob's signature for the closure transaction. - redeemScript := lnc.channelState.FundingRedeemScript + redeemScript := lnc.FundingRedeemScript fundingOut := lnc.ChannelPoint() fundingTxIn := wire.NewTxIn(fundingOut, nil, nil) - bobCloseTx := createCooperativeCloseTx(fundingTxIn, - lnc.channelState.TheirBalance, lnc.channelState.OurBalance, - lnc.channelState.TheirDeliveryScript, lnc.channelState.OurDeliveryScript, + bobCloseTx := lnwallet.CreateCooperativeCloseTx(fundingTxIn, + chanInfo.RemoteBalance, chanInfo.LocalBalance, + lnc.RemoteDeliveryScript, lnc.LocalDeliveryScript, false) - bobSig, err := bobNode.signCommitTx(bobCloseTx, - redeemScript, - int64(lnc.channelState.Capacity)) + bobSig, err := bobNode.signCommitTx(bobCloseTx, redeemScript, int64(lnc.Capacity)) if err != nil { t.Fatalf("unable to generate bob's signature for closing tx: %v", err) } // Broadcast the transaction to the network. This transaction should // be accepted, and found in the next mined block. - ourKey := lnc.channelState.OurMultiSigKey.PubKey().SerializeCompressed() - theirKey := lnc.channelState.TheirMultiSigKey.SerializeCompressed() - witness := spendMultiSig(redeemScript, ourKey, aliceCloseSig, + ourKey := chanReservation.OurContribution().MultiSigKey.SerializeCompressed() + theirKey := chanReservation.TheirContribution().MultiSigKey.SerializeCompressed() + witness := lnwallet.SpendMultiSig(redeemScript, ourKey, aliceCloseSig, theirKey, bobSig) bobCloseTx.TxIn[0].Witness = witness - if err := lnwallet.PublishTransaction(bobCloseTx); err != nil { + if err := wallet.PublishTransaction(bobCloseTx); err != nil { t.Fatalf("broadcast of close tx rejected: %v", err) } } -func testFundingTransactionLockedOutputs(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testFundingTransactionLockedOutputs(miner *rpctest.Harness, + wallet *lnwallet.LightningWallet, t *testing.T) { + // Create two channels, both asking for 8 BTC each, totalling 16 // BTC. // TODO(roasbeef): tests for concurrent funding. // * also func for below fundingAmount := btcutil.Amount(8 * 1e8) - chanReservation1, err := lnwallet.InitChannelReservation(fundingAmount, + chanReservation1, err := wallet.InitChannelReservation(fundingAmount, fundingAmount, testHdSeed, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation 1: %v", err) } - chanReservation2, err := lnwallet.InitChannelReservation(fundingAmount, + chanReservation2, err := wallet.InitChannelReservation(fundingAmount, fundingAmount, testHdSeed, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation 2: %v", err) @@ -563,12 +542,12 @@ func testFundingTransactionLockedOutputs(miner *rpctest.Harness, lnwallet *Light // 90 BTC. We only have around 24BTC worth of outpoints that aren't locked, so // this should fail. amt := btcutil.Amount(90 * 1e8) - failedReservation, err := lnwallet.InitChannelReservation(amt, amt, + failedReservation, err := wallet.InitChannelReservation(amt, amt, testHdSeed, numReqConfs, 4) if err == nil { t.Fatalf("not error returned, should fail on coin selection") } - if err != coinset.ErrCoinsNoSelectionAvailable { + if err != lnwallet.ErrInsufficientFunds { t.Fatalf("error not coinselect error: %v", err) } if failedReservation != nil { @@ -576,28 +555,30 @@ func testFundingTransactionLockedOutputs(miner *rpctest.Harness, lnwallet *Light } } -func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { - // Create a reservation for 22 BTC. - fundingAmount := btcutil.Amount(22 * 1e8) - chanReservation, err := lnwallet.InitChannelReservation(fundingAmount, +func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness, + wallet *lnwallet.LightningWallet, t *testing.T) { + + // Create a reservation for 44 BTC. + fundingAmount := btcutil.Amount(44 * 1e8) + chanReservation, err := wallet.InitChannelReservation(fundingAmount, fundingAmount, testHdSeed, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation: %v", err) } - // There should be three locked outpoints. - lockedOutPoints := lnwallet.LockedOutpoints() - if len(lockedOutPoints) != 6 { + // There should be 12 locked outpoints. + lockedOutPoints := wallet.LockedOutpoints() + if len(lockedOutPoints) != 12 { t.Fatalf("two outpoints should now be locked, instead %v are", len(lockedOutPoints)) } - // Attempt to create another channel with 22 BTC, this should fail. - failedReservation, err := lnwallet.InitChannelReservation(fundingAmount, + // Attempt to create another channel with 44 BTC, this should fail. + _, err = wallet.InitChannelReservation(fundingAmount, fundingAmount, testHdSeed, numReqConfs, 4) - if err != coinset.ErrCoinsNoSelectionAvailable { - t.Fatalf("coin selection succeded should have insufficient funds: %+v", - failedReservation) + if err != lnwallet.ErrInsufficientFunds { + t.Fatalf("coin selection succeded should have insufficient funds: %v", + err) } // Now cancel that old reservation. @@ -606,15 +587,16 @@ func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness, lnwallet *Lig } // Those outpoints should no longer be locked. - lockedOutPoints = lnwallet.LockedOutpoints() + lockedOutPoints = wallet.LockedOutpoints() if len(lockedOutPoints) != 0 { t.Fatalf("outpoints still locked") } - // Reservation ID should now longer be tracked. - _, ok := lnwallet.fundingLimbo[chanReservation.reservationID] - if ok { - t.Fatalf("funding reservation still in map") + // Reservation ID should no longer be tracked. + numReservations := wallet.ActiveReservations() + if len(wallet.ActiveReservations()) != 0 { + t.Fatalf("should have 0 reservations, instead have %v", + numReservations) } // TODO(roasbeef): create method like Balance that ignores locked @@ -622,16 +604,18 @@ func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness, lnwallet *Lig // attempting coin selection. // Request to fund a new channel should now succeeed. - _, err = lnwallet.InitChannelReservation(fundingAmount, fundingAmount, + _, err = wallet.InitChannelReservation(fundingAmount, fundingAmount, testHdSeed, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation: %v", err) } } -func testCancelNonExistantReservation(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testCancelNonExistantReservation(miner *rpctest.Harness, + wallet *lnwallet.LightningWallet, t *testing.T) { + // Create our own reservation, give it some ID. - res := newChannelReservation(1000, 1000, 5000, lnwallet, 22, numReqConfs) + res := lnwallet.NewChannelReservation(1000, 1000, 5000, wallet, 22, numReqConfs) // Attempt to cancel this reservation. This should fail, we know // nothing of it. @@ -640,7 +624,9 @@ func testCancelNonExistantReservation(miner *rpctest.Harness, lnwallet *Lightnin } } -func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, + lnwallet *lnwallet.LightningWallet, t *testing.T) { + // For this scenario, we (lnwallet) will be the channel initiator while bob // will be the recipient. @@ -702,7 +688,7 @@ func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, lnwall t.Fatalf("commitment sig not found") } // Additionally, the funding tx should have been populated. - if chanReservation.fundingTx == nil { + if chanReservation.FinalFundingTx() == nil { t.Fatalf("funding transaction never created!") } // Their funds should also be filled in. @@ -737,8 +723,8 @@ func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, lnwall // Now Bob can generate a signature for our version of the commitment // transaction, allowing us to complete the reservation. bobCommitSig, err := bobNode.signCommitTx( - chanReservation.partialState.OurCommitTx, - chanReservation.partialState.FundingRedeemScript, + chanReservation.LocalCommitTx(), + chanReservation.FundingRedeemScript(), int64(fundingAmt)) if err != nil { t.Fatalf("bob is unable to sign alice's commit tx: %v", err) @@ -755,7 +741,7 @@ func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, lnwall fundingTx := chanReservation.FinalFundingTx() fundingSha := fundingTx.TxSha() nodeID := wire.ShaHash(bobNode.id) - channels, err := lnwallet.channelDB.FetchOpenChannels(&nodeID) + channels, err := lnwallet.ChannelDB.FetchOpenChannels(&nodeID) if err != nil { t.Fatalf("unable to retrieve channel from DB: %v", err) } @@ -768,7 +754,9 @@ func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, lnwall assertChannelOpen(t, miner, uint32(numReqConfs), chanReservation.DispatchChan()) } -func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, + wallet *lnwallet.LightningWallet, t *testing.T) { + // For this scenario, bob will initiate the channel, while we simply act as // the responder. capacity := btcutil.Amount(4 * 1e8) @@ -783,7 +771,7 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwall // Bob sends over a single funding request, so we allocate our // contribution and the necessary resources. fundingAmt := btcutil.Amount(0) - chanReservation, err := lnwallet.InitChannelReservation(capacity, + chanReservation, err := wallet.InitChannelReservation(capacity, fundingAmt, bobNode.id, numReqConfs, 4) if err != nil { t.Fatalf("unable to init channel reservation: %v", err) @@ -821,7 +809,7 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwall if err := chanReservation.ProcessSingleContribution(bobContribution); err != nil { t.Fatalf("unable to process bob's contribution: %v", err) } - if chanReservation.fundingTx != nil { + if chanReservation.FinalFundingTx() != nil { t.Fatalf("funding transaction populated!") } if len(bobContribution.Inputs) != 1 { @@ -848,7 +836,7 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwall t.Fatalf("bob's revocaiton key not found") } - fundingRedeemScript, multiOut, err := genFundingPkScript( + fundingRedeemScript, multiOut, err := lnwallet.GenFundingPkScript( ourContribution.MultiSigKey.SerializeCompressed(), bobContribution.MultiSigKey.SerializeCompressed(), int64(capacity)) @@ -872,11 +860,11 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwall // wallet so it can finalize the transaction by signing bob's commitment // transaction. fundingTxID := fundingTx.TxSha() - _, multiSigIndex := findScriptOutputIndex(fundingTx, multiOut.PkScript) + _, multiSigIndex := lnwallet.FindScriptOutputIndex(fundingTx, multiOut.PkScript) fundingOutpoint := wire.NewOutPoint(&fundingTxID, multiSigIndex) fundingTxIn := wire.NewTxIn(fundingOutpoint, nil, nil) - aliceCommitTx, err := createCommitTx(fundingTxIn, ourContribution.CommitKey, + aliceCommitTx, err := lnwallet.CreateCommitTx(fundingTxIn, ourContribution.CommitKey, bobContribution.CommitKey, ourContribution.RevocationKey, ourContribution.CsvDelay, 0, capacity) if err != nil { @@ -897,10 +885,9 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwall } // Alice should have saved the funding output. - if chanReservation.partialState.FundingOutpoint != fundingOutpoint { + if chanReservation.FundingOutpoint() != fundingOutpoint { t.Fatalf("funding outputs don't match: %#v vs %#v", - chanReservation.partialState.FundingOutpoint, - fundingOutpoint) + chanReservation.FundingOutpoint(), fundingOutpoint) } // Some period of time later, Bob presents us with an SPV proof @@ -913,13 +900,13 @@ func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, lnwall // TODO(roasbeef): bob verify alice's sig } -func testFundingReservationInvalidCounterpartySigs(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testFundingReservationInvalidCounterpartySigs(miner *rpctest.Harness, lnwallet *lnwallet.LightningWallet, t *testing.T) { } -func testFundingTransactionTxFees(miner *rpctest.Harness, lnwallet *LightningWallet, t *testing.T) { +func testFundingTransactionTxFees(miner *rpctest.Harness, lnwallet *lnwallet.LightningWallet, t *testing.T) { } -var walletTests = []func(miner *rpctest.Harness, w *LightningWallet, test *testing.T){ +var walletTests = []func(miner *rpctest.Harness, w *lnwallet.LightningWallet, test *testing.T){ testDualFundingReservationWorkflow, testSingleFunderReservationWorkflowInitiator, testSingleFunderReservationWorkflowResponder, @@ -934,20 +921,29 @@ var walletTests = []func(miner *rpctest.Harness, w *LightningWallet, test *testi } type testLnWallet struct { - lnwallet *LightningWallet + lnwallet *lnwallet.LightningWallet cleanUpFunc func() } -func clearWalletState(w *LightningWallet) error { - w.nextFundingID = 0 - w.fundingLimbo = make(map[uint64]*ChannelReservation) - w.ResetLockedOutpoints() - +func clearWalletState(w *lnwallet.LightningWallet) error { // TODO(roasbeef): should also restore outputs to original state. + w.ResetReservations() - return w.channelDB.Wipe() + return w.ChannelDB.Wipe() } +// TestInterfaces tests all registered interfaces with a unified set of tests +// which excersie each of the required methods found within the WalletController +// interface. +// +// NOTE: In the future, when additional implementations of the WalletController +// interface have been implemented, in order to ensure the new concrete +// implementation is automatically tested, two steps must be undertaken. First, +// one needs add a "non-captured" (_) import from the new sub-package. This +// import should trigger an init() method within the package which registeres +// the interface. Second, an additional case in the switch within the main loop +// below needs to be added which properly initializes the interface. +// // TODO(roasbeef): purge bobNode in favor of dual lnwallet's func TestLightningWallet(t *testing.T) { netParams := &chaincfg.SimNetParams @@ -965,27 +961,72 @@ func TestLightningWallet(t *testing.T) { t.Fatalf("unable to set up mining node: %v", err) } - // Funding via 10 outputs with 4BTC each. - testDir, lnwallet, err := createTestWallet(miningNode, netParams) + rpcConfig := miningNode.RPCConfig() + + chainNotifier, err := btcdnotify.New(&rpcConfig) if err != nil { - t.Fatalf("unable to create test ln wallet: %v", err) + t.Fatalf("unable to create notifier: %v", err) + } + if err := chainNotifier.Start(); err != nil { + t.Fatalf("unable to start notifier: %v", err) } - defer os.RemoveAll(testDir) - defer lnwallet.Shutdown() - // The wallet should now have 40BTC available for spending. - assertProperBalance(t, lnwallet, 1, 40) - - // Execute every test, clearing possibly mutated wallet state after - // each step. - for _, walletTest := range walletTests { - walletTest(miningNode, lnwallet, t) - - // TODO(roasbeef): possible reset mining node's chainstate to - // initial level, cleanly wipe buckets - if err := clearWalletState(lnwallet); err != nil && - err != bolt.ErrBucketNotFound { - t.Fatalf("unable to wipe wallet state: %v", err) + var bio lnwallet.BlockChainIO + var signer lnwallet.Signer + var wc lnwallet.WalletController + for _, walletDriver := range lnwallet.RegisteredWallets() { + tempTestDir, err := ioutil.TempDir("", "lnwallet") + if err != nil { + t.Fatalf("unable to create temp directory: %v", err) } + defer os.RemoveAll(tempTestDir) + + walletType := walletDriver.WalletType + switch walletType { + case "btcwallet": + btcwalletConfig := &btcwallet.Config{ + PrivatePass: privPass, + HdSeed: testHdSeed[:], + DataDir: tempTestDir, + NetParams: netParams, + RpcHost: rpcConfig.Host, + RpcUser: rpcConfig.User, + RpcPass: rpcConfig.Pass, + CACert: rpcConfig.Certificates, + } + wc, err = walletDriver.New(btcwalletConfig) + if err != nil { + t.Fatalf("unable to create btcwallet: %v", err) + } + signer = wc.(*btcwallet.BtcWallet) + bio = wc.(*btcwallet.BtcWallet) + default: + t.Fatalf("unknown wallet driver: %v", walletType) + } + + // Funding via 20 outputs with 4BTC each. + lnwallet, err := createTestWallet(tempTestDir, miningNode, netParams, + chainNotifier, wc, signer, bio) + if err != nil { + t.Fatalf("unable to create test ln wallet: %v", err) + } + + // The wallet should now have 80BTC available for spending. + assertProperBalance(t, lnwallet, 1, 80) + + // Execute every test, clearing possibly mutated wallet state after + // each step. + for _, walletTest := range walletTests { + walletTest(miningNode, lnwallet, t) + + // TODO(roasbeef): possible reset mining node's chainstate to + // initial level, cleanly wipe buckets + if err := clearWalletState(lnwallet); err != nil && + err != bolt.ErrBucketNotFound { + t.Fatalf("unable to wipe wallet state: %v", err) + } + } + + lnwallet.Shutdown() } } diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index c905e9e2..e59dbf97 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -88,7 +88,6 @@ type InputScript struct { // as a signature to our version of the commitment transaction. // * We then verify the validity of all signatures before considering the // channel "open". -// TODO(roasbeef): update with single funder description type ChannelReservation struct { // This mutex MUST be held when either reading or modifying any of the // fields below. @@ -131,11 +130,11 @@ type ChannelReservation struct { wallet *LightningWallet } -// newChannelReservation creates a new channel reservation. This function is +// NewChannelReservation creates a new channel reservation. This function is // used only internally by lnwallet. In order to concurrent safety, the creation // of all channel reservations should be carried out via the // lnwallet.InitChannelReservation interface. -func newChannelReservation(capacity, fundingAmt btcutil.Amount, minFeeRate btcutil.Amount, +func NewChannelReservation(capacity, fundingAmt btcutil.Amount, minFeeRate btcutil.Amount, wallet *LightningWallet, id uint64, numConfs uint16) *ChannelReservation { var ourBalance btcutil.Amount var theirBalance btcutil.Amount @@ -160,7 +159,7 @@ func newChannelReservation(capacity, fundingAmt btcutil.Amount, minFeeRate btcut OurBalance: ourBalance, TheirBalance: theirBalance, MinFeePerKb: minFeeRate, - Db: wallet.channelDB, + Db: wallet.ChannelDB, }, numConfsToOpen: numConfs, reservationID: id, @@ -315,6 +314,26 @@ func (r *ChannelReservation) FinalFundingTx() *wire.MsgTx { return r.fundingTx } +// FundingRedeemScript returns the fully populated funding redeem script. +// +// NOTE: This method will only return a non-nil value after either +// ProcesContribution or ProcessSingleContribution have been executed and +// returned without error. +func (r *ChannelReservation) FundingRedeemScript() []byte { + r.RLock() + defer r.RUnlock() + return r.partialState.FundingRedeemScript +} + +// LocalCommitTx returns the commitment transaction for the local node involved +// in this funding reservation. +func (r *ChannelReservation) LocalCommitTx() *wire.MsgTx { + r.RLock() + defer r.RUnlock() + + return r.partialState.OurCommitTx +} + // FundingOutpoint returns the outpoint of the funding transaction. // // NOTE: The pointer returned will only be set once the .ProcesContribution() @@ -346,6 +365,7 @@ func (r *ChannelReservation) Cancel() error { // transaction for this pending payment channel obtains the configured number // of confirmations. Once confirmations have been obtained, a fully initialized // LightningChannel instance is returned, allowing for channel updates. +// // NOTE: If this method is called before .CompleteReservation(), it will block // indefinitely. func (r *ChannelReservation) DispatchChan() <-chan *LightningChannel { diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 263e34e6..41c1894e 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -4,7 +4,6 @@ import ( "encoding/hex" "errors" "fmt" - "math" "sync" "sync/atomic" @@ -12,17 +11,14 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/elkrem" - "github.com/roasbeef/btcd/btcjson" + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcutil/hdkeychain" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/txscript" "github.com/roasbeef/btcd/wire" "github.com/roasbeef/btcutil" - "github.com/roasbeef/btcutil/coinset" "github.com/roasbeef/btcutil/txsort" - "github.com/roasbeef/btcwallet/chain" - "github.com/roasbeef/btcwallet/waddrmgr" - btcwallet "github.com/roasbeef/btcwallet/wallet" ) const ( @@ -33,6 +29,13 @@ const ( // elkremRootIndex is the top level HD key index from which secrets // used to generate elkrem roots should be derived from. elkremRootIndex = hdkeychain.HardenedKeyStart + 1 + + // identityKeyIndex is the top level HD key index which is used to + // generate/rotate identity keys. + // + // TODO(roasbeef): should instead be child to make room for future + // rotations, etc. + identityKeyIndex = hdkeychain.HardenedKeyStart + 2 ) var ( @@ -221,7 +224,7 @@ type channelOpenMsg struct { type LightningWallet struct { // This mutex is to be held when generating external keys to be used // as multi-sig, and commitment keys within the channel. - KeyGenMtx sync.RWMutex + keyGenMtx sync.RWMutex // This mutex MUST be held when performing coin selection in order to // avoid inadvertently creating multiple funding transaction which @@ -231,25 +234,27 @@ type LightningWallet struct { // A wrapper around a namespace within boltdb reserved for ln-based // wallet meta-data. See the 'channeldb' package for further // information. - channelDB *channeldb.DB + ChannelDB *channeldb.DB // Used by in order to obtain notifications about funding transaction // reaching a specified confirmation depth, and to catch // counterparty's broadcasting revoked commitment states. chainNotifier chainntnfs.ChainNotifier - // The core wallet, all non Lightning Network specific interaction is - // proxied to the internal wallet. - *btcwallet.Wallet + // wallet is the the core wallet, all non Lightning Network specific + // interaction is proxied to the internal wallet. + WalletController + + // Signer is the wallet's current Signer implementation. This Signer is + // used to generate signature for all inputs to potential funding + // transactions, as well as for spends from the funding transaction to + // update the commitment state. + Signer Signer + + // chainIO is an instance of the BlockChainIO interface. chainIO is + // used to lookup the existance of outputs within the utxo set. + chainIO BlockChainIO - // An active RPC connection to a full-node. In the case of a btcd node, - // websockets are used for notifications. If using Bitcoin Core, - // notifications are either generated via long-polling or the usage of - // ZeroMQ. - // TODO(roasbeef): make into interface need: getrawtransaction + gettxout - // * getrawtransaction -> verify proof of channel links - // * gettxout -> verify inputs to funding tx exist and are unspent - rpc *chain.RPCClient // rootKey is the root HD key dervied from a WalletController private // key. This rootKey is used to derive all LN specific secrets. rootKey *hdkeychain.ExtendedKey @@ -269,7 +274,12 @@ type LightningWallet struct { // TODO(roasbeef): zombie garbage collection routine to solve // lost-object/starvation problem/attack. - cfg *Config + // lockedOutPoints is a set of the currently locked outpoint. This + // information is kept in order to provide an easy way to unlock all + // the currently locked outpoints. + lockedOutPoints map[wire.OutPoint]struct{} + + netParams *chaincfg.Params started int32 shutdown int32 @@ -286,21 +296,12 @@ type LightningWallet struct { // // NOTE: The passed channeldb, and ChainNotifier should already be fully // initialized/started before being passed as a function arugment. -func NewLightningWallet(config *Config, cdb *channeldb.DB, - notifier chainntnfs.ChainNotifier) (*LightningWallet, error) { +func NewLightningWallet(cdb *channeldb.DB, notifier chainntnfs.ChainNotifier, + wallet WalletController, signer Signer, bio BlockChainIO, + netParams *chaincfg.Params) (*LightningWallet, error) { - // Ensure the wallet exists or create it when the create flag is set. - netDir := networkDir(config.DataDir, config.NetParams) + // TODO(roasbeef): need a another wallet level config - var pubPass []byte - if config.PublicPass == nil { - pubPass = defaultPubPassphrase - } else { - pubPass = config.PublicPass - } - - loader := btcwallet.NewLoader(config.NetParams, netDir) - walletExists, err := loader.WalletExists() // Fetch the root derivation key from the wallet's HD chain. We'll use // this to generate specific Lightning related secrets on the fly. rootKey, err := wallet.FetchRootKey() @@ -308,68 +309,25 @@ func NewLightningWallet(config *Config, cdb *channeldb.DB, return nil, err } - var createID bool - var wallet *btcwallet.Wallet - if !walletExists { - // Wallet has never been created, perform initial set up. - wallet, err = loader.CreateNewWallet(pubPass, config.PrivatePass, - config.HdSeed) - if err != nil { - return nil, err - } - - createID = true - } else { - // Wallet has been created and been initialized at this point, open it - // along with all the required DB namepsaces, and the DB itself. - wallet, err = loader.OpenExistingWallet(pubPass, false) - if err != nil { - return nil, err - } - } - - if err := wallet.Manager.Unlock(config.PrivatePass); err != nil { - return nil, err - } - - // If we just created the wallet, then reserve, and store a key for - // our ID within the Lightning Network. - if createID { - account := uint32(waddrmgr.DefaultAccountNum) - adrs, err := wallet.Manager.NextInternalAddresses(account, 1, waddrmgr.WitnessPubKey) - if err != nil { - return nil, err - } - - idPubkeyHash := adrs[0].Address().ScriptAddress() - if err := cdb.PutIdKey(idPubkeyHash); err != nil { - return nil, err - } - walletLog.Infof("stored identity key pubkey hash in channeldb") - } - - // Create a special websockets rpc client for btcd which will be used - // by the wallet for notifications, calls, etc. - rpcc, err := chain.NewRPCClient(config.NetParams, config.RpcHost, - config.RpcUser, config.RpcPass, config.CACert, false, 20) + // TODO(roasbeef): always re-derive on the fly? + rootKeyRaw := rootKey.Serialize() + rootMasterKey, err := hdkeychain.NewMaster(rootKeyRaw, netParams) if err != nil { return nil, err } return &LightningWallet{ - chainNotifier: notifier, - rpc: rpcc, - Wallet: wallet, - channelDB: cdb, - msgChan: make(chan interface{}, msgBufferSize), - // TODO(roasbeef): make this atomic.Uint32 instead? Which is - // faster, locks or CAS? I'm guessing CAS because assembly: - // * https://golang.org/src/sync/atomic/asm_amd64.s - nextFundingID: 0, - cfg: config, - fundingLimbo: make(map[uint64]*ChannelReservation), - quit: make(chan struct{}), rootKey: rootMasterKey, + chainNotifier: notifier, + Signer: signer, + WalletController: wallet, + chainIO: bio, + ChannelDB: cdb, + msgChan: make(chan interface{}, msgBufferSize), + nextFundingID: 0, + fundingLimbo: make(map[uint64]*ChannelReservation), + lockedOutPoints: make(map[wire.OutPoint]struct{}), + quit: make(chan struct{}), }, nil } @@ -381,16 +339,10 @@ func (l *LightningWallet) Startup() error { return nil } - // Establish an RPC connection in additino to starting the goroutines - // in the underlying wallet. - if err := l.rpc.Start(); err != nil { + // Start the underlying wallet controller. + if err := l.Start(); err != nil { return err } - l.Start() - - // Pass the rpc client into the wallet so it can sync up to the - // current main chain. - l.SynchronizeRPC(l.rpc) l.wg.Add(1) // TODO(roasbeef): multiple request handlers? @@ -407,16 +359,59 @@ func (l *LightningWallet) Shutdown() error { // Signal the underlying wallet controller to shutdown, waiting until // all active goroutines have been shutdown. - l.Stop() - l.WaitForShutdown() - - l.rpc.Shutdown() + if err := l.Stop(); err != nil { + return err + } close(l.quit) l.wg.Wait() return nil } +// LockOutpoints returns a list of all currently locked outpoint. +func (l *LightningWallet) LockedOutpoints() []*wire.OutPoint { + outPoints := make([]*wire.OutPoint, 0, len(l.lockedOutPoints)) + for outPoint := range l.lockedOutPoints { + outPoints = append(outPoints, &outPoint) + } + + return outPoints +} + +// ResetReservations reset the volatile wallet state which trakcs all currently +// active reservations. +func (l *LightningWallet) ResetReservations() { + l.nextFundingID = 0 + l.fundingLimbo = make(map[uint64]*ChannelReservation) + + for outpoint := range l.lockedOutPoints { + l.UnlockOutpoint(outpoint) + } + l.lockedOutPoints = make(map[wire.OutPoint]struct{}) +} + +// ActiveReservations returns a slice of all the currently active +// (non-cancalled) reservations. +func (l *LightningWallet) ActiveReservations() []*ChannelReservation { + reservations := make([]*ChannelReservation, 0, len(l.fundingLimbo)) + for _, reservation := range l.fundingLimbo { + reservations = append(reservations, reservation) + } + + return reservations +} + +// GetIdentitykey returns the identity private key of the wallet. +// TODO(roasbeef): should be moved elsewhere +func (l *LightningWallet) GetIdentitykey() (*btcec.PrivateKey, error) { + identityKey, err := l.rootKey.Child(identityKeyIndex) + if err != nil { + return nil, err + } + + return identityKey.ECPrivKey() +} + // requestHandler is the primary goroutine(s) resposible for handling, and // dispatching relies to all messages. func (l *LightningWallet) requestHandler() { @@ -489,16 +484,9 @@ func (l *LightningWallet) InitChannelReservation(capacity, // handleFundingReserveRequest processes a message intending to create, and // validate a funding reservation request. func (l *LightningWallet) handleFundingReserveRequest(req *initFundingReserveMsg) { - // Create a limbo and record entry for this newly pending funding request. - l.limboMtx.Lock() - - id := l.nextFundingID - reservation := newChannelReservation(req.capacity, req.fundingAmount, + id := atomic.AddUint64(&l.nextFundingID, 1) + reservation := NewChannelReservation(req.capacity, req.fundingAmount, req.minFeeRate, l, id, req.numConfs) - l.nextFundingID++ - l.fundingLimbo[id] = reservation - - l.limboMtx.Unlock() // Grab the mutex on the ChannelReservation to ensure thead-safety reservation.Lock() @@ -526,27 +514,26 @@ func (l *LightningWallet) handleFundingReserveRequest(req *initFundingReserveMsg // Grab two fresh keys from our HD chain, one will be used for the // multi-sig funding transaction, and the other for the commitment // transaction. - multiSigKey, err := l.getNextRawKey() + multiSigKey, err := l.NewRawKey() if err != nil { req.err <- err req.resp <- nil return } - commitKey, err := l.getNextRawKey() + commitKey, err := l.NewRawKey() if err != nil { req.err <- err req.resp <- nil return } reservation.partialState.OurMultiSigKey = multiSigKey - ourContribution.MultiSigKey = multiSigKey.PubKey() + ourContribution.MultiSigKey = multiSigKey reservation.partialState.OurCommitKey = commitKey - ourContribution.CommitKey = commitKey.PubKey() + ourContribution.CommitKey = commitKey // Generate a fresh address to be used in the case of a cooperative // channel close. - deliveryAddress, err := l.NewAddress(waddrmgr.DefaultAccountNum, - waddrmgr.WitnessPubKey) + deliveryAddress, err := l.NewAddress(WitnessPubKey, false) if err != nil { req.err <- err req.resp <- nil @@ -561,6 +548,12 @@ func (l *LightningWallet) handleFundingReserveRequest(req *initFundingReserveMsg reservation.partialState.OurDeliveryScript = deliveryScript ourContribution.DeliveryAddress = deliveryAddress + // Create a limbo and record entry for this newly pending funding + // request. + l.limboMtx.Lock() + l.fundingLimbo[id] = reservation + l.limboMtx.Unlock() + // Funding reservation request succesfully handled. The funding inputs // will be marked as unavailable until the reservation is either // completed, or cancecled. @@ -591,6 +584,7 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs // Mark all previously locked outpoints as usuable for future funding // requests. for _, unusedInput := range pendingReservation.ourContribution.Inputs { + delete(l.lockedOutPoints, unusedInput.PreviousOutPoint) l.UnlockOutpoint(unusedInput.PreviousOutPoint) } @@ -652,7 +646,7 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // Finally, add the 2-of-2 multi-sig output which will set up the lightning // channel. channelCapacity := int64(pendingReservation.partialState.Capacity) - redeemScript, multiSigOut, err := genFundingPkScript(ourKey.PubKey().SerializeCompressed(), + redeemScript, multiSigOut, err := GenFundingPkScript(ourKey.SerializeCompressed(), theirKey.SerializeCompressed(), channelCapacity) if err != nil { req.err <- err @@ -660,21 +654,6 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { } pendingReservation.partialState.FundingRedeemScript = redeemScript - // Register intent for notifications related to the funding output. - // This'll allow us to properly track the number of confirmations the - // funding tx has once it has been broadcasted. - // TODO(roasbeef): remove - lastBlock := l.Manager.SyncedTo() - scriptAddr, err := l.Manager.ImportScript(redeemScript, &lastBlock) - if err != nil { - req.err <- err - return - } - if err := l.rpc.NotifyReceived([]btcutil.Address{scriptAddr.Address()}); err != nil { - req.err <- err - return - } - // Sort the transaction. Since both side agree to a cannonical // ordering, by sorting we no longer need to send the entire // transaction. Only signatures will be exchanged. @@ -684,81 +663,30 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // Next, sign all inputs that are ours, collecting the signatures in // order of the inputs. pendingReservation.ourFundingInputScripts = make([]*InputScript, 0, len(ourContribution.Inputs)) - hashCache := txscript.NewTxSigHashes(fundingTx) + signDesc := SignDescriptor{ + HashType: txscript.SigHashAll, + SigHashes: txscript.NewTxSigHashes(fundingTx), + } for i, txIn := range fundingTx.TxIn { - // Does the wallet know about the txin? - txDetail, _ := l.TxStore.TxDetails(&txIn.PreviousOutPoint.Hash) - if txDetail == nil { + info, err := l.FetchInputInfo(&txIn.PreviousOutPoint) + if err == ErrNotMine { continue - } - - // Is this our txin? - prevIndex := txIn.PreviousOutPoint.Index - prevOut := txDetail.TxRecord.MsgTx.TxOut[prevIndex] - _, addrs, _, _ := txscript.ExtractPkScriptAddrs(prevOut.PkScript, l.cfg.NetParams) - apkh := addrs[0] - - ai, err := l.Manager.Address(apkh) - if err != nil { - req.err <- fmt.Errorf("cannot get address info: %v", err) - return - } - pka := ai.(waddrmgr.ManagedPubKeyAddress) - privKey, err := pka.PrivKey() - if err != nil { - req.err <- fmt.Errorf("cannot get private key: %v", err) + } else if err != nil { + req.err <- err return } - var witnessProgram []byte - inputScript := &InputScript{} + signDesc.Output = info + signDesc.InputIndex = i - // If we're spending p2wkh output nested within a p2sh output, - // then we'll need to attach a sigScript in addition to witness - // data. - if pka.IsNestedWitness() { - pubKey := privKey.PubKey() - pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed()) - - // Next, we'll generate a valid sigScript that'll allow us to spend - // the p2sh output. The sigScript will contain only a single push of - // the p2wkh witness program corresponding to the matching public key - // of this address. - p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, - l.cfg.NetParams) - if err != nil { - req.err <- fmt.Errorf("unable to create p2wkh addr: %v", err) - return - } - witnessProgram, err = txscript.PayToAddrScript(p2wkhAddr) - if err != nil { - req.err <- fmt.Errorf("unable to create witness program: %v", err) - return - } - bldr := txscript.NewScriptBuilder() - bldr.AddData(witnessProgram) - sigScript, err := bldr.Script() - if err != nil { - req.err <- fmt.Errorf("unable to create scriptsig: %v", err) - return - } - txIn.SignatureScript = sigScript - inputScript.ScriptSig = sigScript - } else { - witnessProgram = prevOut.PkScript - } - - // Generate a valid witness stack for the input. - inputValue := prevOut.Value - witnessScript, err := txscript.WitnessScript(fundingTx, hashCache, i, - inputValue, witnessProgram, txscript.SigHashAll, privKey, true) + inputScript, err := l.Signer.ComputeInputScript(fundingTx, &signDesc) if err != nil { - req.err <- fmt.Errorf("cannot create witnessscript: %s", err) + req.err <- err return } - txIn.Witness = witnessScript - inputScript.Witness = witnessScript + txIn.SignatureScript = inputScript.ScriptSig + txIn.Witness = inputScript.Witness pendingReservation.ourFundingInputScripts = append( pendingReservation.ourFundingInputScripts, inputScript, @@ -766,10 +694,10 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { } // Locate the index of the multi-sig outpoint in order to record it - // since the outputs are cannonically sorted. If this is a sigle funder + // since the outputs are cannonically sorted. If this is a single funder // workflow, then we'll also need to send this to the remote node. fundingTxID := fundingTx.TxSha() - _, multiSigIndex := findScriptOutputIndex(fundingTx, multiSigOut.PkScript) + _, multiSigIndex := FindScriptOutputIndex(fundingTx, multiSigOut.PkScript) fundingOutpoint := wire.NewOutPoint(&fundingTxID, multiSigIndex) pendingReservation.partialState.FundingOutpoint = fundingOutpoint @@ -799,7 +727,7 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { return } theirCommitKey := theirContribution.CommitKey - ourRevokeKey := deriveRevocationPubkey(theirCommitKey, firstPreimage[:]) + ourRevokeKey := DeriveRevocationPubkey(theirCommitKey, firstPreimage[:]) // Create the txIn to our commitment transaction; required to construct // the commitment transactions. @@ -811,14 +739,14 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { ourBalance := ourContribution.FundingAmount theirBalance := theirContribution.FundingAmount ourCommitKey := ourContribution.CommitKey - ourCommitTx, err := createCommitTx(fundingTxIn, ourCommitKey, theirCommitKey, + ourCommitTx, err := CreateCommitTx(fundingTxIn, ourCommitKey, theirCommitKey, ourRevokeKey, ourContribution.CsvDelay, ourBalance, theirBalance) if err != nil { req.err <- err return } - theirCommitTx, err := createCommitTx(fundingTxIn, theirCommitKey, ourCommitKey, + theirCommitTx, err := CreateCommitTx(fundingTxIn, theirCommitKey, ourCommitKey, theirContribution.RevocationKey, theirContribution.CsvDelay, theirBalance, ourBalance) if err != nil { @@ -849,10 +777,15 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // Generate a signature for their version of the initial commitment // transaction. - hashCache = txscript.NewTxSigHashes(theirCommitTx) - channelBalance := pendingReservation.partialState.Capacity - sigTheirCommit, err := txscript.RawTxInWitnessSignature(theirCommitTx, hashCache, 0, - int64(channelBalance), redeemScript, txscript.SigHashAll, ourKey) + signDesc = SignDescriptor{ + RedeemScript: redeemScript, + PubKey: ourKey, + Output: multiSigOut, + HashType: txscript.SigHashAll, + SigHashes: txscript.NewTxSigHashes(theirCommitTx), + InputIndex: 0, + } + sigTheirCommit, err := l.Signer.SignOutputRaw(theirCommitTx, &signDesc) if err != nil { req.err <- err return @@ -890,7 +823,7 @@ func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg ourKey := pendingReservation.partialState.OurMultiSigKey theirKey := theirContribution.MultiSigKey channelCapacity := int64(pendingReservation.partialState.Capacity) - redeemScript, _, err := genFundingPkScript(ourKey.PubKey().SerializeCompressed(), + redeemScript, _, err := GenFundingPkScript(ourKey.SerializeCompressed(), theirKey.SerializeCompressed(), channelCapacity) if err != nil { req.err <- err @@ -915,7 +848,7 @@ func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg } pendingReservation.partialState.LocalElkrem = elkremSender theirCommitKey := theirContribution.CommitKey - ourRevokeKey := deriveRevocationPubkey(theirCommitKey, firstPreimage[:]) + ourRevokeKey := DeriveRevocationPubkey(theirCommitKey, firstPreimage[:]) // Initialize an empty sha-chain for them, tracking the current pending // revocation hash (we don't yet know the pre-image so we can't add it @@ -976,25 +909,16 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs // Fetch the alleged previous output along with the // pkscript referenced by this input. prevOut := txin.PreviousOutPoint - output, err := l.rpc.GetTxOut(&prevOut.Hash, prevOut.Index, false) + output, err := l.chainIO.GetUtxo(&prevOut.Hash, prevOut.Index) if output == nil { msg.err <- fmt.Errorf("input to funding tx does not exist: %v", err) return } - pkScript, err := hex.DecodeString(output.ScriptPubKey.Hex) - if err != nil { - msg.err <- err - return - } - // Sadly, gettxout returns the output value in BTC - // instead of satoshis. - inputValue := int64(output.Value) * 1e8 - // Ensure that the witness+sigScript combo is valid. - vm, err := txscript.NewEngine(pkScript, + vm, err := txscript.NewEngine(output.PkScript, fundingTx, i, txscript.StandardVerifyFlags, nil, - fundingHashCache, inputValue) + fundingHashCache, output.Value) if err != nil { // TODO(roasbeef): cancel at this stage if invalid sigs? msg.err <- fmt.Errorf("cannot create script engine: %s", err) @@ -1014,54 +938,37 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs pendingReservation.theirCommitmentSig = msg.theirCommitmentSig commitTx := pendingReservation.partialState.OurCommitTx theirKey := pendingReservation.theirContribution.MultiSigKey - ourKey := pendingReservation.partialState.OurMultiSigKey // Re-generate both the redeemScript and p2sh output. We sign the // redeemScript script, but include the p2sh output as the subscript // for verification. redeemScript := pendingReservation.partialState.FundingRedeemScript - p2wsh, err := witnessScriptHash(redeemScript) - if err != nil { - msg.err <- err - return - } - - // First, we sign our copy of the commitment transaction ourselves. - channelValue := int64(pendingReservation.partialState.Capacity) - hashCache := txscript.NewTxSigHashes(commitTx) - ourCommitSig, err := txscript.RawTxInWitnessSignature(commitTx, hashCache, 0, - channelValue, redeemScript, txscript.SigHashAll, ourKey) - if err != nil { - msg.err <- err - return - } // Next, create the spending scriptSig, and then verify that the script // is complete, allowing us to spend from the funding transaction. theirCommitSig := msg.theirCommitmentSig - ourKeySer := ourKey.PubKey().SerializeCompressed() - theirKeySer := theirKey.SerializeCompressed() - witness := spendMultiSig(redeemScript, ourKeySer, ourCommitSig, - theirKeySer, theirCommitSig) - - // Finally, create an instance of a Script VM, and ensure that the - // Script executes succesfully. - commitTx.TxIn[0].Witness = witness - vm, err := txscript.NewEngine(p2wsh, - commitTx, 0, txscript.StandardVerifyFlags, nil, - nil, channelValue) + channelValue := int64(pendingReservation.partialState.Capacity) + hashCache := txscript.NewTxSigHashes(commitTx) + sigHash, err := txscript.CalcWitnessSigHash(redeemScript, hashCache, + txscript.SigHashAll, commitTx, 0, channelValue) if err != nil { - msg.err <- err - return - } - if err := vm.Execute(); err != nil { msg.err <- fmt.Errorf("counterparty's commitment signature is invalid: %v", err) return } - // Strip and store the signature to ensure that our commitment - // transaction doesn't stay hot. - commitTx.TxIn[0].Witness = nil + walletLog.Infof("sighash verify: %v", hex.EncodeToString(sigHash)) + walletLog.Infof("initer verifying tx: %v", spew.Sdump(commitTx)) + + // Verify that we've received a valid signature from the remote party + // for our version of the commitment transaction. + sig, err := btcec.ParseSignature(theirCommitSig, btcec.S256()) + if err != nil { + msg.err <- err + return + } else if !sig.Verify(sigHash, theirKey) { + msg.err <- fmt.Errorf("counterparty's commitment signature is invalid") + return + } pendingReservation.partialState.OurCommitSig = theirCommitSig // Funding complete, this entry can be removed from limbo. @@ -1126,14 +1033,14 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { theirCommitKey := pendingReservation.theirContribution.CommitKey ourBalance := pendingReservation.ourContribution.FundingAmount theirBalance := pendingReservation.theirContribution.FundingAmount - ourCommitTx, err := createCommitTx(fundingTxIn, ourCommitKey, theirCommitKey, + ourCommitTx, err := CreateCommitTx(fundingTxIn, ourCommitKey, theirCommitKey, pendingReservation.ourContribution.RevocationKey, pendingReservation.ourContribution.CsvDelay, ourBalance, theirBalance) if err != nil { req.err <- err return } - theirCommitTx, err := createCommitTx(fundingTxIn, theirCommitKey, ourCommitKey, + theirCommitTx, err := CreateCommitTx(fundingTxIn, theirCommitKey, ourCommitKey, req.revokeKey, pendingReservation.theirContribution.CsvDelay, theirBalance, ourBalance) if err != nil { @@ -1148,65 +1055,51 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { pendingReservation.partialState.OurCommitTx = ourCommitTx txsort.InPlaceSort(theirCommitTx) - // Verify that their signature of valid for our current commitment - // transaction. Re-generate both the redeemScript and p2sh output. We - // sign the redeemScript script, but include the p2sh output as the - // subscript for verification. - // TODO(roasbeef): replace with regular sighash calculation once the PR - // is merged. redeemScript := pendingReservation.partialState.FundingRedeemScript - p2wsh, err := witnessScriptHash(redeemScript) - if err != nil { - req.err <- err - return - } - // TODO(roasbeef): remove all duplication after merge. - - // First, we sign our copy of the commitment transaction ourselves. channelValue := int64(pendingReservation.partialState.Capacity) hashCache := txscript.NewTxSigHashes(ourCommitTx) theirKey := pendingReservation.theirContribution.MultiSigKey ourKey := pendingReservation.partialState.OurMultiSigKey - ourCommitSig, err := txscript.RawTxInWitnessSignature(ourCommitTx, hashCache, 0, - channelValue, redeemScript, txscript.SigHashAll, ourKey) + + sigHash, err := txscript.CalcWitnessSigHash(redeemScript, hashCache, + txscript.SigHashAll, ourCommitTx, 0, channelValue) if err != nil { req.err <- err return } - // Next, create the spending scriptSig, and then verify that the script - // is complete, allowing us to spend from the funding transaction. - ourKeySer := ourKey.PubKey().SerializeCompressed() - theirKeySer := theirKey.SerializeCompressed() - witness := spendMultiSig(redeemScript, ourKeySer, ourCommitSig, - theirKeySer, req.theirCommitmentSig) - - // Finally, create an instance of a Script VM, and ensure that the - // Script executes succesfully. - ourCommitTx.TxIn[0].Witness = witness - // TODO(roasbeef): replace engine with plain sighash check - vm, err := txscript.NewEngine(p2wsh, ourCommitTx, 0, - txscript.StandardVerifyFlags, nil, nil, channelValue) + // Verify that we've received a valid signature from the remote party + // for our version of the commitment transaction. + sig, err := btcec.ParseSignature(req.theirCommitmentSig, btcec.S256()) if err != nil { req.err <- err return - } - if err := vm.Execute(); err != nil { - req.err <- fmt.Errorf("counterparty's commitment signature is invalid: %v", err) + } else if !sig.Verify(sigHash, theirKey) { + req.err <- fmt.Errorf("counterparty's commitment signature is invalid") return } - - // Strip and store the signature to ensure that our commitment - // transaction doesn't stay hot. - ourCommitTx.TxIn[0].Witness = nil pendingReservation.partialState.OurCommitSig = req.theirCommitmentSig // With their signature for our version of the commitment transactions // verified, we can now generate a signature for their version, // allowing the funding transaction to be safely broadcast. - hashCache = txscript.NewTxSigHashes(theirCommitTx) - sigTheirCommit, err := txscript.RawTxInWitnessSignature(theirCommitTx, hashCache, 0, - channelValue, redeemScript, txscript.SigHashAll, ourKey) + p2wsh, err := witnessScriptHash(redeemScript) + if err != nil { + req.err <- err + return + } + signDesc := SignDescriptor{ + RedeemScript: redeemScript, + PubKey: ourKey, + Output: &wire.TxOut{ + PkScript: p2wsh, + Value: channelValue, + }, + HashType: txscript.SigHashAll, + SigHashes: txscript.NewTxSigHashes(theirCommitTx), + InputIndex: 0, + } + sigTheirCommit, err := l.Signer.SignOutputRaw(theirCommitTx, &signDesc) if err != nil { req.err <- err return @@ -1249,8 +1142,7 @@ func (l *LightningWallet) handleChannelOpen(req *channelOpenMsg) { // Finally, create and officially open the payment channel! // TODO(roasbeef): CreationTime once tx is 'open' - channel, _ := NewLightningChannel(l, l.chainNotifier, l.channelDB, - res.partialState) + channel, _ := NewLightningChannel(l.Signer, l, l.chainNotifier, res.partialState) res.chanOpen <- channel req.err <- nil @@ -1291,61 +1183,11 @@ out: // Finally, create and officially open the payment channel! // TODO(roasbeef): CreationTime once tx is 'open' - channel, _ := NewLightningChannel(l, l.chainNotifier, l.channelDB, + channel, _ := NewLightningChannel(l.Signer, l, l.chainNotifier, res.partialState) res.chanOpen <- channel } -// getNextRawKey retrieves the next key within our HD key-chain for use within -// as a multi-sig key within the funding transaction, or within the commitment -// transaction's outputs. -// TODO(roasbeef): on shutdown, write state of pending keys, then read back? -func (l *LightningWallet) getNextRawKey() (*btcec.PrivateKey, error) { - l.KeyGenMtx.Lock() - defer l.KeyGenMtx.Unlock() - - nextAddr, err := l.Manager.NextExternalAddresses(waddrmgr.DefaultAccountNum, - 1, waddrmgr.WitnessPubKey) - if err != nil { - return nil, err - } - - pkAddr := nextAddr[0].(waddrmgr.ManagedPubKeyAddress) - - return pkAddr.PrivKey() -} - -// ListUnspentWitness returns a slice of all the unspent outputs the wallet -// controls which pay to witness programs either directly or indirectly. -func (l *LightningWallet) ListUnspentWitness(minConfs int32) ([]*btcjson.ListUnspentResult, error) { - // First, grab all the unfiltered currently unspent outputs. - maxConfs := int32(math.MaxInt32) - unspentOutputs, err := l.ListUnspent(minConfs, maxConfs, nil) - if err != nil { - return nil, err - } - - // Next, we'll run through all the regular outputs, only saving those - // which are p2wkh outputs or a p2wsh output nested within a p2sh output. - witnessOutputs := make([]*btcjson.ListUnspentResult, 0, len(unspentOutputs)) - for _, output := range unspentOutputs { - pkScript, err := hex.DecodeString(output.ScriptPubKey) - if err != nil { - return nil, err - } - - // TODO(roasbeef): this assumes all p2sh outputs returned by - // the wallet are nested p2sh... - if txscript.IsPayToWitnessPubKeyHash(pkScript) || - txscript.IsPayToScriptHash(pkScript) { - witnessOutputs = append(witnessOutputs, output) - } - - } - - return witnessOutputs, nil -} - // selectCoinsAndChange performs coin selection in order to obtain witness // outputs which sum to at least 'numCoins' amount of satoshis. If coin // selection is succesful/possible, then the selected coins are available within