func testFundingTransactionLockedOutputs(miner *rpctest.Harness, wallet *lnwallet.LightningWallet, t *testing.T) { t.Log("Running funding txn locked outputs test") // Create a single channel asking for 16 BTC total. fundingAmount := btcutil.Amount(8 * 1e8) _, err := wallet.InitChannelReservation(fundingAmount, fundingAmount, testPub, bobAddr, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation 1: %v", err) } // Now attempt to reserve funds for another channel, this time // requesting 900 BTC. We only have around 64BTC worth of outpoints // that aren't locked, so this should fail. amt := btcutil.Amount(900 * 1e8) failedReservation, err := wallet.InitChannelReservation(amt, amt, testPub, bobAddr, numReqConfs, 4) if err == nil { t.Fatalf("not error returned, should fail on coin selection") } if _, ok := err.(*lnwallet.ErrInsufficientFunds); !ok { t.Fatalf("error not coinselect error: %v", err) } if failedReservation != nil { t.Fatalf("reservation should be nil") } }
func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness, wallet *lnwallet.LightningWallet, t *testing.T) { t.Log("Running funding insufficient funds tests") // Create a reservation for 44 BTC. fundingAmount := btcutil.Amount(44 * 1e8) chanReservation, err := wallet.InitChannelReservation(fundingAmount, fundingAmount, testPub, bobAddr, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation: %v", err) } // Attempt to create another channel with 44 BTC, this should fail. _, err = wallet.InitChannelReservation(fundingAmount, fundingAmount, testPub, bobAddr, numReqConfs, 4) if _, ok := err.(*lnwallet.ErrInsufficientFunds); !ok { t.Fatalf("coin selection succeded should have insufficient funds: %v", err) } // Now cancel that old reservation. if err := chanReservation.Cancel(); err != nil { t.Fatalf("unable to cancel reservation: %v", err) } // Those outpoints should no longer be locked. lockedOutPoints := wallet.LockedOutpoints() if len(lockedOutPoints) != 0 { t.Fatalf("outpoints still locked") } // 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 // outpoints, will let us fail early/fast instead of querying and // attempting coin selection. // Request to fund a new channel should now succeeed. _, err = wallet.InitChannelReservation(fundingAmount, fundingAmount, testPub, bobAddr, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation: %v", err) } }
func testSingleFunderReservationWorkflowResponder(miner *rpctest.Harness, wallet *lnwallet.LightningWallet, t *testing.T) { t.Log("Running single funder workflow responder test") // For this scenario, bob will initiate the channel, while we simply act as // the responder. capacity := btcutil.Amount(4 * 1e8) // Create the bob-test wallet which will be initiator of a single // funder channel shortly. bobNode, err := newBobNode(miner, capacity) if err != nil { t.Fatalf("unable to create bob node: %v", err) } // Bob sends over a single funding request, so we allocate our // contribution and the necessary resources. fundingAmt := btcutil.Amount(0) chanReservation, err := wallet.InitChannelReservation(capacity, fundingAmt, bobNode.id, bobAddr, numReqConfs, 4) if err != nil { t.Fatalf("unable to init channel reservation: %v", err) } // Verify all contribution fields have been set properly. Since we are // the recipient of a single-funder channel, we shouldn't have selected // any coins or generated any change outputs. ourContribution := chanReservation.OurContribution() if len(ourContribution.Inputs) != 0 { t.Fatalf("outputs for funding tx not properly selected, have %v "+ "outputs should have 0", len(ourContribution.Inputs)) } if len(ourContribution.ChangeOutputs) != 0 { t.Fatalf("coin selection failed, should have no change outputs, "+ "instead have: %v", ourContribution.ChangeOutputs[0].Value) } if ourContribution.MultiSigKey == nil { t.Fatalf("alice's key for multi-sig not found") } if ourContribution.CommitKey == nil { t.Fatalf("alice's key for commit not found") } if ourContribution.DeliveryAddress == nil { t.Fatalf("alice's final delivery address not found") } if ourContribution.CsvDelay == 0 { t.Fatalf("csv delay not set") } // Next we process Bob's single funder contribution which doesn't // include any inputs or change addresses, as only Bob will construct // the funding transaction. bobContribution := bobNode.Contribution(ourContribution.CommitKey) if err := chanReservation.ProcessSingleContribution(bobContribution); err != nil { t.Fatalf("unable to process bob's contribution: %v", err) } if chanReservation.FinalFundingTx() != nil { t.Fatalf("funding transaction populated!") } if len(bobContribution.Inputs) != 1 { t.Fatalf("bob shouldn't have one inputs, instead has %v", len(bobContribution.Inputs)) } if ourContribution.RevocationKey == nil { t.Fatalf("alice's revocation key not found") } if len(bobContribution.ChangeOutputs) != 1 { t.Fatalf("bob shouldn't have one change output, instead "+ "has %v", len(bobContribution.ChangeOutputs)) } if bobContribution.MultiSigKey == nil { t.Fatalf("bob's key for multi-sig not found") } if bobContribution.CommitKey == nil { t.Fatalf("bob's key for commit tx not found") } if bobContribution.DeliveryAddress == nil { t.Fatalf("bob's final delivery address not found") } if bobContribution.RevocationKey == nil { t.Fatalf("bob's revocaiton key not found") } fundingRedeemScript, multiOut, err := lnwallet.GenFundingPkScript( ourContribution.MultiSigKey.SerializeCompressed(), bobContribution.MultiSigKey.SerializeCompressed(), // TODO(roasbeef): account for hard-coded fee, remove bob node int64(capacity)+5000) if err != nil { t.Fatalf("unable to generate multi-sig output: %v", err) } // At this point, we send Bob our contribution, allowing him to // construct the funding transaction, and sign our version of the // commitment transaction. fundingTx := wire.NewMsgTx() fundingTx.AddTxIn(bobNode.availableOutputs[0]) fundingTx.AddTxOut(bobNode.changeOutputs[0]) fundingTx.AddTxOut(multiOut) txsort.InPlaceSort(fundingTx) if _, err := bobNode.signFundingTx(fundingTx); err != nil { t.Fatalf("unable to generate bob's funding sigs: %v", err) } // Locate the output index of the 2-of-2 in order to send back to the // wallet so it can finalize the transaction by signing bob's commitment // transaction. fundingTxID := fundingTx.TxSha() _, multiSigIndex := lnwallet.FindScriptOutputIndex(fundingTx, multiOut.PkScript) fundingOutpoint := wire.NewOutPoint(&fundingTxID, multiSigIndex) fundingTxIn := wire.NewTxIn(fundingOutpoint, nil, nil) aliceCommitTx, err := lnwallet.CreateCommitTx(fundingTxIn, ourContribution.CommitKey, bobContribution.CommitKey, ourContribution.RevocationKey, ourContribution.CsvDelay, 0, capacity) if err != nil { t.Fatalf("unable to create alice's commit tx: %v", err) } txsort.InPlaceSort(aliceCommitTx) bobCommitSig, err := bobNode.signCommitTx(aliceCommitTx, // TODO(roasbeef): account for hard-coded fee, remove bob node fundingRedeemScript, int64(capacity)+5000) if err != nil { t.Fatalf("unable to sign alice's commit tx: %v", err) } // With this stage complete, Alice can now complete the reservation. bobRevokeKey := bobContribution.RevocationKey if err := chanReservation.CompleteReservationSingle(bobRevokeKey, fundingOutpoint, bobCommitSig); err != nil { t.Fatalf("unable to complete reservation: %v", err) } // Alice should have saved the funding output. if chanReservation.FundingOutpoint() != fundingOutpoint { t.Fatalf("funding outputs don't match: %#v vs %#v", chanReservation.FundingOutpoint(), fundingOutpoint) } // Some period of time later, Bob presents us with an SPV proof // attesting to an open channel. At this point Alice recognizes the // channel, saves the state to disk, and creates the channel itself. if _, err := chanReservation.FinalizeReservation(); err != nil { t.Fatalf("unable to finalize reservation: %v", err) } // TODO(roasbeef): bob verify alice's sig }
func testSingleFunderReservationWorkflowInitiator(miner *rpctest.Harness, lnwallet *lnwallet.LightningWallet, t *testing.T) { t.Log("Running single funder workflow initiator test") // For this scenario, we (lnwallet) will be the channel initiator while bob // will be the recipient. // Create the bob-test wallet which will be the other side of our funding // channel. bobNode, err := newBobNode(miner, 0) if err != nil { t.Fatalf("unable to create bob node: %v", err) } // Initialize a reservation for a channel with 4 BTC funded solely by us. fundingAmt := btcutil.Amount(4 * 1e8) chanReservation, err := lnwallet.InitChannelReservation(fundingAmt, fundingAmt, bobNode.id, bobAddr, numReqConfs, 4) if err != nil { t.Fatalf("unable to init channel reservation: %v", err) } // Verify all contribution fields have been set properly. ourContribution := chanReservation.OurContribution() if len(ourContribution.Inputs) < 1 { t.Fatalf("outputs for funding tx not properly selected, have %v "+ "outputs should at least 1", len(ourContribution.Inputs)) } if len(ourContribution.ChangeOutputs) != 1 { t.Fatalf("coin selection failed, should have one change outputs, "+ "instead have: %v", len(ourContribution.ChangeOutputs)) } if ourContribution.MultiSigKey == nil { t.Fatalf("alice's key for multi-sig not found") } if ourContribution.CommitKey == nil { t.Fatalf("alice's key for commit not found") } if ourContribution.DeliveryAddress == nil { t.Fatalf("alice's final delivery address not found") } if ourContribution.CsvDelay == 0 { t.Fatalf("csv delay not set") } // At this point bob now responds to our request with a response // containing his channel contribution. The contribution will have no // inputs, only a multi-sig key, csv delay, etc. bobContribution := bobNode.SingleContribution(ourContribution.CommitKey) if err := chanReservation.ProcessContribution(bobContribution); err != nil { t.Fatalf("unable to add bob's contribution: %v", err) } // At this point, the reservation should have our signatures, and a // partial funding transaction (missing bob's sigs). theirContribution := chanReservation.TheirContribution() ourFundingSigs, ourCommitSig := chanReservation.OurSignatures() if ourFundingSigs == nil { t.Fatalf("funding sigs not found") } if ourCommitSig == nil { t.Fatalf("commitment sig not found") } // Additionally, the funding tx should have been populated. if chanReservation.FinalFundingTx() == nil { t.Fatalf("funding transaction never created!") } // Their funds should also be filled in. if len(theirContribution.Inputs) != 0 { t.Fatalf("bob shouldn't have any inputs, instead has %v", len(theirContribution.Inputs)) } if len(theirContribution.ChangeOutputs) != 0 { t.Fatalf("bob shouldn't have any change outputs, instead "+ "has %v", theirContribution.ChangeOutputs[0].Value) } if ourContribution.RevocationKey == nil { t.Fatalf("alice's revocation hash not found") } if theirContribution.MultiSigKey == nil { t.Fatalf("bob's key for multi-sig not found") } if theirContribution.CommitKey == nil { t.Fatalf("bob's key for commit tx not found") } if theirContribution.DeliveryAddress == nil { t.Fatalf("bob's final delivery address not found") } if theirContribution.RevocationKey == nil { t.Fatalf("bob's revocaiton hash not found") } // With this contribution processed, we're able to create the // funding+commitment transactions, as well as generate a signature // for bob's version of the commitment transaction. // // Now Bob can generate a signature for our version of the commitment // transaction, allowing us to complete the reservation. bobCommitSig, err := bobNode.signCommitTx( chanReservation.LocalCommitTx(), chanReservation.FundingRedeemScript(), // TODO(roasbeef): account for current hard-coded fee, need to // remove bobNode entirely int64(fundingAmt)+5000) if err != nil { t.Fatalf("bob is unable to sign alice's commit tx: %v", err) } if err := chanReservation.CompleteReservation(nil, bobCommitSig); err != nil { t.Fatalf("unable to complete funding tx: %v", err) } // TODO(roasbeef): verify our sig for bob's once sighash change is // merged. // The resulting active channel state should have been persisted to the DB. // TODO(roasbeef): de-duplicate fundingTx := chanReservation.FinalFundingTx() fundingSha := fundingTx.TxSha() channels, err := lnwallet.ChannelDB.FetchOpenChannels(bobNode.id) if err != nil { t.Fatalf("unable to retrieve channel from DB: %v", err) } if !bytes.Equal(channels[0].FundingOutpoint.Hash[:], fundingSha[:]) { t.Fatalf("channel state not properly saved: %v vs %v", hex.EncodeToString(channels[0].FundingOutpoint.Hash[:]), hex.EncodeToString(fundingSha[:])) } assertChannelOpen(t, miner, uint32(numReqConfs), chanReservation.DispatchChan()) }
func testDualFundingReservationWorkflow(miner *rpctest.Harness, wallet *lnwallet.LightningWallet, t *testing.T) { t.Log("Running dual reservation workflow test") // Create the bob-test wallet which will be the other side of our funding // channel. fundingAmount := btcutil.Amount(5 * 1e8) bobNode, err := newBobNode(miner, fundingAmount) if err != nil { t.Fatalf("unable to create bob node: %v", err) } // Bob initiates a channel funded with 5 BTC for each side, so 10 // BTC total. He also generates 2 BTC in change. chanReservation, err := wallet.InitChannelReservation(fundingAmount*2, fundingAmount, bobNode.id, bobAddr, numReqConfs, 4) if err != nil { t.Fatalf("unable to initialize funding reservation: %v", err) } // The channel reservation should now be populated with a multi-sig key // from our HD chain, a change output with 3 BTC, and 2 outputs selected // of 4 BTC each. Additionally, the rest of the items needed to fufill a // funding contribution should also have been filled in. ourContribution := chanReservation.OurContribution() if len(ourContribution.Inputs) != 2 { t.Fatalf("outputs for funding tx not properly selected, have %v "+ "outputs should have 2", len(ourContribution.Inputs)) } if ourContribution.MultiSigKey == nil { t.Fatalf("alice's key for multi-sig not found") } if ourContribution.CommitKey == nil { t.Fatalf("alice's key for commit not found") } if ourContribution.DeliveryAddress == nil { t.Fatalf("alice's final delivery address not found") } if ourContribution.CsvDelay == 0 { t.Fatalf("csv delay not set") } // Bob sends over his output, change addr, pub keys, initial revocation, // final delivery address, and his accepted csv delay for the // commitment transactions. bobContribution := bobNode.Contribution(ourContribution.CommitKey) if err := chanReservation.ProcessContribution(bobContribution); err != nil { t.Fatalf("unable to add bob's funds to the funding tx: %v", err) } // At this point, the reservation should have our signatures, and a // partial funding transaction (missing bob's sigs). theirContribution := chanReservation.TheirContribution() ourFundingSigs, ourCommitSig := chanReservation.OurSignatures() if len(ourFundingSigs) != 2 { t.Fatalf("only %v of our sigs present, should have 2", len(ourFundingSigs)) } if ourCommitSig == nil { t.Fatalf("commitment sig not found") } if ourContribution.RevocationKey == nil { t.Fatalf("alice's revocation key not found") } // Additionally, the funding tx should have been populated. 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 "+ "outputs should have 2", len(theirContribution.Inputs)) } if theirContribution.ChangeOutputs[0].Value != 2e8 { t.Fatalf("bob should have one change output with value 2e8"+ "satoshis, is instead %v", theirContribution.ChangeOutputs[0].Value) } if theirContribution.MultiSigKey == nil { t.Fatalf("bob's key for multi-sig not found") } if theirContribution.CommitKey == nil { t.Fatalf("bob's key for commit tx not found") } if theirContribution.DeliveryAddress == nil { t.Fatalf("bob's final delivery address not found") } if theirContribution.RevocationKey == nil { t.Fatalf("bob's revocaiton key not found") } // TODO(roasbeef): account for current hard-coded commit fee, // need to remove bob all together chanCapacity := int64(10e8 + 5000) // Alice responds with her output, change addr, multi-sig key and signatures. // Bob then responds with his signatures. bobsSigs, err := bobNode.signFundingTx(fundingTx) if err != nil { t.Fatalf("unable to sign inputs for bob: %v", err) } commitSig, err := bobNode.signCommitTx( chanReservation.LocalCommitTx(), chanReservation.FundingRedeemScript(), chanCapacity) if err != nil { t.Fatalf("bob is unable to sign alice's commit tx: %v", err) } if err := chanReservation.CompleteReservation(bobsSigs, commitSig); err != nil { t.Fatalf("unable to complete funding tx: %v", err) } // At this point, the channel can be considered "open" when the funding // txn hits a "comfortable" depth. // The resulting active channel state should have been persisted to the DB. fundingSha := fundingTx.TxSha() channels, err := wallet.ChannelDB.FetchOpenChannels(bobNode.id) if err != nil { t.Fatalf("unable to retrieve channel from DB: %v", err) } if !bytes.Equal(channels[0].FundingOutpoint.Hash[:], fundingSha[:]) { t.Fatalf("channel state not properly saved") } // Assert that tha channel opens after a single block. lnc := assertChannelOpen(t, miner, uint32(numReqConfs), chanReservation.DispatchChan()) // Now that the channel is open, execute a cooperative closure of the // now open channel. aliceCloseSig, _, err := lnc.InitCooperativeClose() 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. witnessScript := lnc.FundingWitnessScript fundingOut := lnc.ChannelPoint() fundingTxIn := wire.NewTxIn(fundingOut, nil, nil) bobCloseTx := lnwallet.CreateCooperativeCloseTx(fundingTxIn, chanInfo.RemoteBalance, chanInfo.LocalBalance, lnc.RemoteDeliveryScript, lnc.LocalDeliveryScript, false) bobSig, err := bobNode.signCommitTx(bobCloseTx, witnessScript, 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 := chanReservation.OurContribution().MultiSigKey.SerializeCompressed() theirKey := chanReservation.TheirContribution().MultiSigKey.SerializeCompressed() witness := lnwallet.SpendMultiSig(witnessScript, ourKey, aliceCloseSig, theirKey, bobSig) bobCloseTx.TxIn[0].Witness = witness if err := wallet.PublishTransaction(bobCloseTx); err != nil { t.Fatalf("broadcast of close tx rejected: %v", err) } }