func testCoinSelector(tests []coinSelectTest, t *testing.T) { for testIndex, test := range tests { cs, err := test.selector.CoinSelect(test.targetValue, test.inputCoins) if err != test.expectedError { t.Errorf("[%d] expected a different error: got=%v, expected=%v", testIndex, err, test.expectedError) continue } if test.expectedCoins != nil { if cs == nil { t.Errorf("[%d] expected non-nil coinset", testIndex) continue } coins := cs.Coins() if len(coins) != len(test.expectedCoins) { t.Errorf("[%d] expected different number of coins: got=%d, expected=%d", testIndex, len(coins), len(test.expectedCoins)) continue } for n := 0; n < len(test.expectedCoins); n++ { if coins[n] != test.expectedCoins[n] { t.Errorf("[%d] expected different coins at coin index %d: got=%#v, expected=%#v", testIndex, n, coins[n], test.expectedCoins[n]) continue } } coinSet := coinset.NewCoinSet(coins) if coinSet.TotalValue() < test.targetValue { t.Errorf("[%d] targetValue not satistifed", testIndex) continue } } } }
func TestCoinSet(t *testing.T) { cs := coinset.NewCoinSet(nil) if cs.PopCoin() != nil { t.Error("Expected popCoin of empty to be nil") } if cs.ShiftCoin() != nil { t.Error("Expected shiftCoin of empty to be nil") } cs.PushCoin(coins[0]) cs.PushCoin(coins[1]) cs.PushCoin(coins[2]) if cs.PopCoin() != coins[2] { t.Error("Expected third coin") } if cs.ShiftCoin() != coins[0] { t.Error("Expected first coin") } mtx := coinset.NewMsgTxWithInputCoins(cs) if len(mtx.TxIn) != 1 { t.Errorf("Expected only 1 TxIn, got %d", len(mtx.TxIn)) } op := mtx.TxIn[0].PreviousOutPoint if !op.Hash.IsEqual(coins[1].Hash()) || op.Index != coins[1].Index() { t.Errorf("Expected the second coin to be added as input to mtx") } }
// 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.fundingType, req.fundingAmount, req.minFeeRate, l, id) l.nextFundingID++ l.fundingLimbo[id] = reservation l.limboMtx.Unlock() // Grab the mutex on the ChannelReservation to ensure thead-safety reservation.Lock() defer reservation.Unlock() reservation.partialState.TheirLNID = req.nodeID ourContribution := reservation.ourContribution ourContribution.CsvDelay = req.csvDelay // We hold the coin select mutex while querying for outputs, and // performing coin selection in order to avoid inadvertent double spends // accross funding transactions. // NOTE: we don't use defer her so we can properly release the lock // when we encounter an error condition. l.coinSelectMtx.Lock() // Find all unlocked unspent outputs with greater than 6 confirmations. maxConfs := int32(math.MaxInt32) // TODO(roasbeef): make 6 a config paramter? unspentOutputs, err := l.ListUnspent(6, maxConfs, nil) if err != nil { l.coinSelectMtx.Unlock() req.err <- err req.resp <- nil return } // Convert the outputs to coins for coin selection below. coins, err := outputsToCoins(unspentOutputs) if err != nil { l.coinSelectMtx.Unlock() req.err <- err req.resp <- nil return } // Peform coin selection over our available, unlocked unspent outputs // in order to find enough coins to meet the funding amount requirements. // // TODO(roasbeef): Should extend coinset with optimal coin selection // heuristics for our use case. // TODO(roasbeef): factor in fees.. // TODO(roasbeef): possibly integrate the fee prediction project? if // results hold up... // NOTE: this current selection assumes "priority" is still a thing. selector := &coinset.MaxValueAgeCoinSelector{ MaxInputs: 10, MinChangeAmount: 10000, } selectedCoins, err := selector.CoinSelect(req.fundingAmount, coins) if err != nil { l.coinSelectMtx.Unlock() req.err <- err req.resp <- nil return } // Lock the selected coins. These coins are now "reserved", this // prevents concurrent funding requests from referring to and this // double-spending the same set of coins. ourContribution.Inputs = make([]*wire.TxIn, len(selectedCoins.Coins())) for i, coin := range selectedCoins.Coins() { txout := wire.NewOutPoint(coin.Hash(), coin.Index()) l.LockOutpoint(*txout) // Empty sig script, we'll actually sign if this reservation is // queued up to be completed (the other side accepts). outPoint := wire.NewOutPoint(coin.Hash(), coin.Index()) ourContribution.Inputs[i] = wire.NewTxIn(outPoint, nil) } l.coinSelectMtx.Unlock() // Create some possibly neccessary change outputs. selectedTotalValue := coinset.NewCoinSet(selectedCoins.Coins()).TotalValue() if selectedTotalValue > req.fundingAmount { ourContribution.ChangeOutputs = make([]*wire.TxOut, 1) // Change is necessary. Query for an available change address to // send the remainder to. changeAddr, err := l.NewChangeAddress(waddrmgr.DefaultAccountNum) if err != nil { req.err <- err req.resp <- nil return } changeAddrScript, err := txscript.PayToAddrScript(changeAddr) if err != nil { req.err <- err req.resp <- nil return } changeAmount := selectedTotalValue - req.fundingAmount ourContribution.ChangeOutputs[0] = wire.NewTxOut(int64(changeAmount), changeAddrScript) } // TODO(roasbeef): re-calculate fees here to minFeePerKB, may need more inputs // Grab two fresh keys from out HD chain, one will be used for the // multi-sig funding transaction, and the other for the commitment // transaction. multiSigKey, err := l.getNextRawKey() if err != nil { req.err <- err req.resp <- nil return } commitKey, err := l.getNextRawKey() if err != nil { req.err <- err req.resp <- nil return } reservation.partialState.MultiSigKey = multiSigKey ourContribution.MultiSigKey = multiSigKey.PubKey() reservation.partialState.OurCommitKey = commitKey ourContribution.CommitKey = commitKey.PubKey() // Generate a fresh address to be used in the case of a cooperative // channel close. deliveryAddress, err := l.NewAddress(waddrmgr.DefaultAccountNum) if err != nil { req.err <- err req.resp <- nil return } reservation.partialState.OurDeliveryAddress = deliveryAddress ourContribution.DeliveryAddress = deliveryAddress // Create a new shaChain for verifiable transaction revocations. This // will be used to generate revocation hashes for our past/current // commitment transactions once we start to make payments within the // channel. shaChain, err := shachain.NewFromSeed(nil, 0) if err != nil { req.err <- err req.resp <- nil return } reservation.partialState.OurShaChain = shaChain copy(ourContribution.RevocationHash[:], shaChain.CurrentRevocationHash()) // Funding reservation request succesfully handled. The funding inputs // will be marked as unavailable until the reservation is either // completed, or cancecled. req.resp <- reservation req.err <- nil }