diff --git a/chainntnfs/btcdnotify/btcd.go b/chainntnfs/btcdnotify/btcd.go index 257a9bab..18d41191 100644 --- a/chainntnfs/btcdnotify/btcd.go +++ b/chainntnfs/btcdnotify/btcd.go @@ -21,6 +21,11 @@ const ( notifierType = "btcd" ) +var ( + ErrChainNotifierShuttingDown = errors.New("chainntnfs: system interrupt " + + "while attempting to register for spend notification.") +) + // chainUpdate encapsulates an update to the current main chain. This struct is // used as an element within an unbounded queue in order to avoid blocking the // main rpc dispatch rule. @@ -549,8 +554,7 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint) (*chainntnfs.S select { case <-b.quit: - return nil, errors.New("chainntnfs: system interrupt while " + - "attempting to register for spend notification.") + return nil, ErrChainNotifierShuttingDown case b.notificationRegistry <- ntfn: } @@ -611,8 +615,7 @@ func (b *BtcdNotifier) RegisterConfirmationsNtfn(txid *wire.ShaHash, select { case <-b.quit: - return nil, errors.New("chainntnfs: system interrupt while " + - "attempting to register for confirmation notification.") + return nil, ErrChainNotifierShuttingDown case b.notificationRegistry <- ntfn: return &chainntnfs.ConfirmationEvent{ Confirmed: ntfn.finConf, diff --git a/lnd_test.go b/lnd_test.go index c79f469b..23a2b9fd 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -172,7 +172,7 @@ func openChannelAndAssert(t *harnessTest, net *networkHarness, ctx context.Conte func closeChannelAndAssert(t *harnessTest, net *networkHarness, ctx context.Context, node *lightningNode, fundingChanPoint *lnrpc.ChannelPoint, force bool) *wire.ShaHash { - closeUpdates, err := net.CloseChannel(ctx, node, fundingChanPoint, force) + closeUpdates, _, err := net.CloseChannel(ctx, node, fundingChanPoint, force) if err != nil { t.Fatalf("unable to close channel: %v", err) } @@ -275,11 +275,13 @@ func testChannelBalance(net *networkHarness, t *harnessTest) { // force closes the channel after some cursory assertions. Within the test, two // transactions should be broadcast on-chain, the commitment transaction itself // (which closes the channel), and the sweep transaction a few blocks later -// once the output(s) become mature. +// once the output(s) become mature. This test also includes several restarts +// to ensure that the transaction output states are persisted throughout +// the forced closure process. // -// TODO(roabeef): also add an unsettled HTLC before force closing. +// TODO(roasbeef): also add an unsettled HTLC before force closing. func testChannelForceClosure(net *networkHarness, t *harnessTest) { - timeout := time.Duration(time.Second * 5) + timeout := time.Duration(time.Second * 10) ctxb := context.Background() // First establish a channel ween with a capacity of 100k satoshis @@ -306,28 +308,63 @@ func testChannelForceClosure(net *networkHarness, t *harnessTest) { // the channel. This will also assert that the commitment transaction // was immediately broadcast in order to fulfill the force closure // request. - closeUpdate, err := net.CloseChannel(ctxb, net.Alice, chanPoint, true) + _, closingTxID, err := net.CloseChannel(ctxb, net.Alice, chanPoint, true) if err != nil { t.Fatalf("unable to execute force channel closure: %v", err) } + // The several restarts in this test are intended to ensure that when a + // channel is force-closed, the UTXO nursery has persisted the state of + // the channel in the closure process and will recover the correct state + // when the system comes back on line. This restart tests state + // persistence at the beginning of the process, when the commitment + // transaction has been broadcast but not yet confirmed in a block. + if err := net.RestartNode(net.Alice, nil); err != nil { + t.Fatalf("Node restart failed: %v", err) + } // Mine a block which should confirm the commitment transaction // broadcast as a result of the force closure. if _, err := net.Miner.Node.Generate(1); err != nil { t.Fatalf("unable to generate block: %v", err) } - ctxt, _ = context.WithTimeout(ctxb, timeout) - closingTxID, err := net.WaitForChannelClose(ctxt, closeUpdate) - if err != nil { - t.Fatalf("error while waiting for channel close: %v", err) + + // The following sleep provides time for the UTXO nursery to move the + // output from the preschool to the kindergarten database buckets + // prior to RestartNode() being triggered. Without this sleep, the + // database update may fail, causing the UTXO nursery to retry the move + // operation upon restart. This will change the blockheights from what + // is expected by the test. + // TODO(bvu): refactor out this sleep. + duration := time.Millisecond * 300 + time.Sleep(duration) + + // The following restart is intended to ensure that outputs from the + // force close commitment transaction have been persisted once the + // transaction has been confirmed, but before the outputs are spendable + // (the "kindergarten" bucket.) + if err := net.RestartNode(net.Alice, nil); err != nil { + t.Fatalf("Node restart failed: %v", err) } // Currently within the codebase, the default CSV is 4 relative blocks. - // So generate exactly 4 new blocks. + // For the persistence test, we generate three blocks, then trigger + // a restart and then generate the final block that should trigger + // the creation of the sweep transaction. // TODO(roasbeef): should check default value in config here instead, // or make delay a param const defaultCSV = 4 - if _, err := net.Miner.Node.Generate(defaultCSV); err != nil { + if _, err := net.Miner.Node.Generate(defaultCSV - 1); err != nil { + t.Fatalf("unable to mine blocks: %v", err) + } + + // The following restart checks to ensure that outputs in the kindergarten + // bucket are persisted while waiting for the required number of + // confirmations to be reported. + if err := net.RestartNode(net.Alice, nil); err != nil { + t.Fatalf("Node restart failed: %v", err) + } + + if _, err := net.Miner.Node.Generate(1); err != nil { t.Fatalf("unable to mine blocks: %v", err) } @@ -336,20 +373,21 @@ func testChannelForceClosure(net *networkHarness, t *harnessTest) { // broadcast. var sweepingTXID *wire.ShaHash var mempool []*wire.ShaHash + mempoolTimeout := time.After(3 * time.Second) + checkMempoolTick := time.Tick(100 * time.Millisecond) mempoolPoll: for { select { - case <-time.After(time.Second * 5): + case <-mempoolTimeout: t.Fatalf("sweep tx not found in mempool") - default: + case <-checkMempoolTick: mempool, err = net.Miner.Node.GetRawMempool() if err != nil { t.Fatalf("unable to fetch node's mempool: %v", err) } - if len(mempool) == 0 { - continue + if len(mempool) != 0 { + break mempoolPoll } - break mempoolPoll } } diff --git a/networktest.go b/networktest.go index c95825b8..f346b979 100644 --- a/networktest.go +++ b/networktest.go @@ -734,7 +734,7 @@ func (n *networkHarness) WaitForChannelOpen(ctx context.Context, // pending, then an error is returned. func (n *networkHarness) CloseChannel(ctx context.Context, lnNode *lightningNode, cp *lnrpc.ChannelPoint, - force bool) (lnrpc.Lightning_CloseChannelClient, error) { + force bool) (lnrpc.Lightning_CloseChannelClient, *wire.ShaHash, error) { closeReq := &lnrpc.CloseChannelRequest{ ChannelPoint: cp, @@ -742,11 +742,11 @@ func (n *networkHarness) CloseChannel(ctx context.Context, } closeRespStream, err := lnNode.CloseChannel(ctx, closeReq) if err != nil { - return nil, fmt.Errorf("unable to close channel: %v", err) + return nil, nil, fmt.Errorf("unable to close channel: %v", err) } errChan := make(chan error) - fin := make(chan struct{}) + fin := make(chan *wire.ShaHash) go func() { // Consume the "channel close" update in order to wait for the closing // transaction to be broadcast, then wait for the closing tx to be seen @@ -772,20 +772,19 @@ func (n *networkHarness) CloseChannel(ctx context.Context, errChan <- err return } - - close(fin) + fin <- closeTxid }() // Wait until either the deadline for the context expires, an error // occurs, or the channel close update is received. select { case <-ctx.Done(): - return nil, fmt.Errorf("timeout reached before channel close " + + return nil, nil, fmt.Errorf("timeout reached before channel close " + "initiated") case err := <-errChan: - return nil, err - case <-fin: - return closeRespStream, nil + return nil, nil, err + case closeTxid := <-fin: + return closeRespStream, closeTxid, nil } } diff --git a/server.go b/server.go index fef7c75e..6b921ce3 100644 --- a/server.go +++ b/server.go @@ -108,7 +108,7 @@ func newServer(listenAddrs []string, notifier chainntnfs.ChainNotifier, chanDB: chanDB, invoices: newInvoiceRegistry(chanDB), - utxoNursery: newUtxoNursery(notifier, wallet), + utxoNursery: newUtxoNursery(chanDB, notifier, wallet), htlcSwitch: newHtlcSwitch(), identityPriv: privKey, diff --git a/utxonursery.go b/utxonursery.go index f92bf417..dfe0d3d2 100644 --- a/utxonursery.go +++ b/utxonursery.go @@ -1,18 +1,85 @@ package main import ( + "bytes" + "encoding/binary" + "errors" + "io" "sync" "sync/atomic" + "github.com/boltdb/bolt" "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/txscript" "github.com/roasbeef/btcd/wire" "github.com/roasbeef/btcutil" ) +var ( + // preschoolBucket stores outputs from commitment transactions that + // have been broadcast, but not yet confirmed. This set of outputs is + // persisted in case the system is shut down between the time when + // the commitment has been broadcast and the time the transaction + // has been confirmed on the blockchain. + preschoolBucket = []byte("psc") + + // kindergartenBucket stores outputs from commitment transactions that + // have received an initial confirmation, but which aren't yet spendable + // because they require additional confirmations enforced by Check + // Sequence Verify. Once required additional confirmations have been reported, + // a sweep transaction will be created to move the funds out of these + // outputs. After a further six confirmations have been reported, the outputs + // will be deleted from this bucket. The purpose of this additional wait + // time is to ensure that a block reorganization doesn't result in the + // sweep transaction getting re-organized out of the chain. + kindergartenBucket = []byte("kdg") + + // lastGraduatedHeightKey is used to persist the last blockheight that + // has been checked for graduating outputs. When the nursery is restarted, + // lastGraduatedHeightKey is used to determine the point from which it's + // necessary to catch up. + lastGraduatedHeightKey = []byte("lgh") + + byteOrder = binary.BigEndian +) + +// witnessType determines how an output's witness will be generated. The default +// commitmentTimeLock type will generate a witness that will allow spending of a +// time-locked transaction enforced by CheckSequenceVerify. +type witnessType uint16 + +const ( + commitmentTimeLock witnessType = 0 +) + +// witnessGenerator represents a function which is able to generate the final +// witness for a particular public key script. This function acts as an +// abstraction layer, hiding the details of the underlying script from the +// utxoNursery. +type witnessGenerator func(tx *wire.MsgTx, hc *txscript.TxSigHashes, inputIndex int) ([][]byte, error) + +// generateFunc will return the witnessGenerator function that a kidOutput uses to +// generate the witness for a sweep transaction. Currently there is only one witnessType +// but this will be expanded. +func (wt *witnessType) generateFunc(signer *lnwallet.Signer, descriptor *lnwallet.SignDescriptor) witnessGenerator { + switch *wt { + case commitmentTimeLock: + return func(tx *wire.MsgTx, hc *txscript.TxSigHashes, inputIndex int) ([][]byte, error) { + desc := descriptor + desc.SigHashes = hc + desc.InputIndex = inputIndex + + return lnwallet.CommitSpendTimeout(*signer, desc, tx) + } + } + + return nil +} + // utxoNursery is a system dedicated to incubating time-locked outputs created // by the broadcast of a commitment transaction either by us, or the remote // peer. The nursery accepts outputs and "incubates" them until they've reached @@ -27,14 +94,10 @@ type utxoNursery struct { notifier chainntnfs.ChainNotifier wallet *lnwallet.LightningWallet - db channeldb.DB + db *channeldb.DB requests chan *incubationRequest - // TODO(roasbeef): persist to disk afterwards - unstagedOutputs map[wire.OutPoint]*immatureOutput - stagedOutputs map[uint32][]*immatureOutput - started uint32 stopped uint32 quit chan struct{} @@ -43,16 +106,15 @@ type utxoNursery struct { // newUtxoNursery creates a new instance of the utxoNursery from a // ChainNotifier and LightningWallet instance. -func newUtxoNursery(notifier chainntnfs.ChainNotifier, +func newUtxoNursery(db *channeldb.DB, notifier chainntnfs.ChainNotifier, wallet *lnwallet.LightningWallet) *utxoNursery { return &utxoNursery{ - notifier: notifier, - wallet: wallet, - requests: make(chan *incubationRequest), - unstagedOutputs: make(map[wire.OutPoint]*immatureOutput), - stagedOutputs: make(map[uint32][]*immatureOutput), - quit: make(chan struct{}), + notifier: notifier, + wallet: wallet, + requests: make(chan *incubationRequest), + db: db, + quit: make(chan struct{}), } } @@ -65,13 +127,114 @@ func (u *utxoNursery) Start() error { utxnLog.Tracef("Starting UTXO nursery") + if err := u.reloadPreschool(); err != nil { + return err + } + + // Register with the notifier to receive notifications for each newly + // connected block. We register during startup to ensure that no blocks + // are missed while we are handling blocks that were missed during the + // time the UTXO nursery was unavailable. + newBlockChan, err := u.notifier.RegisterBlockEpochNtfn() + if err != nil { + return err + } + if err := u.catchUpKindergarten(); err != nil { + return err + } + u.wg.Add(1) - go u.incubator() + go u.incubator(newBlockChan) return nil } -// Stop gracefully shutsdown any lingering goroutines launched during normal +// reloadPreschool re-initializes the chain notifier with all of the outputs +// that had been saved to the "preschool" database bucket prior to shutdown. +func (u *utxoNursery) reloadPreschool() error { + err := u.db.View(func(tx *bolt.Tx) error { + psclBucket := tx.Bucket(preschoolBucket) + if psclBucket == nil { + return nil + } + if err := psclBucket.ForEach(func(outputBytes, kidBytes []byte) error { + psclOutput, err := deserializeKidOutput(bytes.NewBuffer(kidBytes)) + + outpoint := psclOutput.outPoint + sourceTxid := outpoint.Hash + + confChan, err := u.notifier.RegisterConfirmationsNtfn(&sourceTxid, 1) + if err != nil { + return err + } + + utxnLog.Infof("Preschool outpoint %v re-registered for confirmation "+ + "notification.", psclOutput.outPoint) + go psclOutput.waitForPromotion(u.db, confChan) + return nil + }); err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + + return nil +} + +// catchUpKindergarten handles the graduation of kindergarten outputs from +// blocks that were missed while the UTXO Nursery was down or offline. +// graduateMissedBlocks is called during the startup of the UTXO Nursery. +func (u *utxoNursery) catchUpKindergarten() error { + var lastGraduatedHeight uint32 + + // Query the database for the most recently processed block + err := u.db.View(func(tx *bolt.Tx) error { + kgtnBucket := tx.Bucket(kindergartenBucket) + if kgtnBucket == nil { + return nil + } + heightBytes := kgtnBucket.Get(lastGraduatedHeightKey) + if heightBytes == nil { + return nil + } + + lastGraduatedHeight = byteOrder.Uint32(heightBytes) + return nil + }) + if err != nil { + return err + } + + // Get the most recently mined block + _, bestHeight, err := u.wallet.ChainIO.GetBestBlock() + if err != nil { + return err + } + + // Loop through and check for graduating outputs at each of the missed + // block heights. + if lastGraduatedHeight != 0 { + graduationHeight := lastGraduatedHeight + 1 + + utxnLog.Infof("Processing outputs from missed blocks. Starting with "+ + "blockheight: %v, to current blockheight: %v", graduationHeight, + bestHeight) + + for graduationHeight <= uint32(bestHeight) { + if err := u.graduateKindergarten(graduationHeight); err != nil { + return err + } + graduationHeight = graduationHeight + 1 + } + } + + return nil +} + +// Stop gracefully shuts down any lingering goroutines launched during normal // operation of the utxoNursery. func (u *utxoNursery) Stop() error { if !atomic.CompareAndSwapUint32(&u.stopped, 0, 1) { @@ -86,87 +249,94 @@ func (u *utxoNursery) Stop() error { return nil } -// incubator is tasked with watching over all immature outputs until they've -// reached "maturity", after which they'll be swept into the underlying wallet -// in batches within a single transaction. Immature outputs can be divided into -// three stages: early stage, mid stage, and final stage. During the early -// stage, the transaction containing the output has not yet been confirmed. -// Once the txn creating the output is confirmed, then output moves to the mid -// stage wherein a dedicated goroutine waits until it has reached "maturity". -// Once an output is mature, it will be swept into the wallet at the earlier -// possible height. -func (u *utxoNursery) incubator() { - defer u.wg.Done() +// kidOutput represents an output that's waiting for a required blockheight +// before its funds will be available to be moved into the user's wallet. +// The struct includes a witnessGenerator closure which will be used to +// generate the witness required to sweep the output once it's mature. +// TODO(roasbeef): make into interface? can't gob functions +type kidOutput struct { + amt btcutil.Amount + outPoint wire.OutPoint - // Register with the notifier to receive notifications for each newly - // connected block. - newBlocks, err := u.notifier.RegisterBlockEpochNtfn() - if err != nil { - utxnLog.Errorf("unable to register for block epoch "+ - "notifications: %v", err) + witnessFunc witnessGenerator + + // TODO(roasbeef): using block timeouts everywhere currently, will need + // to modify logic later to account for MTP based timeouts. + blocksToMaturity uint32 + confHeight uint32 + + signDescriptor *lnwallet.SignDescriptor + witnessType witnessType +} + +// incubationRequest is a request to the utxoNursery to incubate a set of +// outputs until their mature, finally sweeping them into the wallet once +// available. +type incubationRequest struct { + outputs []*kidOutput +} + +// incubateOutputs sends a request to utxoNursery to incubate the outputs +// defined within the summary of a closed channel. Individually, as all outputs +// reach maturity they'll be swept back into the wallet. +func (u *utxoNursery) incubateOutputs(closeSummary *lnwallet.ForceCloseSummary) { + outputAmt := btcutil.Amount(closeSummary.SelfOutputSignDesc.Output.Value) + selfOutput := &kidOutput{ + amt: outputAmt, + outPoint: closeSummary.SelfOutpoint, + blocksToMaturity: closeSummary.SelfOutputMaturity, + signDescriptor: closeSummary.SelfOutputSignDesc, + witnessType: commitmentTimeLock, } - // Outputs that are transitioning from early to mid-stage are sent over - // this channel by each output's dedicated watcher goroutine. - midStageOutputs := make(chan *immatureOutput) + u.requests <- &incubationRequest{ + outputs: []*kidOutput{selfOutput}, + } +} + +// incubator is tasked with watching over all outputs from channel closes as they +// transition from being broadcast (at which point they move into the "preschool +// state"), then confirmed and waiting for the necessary number of blocks to +// be confirmed (as specified as kidOutput.blocksToMaturity and enforced by +// CheckSequenceVerify). When the necessary block height has been reached, the +// output has "matured" and the waitForGraduation function will generate a +// sweep transaction to move funds from the commitment transaction into the +// user's wallet. +func (u *utxoNursery) incubator(newBlockChan *chainntnfs.BlockEpochEvent) { + defer u.wg.Done() + out: for { select { - case earlyStagers := <-u.requests: + case preschoolRequest := <-u.requests: utxnLog.Infof("Incubating %v new outputs", - len(earlyStagers.outputs)) + len(preschoolRequest.outputs)) - for _, immatureUtxo := range earlyStagers.outputs { - outpoint := immatureUtxo.outPoint - sourceTXID := outpoint.Hash + for _, output := range preschoolRequest.outputs { + sourceTxid := output.outPoint.Hash - // Register for a confirmation once the - // generating txn has been confirmed. - confChan, err := u.notifier.RegisterConfirmationsNtfn(&sourceTXID, 1) - if err != nil { - utxnLog.Errorf("unable to register for confirmations "+ - "for txid: %v", sourceTXID) + if err := output.enterPreschool(u.db); err != nil { + utxnLog.Errorf("unable to add kidOutput to preschool: %v, %v ", + output, err) continue } - // TODO(roasbeef): should be an on-disk - // at-least-once task queue - u.unstagedOutputs[outpoint] = immatureUtxo + // Register for a notification that will trigger graduation from + // preschool to kindergarten when the channel close transaction + // has been confirmed. + confChan, err := u.notifier.RegisterConfirmationsNtfn(&sourceTxid, 1) + if err != nil { + utxnLog.Errorf("unable to register output for confirmation: %v", + sourceTxid) + continue + } - // Launch a dedicated goroutine which will send - // the output back to the incubator once the - // source txn has been confirmed. - go func() { - confDetails, ok := <-confChan.Confirmed - if !ok { - utxnLog.Errorf("notification chan "+ - "closed, can't advance output %v", outpoint) - return - } - - confHeight := uint32(confDetails.BlockHeight) - utxnLog.Infof("Outpoint %v confirmed in "+ - "block %v moving to mid-stage", - outpoint, confHeight) - immatureUtxo.confHeight = confHeight - midStageOutputs <- immatureUtxo - }() + // Launch a dedicated goroutine that will move the output from + // the preschool bucket to the kindergarten bucket once the + // channel close transaction has been confirmed. + go output.waitForPromotion(u.db, confChan) } - // TODO(roasbeef): rename to preschool and kindergarden - case midUtxo := <-midStageOutputs: - // The transaction creating the output has been - // created, so we move it from early stage to - // mid-stage. - delete(u.unstagedOutputs, midUtxo.outPoint) - - // TODO(roasbeef): your off-by-one sense are tingling... - maturityHeight := midUtxo.confHeight + midUtxo.blocksToMaturity - u.stagedOutputs[maturityHeight] = append(u.stagedOutputs[maturityHeight], midUtxo) - - utxnLog.Infof("Outpoint %v now mid-stage, will mature "+ - "at height %v (delay of %v)", midUtxo.outPoint, - maturityHeight, midUtxo.blocksToMaturity) - case epoch, ok := <-newBlocks.Epochs: + case epoch, ok := <-newBlockChan.Epochs: // If the epoch channel has been closed, then the // ChainNotifier is exiting which means the daemon is // as well. Therefore, we exit early also in order to @@ -175,59 +345,244 @@ out: return } - // A new block has just been connected, check to see if - // we have any new outputs that can be swept into the - // wallet. - newHeight := uint32(epoch.Height) - matureOutputs, ok := u.stagedOutputs[newHeight] - if !ok { - continue + if err := u.graduateKindergarten(uint32(epoch.Height)); err != nil { + utxnLog.Errorf("error while graduating kindergarten outputs: %v", err) } - utxnLog.Infof("New block: height=%v hash=%v, "+ - "sweeping %v mature outputs", newHeight, - epoch.Hash, len(matureOutputs)) - - // Create a transation which sweeps all the newly - // mature outputs into a output controlled by the - // wallet. - // TODO(roasbeef): can be more intelligent about - // buffering outputs to be more efficient on-chain. - sweepTx, err := u.createSweepTx(matureOutputs) - if err != nil { - // TODO(roasbeef): retry logic? - utxnLog.Errorf("unable to create sweep tx: %v", err) - continue - } - - utxnLog.Infof("Sweeping %v time-locked outputs "+ - "with sweep tx: %v", len(matureOutputs), - newLogClosure(func() string { - return spew.Sdump(sweepTx) - })) - - // With the sweep transaction fully signed, broadcast - // the transaction to the network. Additionally, we can - // stop tracking these outputs as they've just been - // sweeped. - err = u.wallet.PublishTransaction(sweepTx) - if err != nil { - utxnLog.Errorf("unable to broadcast sweep tx: %v, %v", - err, spew.Sdump(sweepTx)) - continue - } - delete(u.stagedOutputs, newHeight) case <-u.quit: break out } } } +// enterPreschool is the first stage in the process of transferring funds from +// a force closed channel into the user's wallet. When an output is in the +// "preschool" stage, the daemon is waiting for the initial confirmation of the +// commitment transaction. +func (k *kidOutput) enterPreschool(db *channeldb.DB) error { + err := db.Update(func(tx *bolt.Tx) error { + psclBucket, err := tx.CreateBucketIfNotExists(preschoolBucket) + if err != nil { + return err + } + + var outpointBytes bytes.Buffer + if err := writeOutpoint(&outpointBytes, &k.outPoint); err != nil { + return err + } + + var kidBytes bytes.Buffer + if err := serializeKidOutput(&kidBytes, k); err != nil { + return err + } + + if err := psclBucket.Put(outpointBytes.Bytes(), kidBytes.Bytes()); err != nil { + return err + } + + utxnLog.Infof("Outpoint %v now in preschool, waiting for "+ + "initial confirmation", k.outPoint) + + return nil + }) + if err != nil { + return err + } + + return nil +} + +// waitForPromotion is intended to be run as a goroutine that will wait until +// a channel force close commitment transaction has been included in a +// confirmed block. Once the transaction has been confirmed (as reported by +// the Chain Notifier), waitForPromotion will delete the output from the +// "preschool" database bucket and atomically add it to the "kindergarten" +// database bucket. This is the second step in the output incubation process. +func (k *kidOutput) waitForPromotion(db *channeldb.DB, confChan *chainntnfs.ConfirmationEvent) { + txConfirmation, ok := <-confChan.Confirmed + if !ok { + utxnLog.Errorf("notification chan "+ + "closed, can't advance output %v", k.outPoint) + return + } + + utxnLog.Infof("Outpoint %v confirmed in block %v moving to kindergarten", + k.outPoint, txConfirmation.BlockHeight) + + k.confHeight = uint32(txConfirmation.BlockHeight) + + // The following block deletes a kidOutput from the preschool database bucket + // and adds it to the kindergarten database bucket which is keyed by block + // height. Keys and values are serialized into byte array form prior to + // database insertion. + err := db.Update(func(tx *bolt.Tx) error { + psclBucket := tx.Bucket(preschoolBucket) + if psclBucket == nil { + return errors.New("unable to open preschool bucket") + } + + var outpointBytes bytes.Buffer + if err := writeOutpoint(&outpointBytes, &k.outPoint); err != nil { + return err + } + if err := psclBucket.Delete(outpointBytes.Bytes()); err != nil { + utxnLog.Errorf("unable to delete kindergarten output from "+ + "preschool bucket: %v", k.outPoint) + return err + } + + kgtnBucket, err := tx.CreateBucketIfNotExists(kindergartenBucket) + if err != nil { + return err + } + + maturityHeight := k.confHeight + + k.blocksToMaturity + heightBytes := make([]byte, 4) + byteOrder.PutUint32(heightBytes, uint32(maturityHeight)) + + var existingOutputs []byte + if results := kgtnBucket.Get(heightBytes); results != nil { + existingOutputs = results + } + + b := bytes.NewBuffer(existingOutputs) + if err := serializeKidOutput(b, k); err != nil { + return err + } + + if err := kgtnBucket.Put(heightBytes, b.Bytes()); err != nil { + return err + } + + utxnLog.Infof("Outpoint %v now in kindergarten, will mature "+ + "at height %v (delay of %v)", k.outPoint, + maturityHeight, k.blocksToMaturity) + return nil + }) + if err != nil { + utxnLog.Errorf("unable to move kid output from preschool bucket "+ + "to kindergarten bucket: %v", err) + } +} + +// graduateKindergarten handles the steps involed with moving funds +// from a force close commitment transaction into a user's wallet after the output +// from the commitment transaction has become spendable. graduateKindergarten +// is called both when a new block notification has been received and also +// at startup in order to process graduations from blocks missed while the +// UTXO nursery was offline. +func (u *utxoNursery) graduateKindergarten(blockHeight uint32) error { + kgtnOutputs, err := fetchGraduatingOutputs(u.db, u.wallet, blockHeight) + if err != nil { + return err + } + + if len(kgtnOutputs) > 0 { + if err := sweepGraduatingOutputs(u.wallet, kgtnOutputs); err != nil { + return err + } + } + + deleteHeight := blockHeight - 6 + if err := deleteGraduatedOutputs(u.db, deleteHeight); err != nil { + return err + } + + if err := putLastHeightGraduated(u.db, blockHeight); err != nil { + return err + } + + return nil +} + +// fetchGraduatingOutputs checks the "kindergarten" database bucket whenever a +// new block is received in order to determine if commitment transaction +// outputs have become newly spendable. If fetchGraduatingOutputs finds +// outputs that are ready for "graduation," it passes them on to be swept. +// This is the third step in the output incubation process. +func fetchGraduatingOutputs(db *channeldb.DB, wallet *lnwallet.LightningWallet, blockHeight uint32) ([]*kidOutput, error) { + var results []byte + + err := db.View(func(tx *bolt.Tx) error { + // A new block has just been connected, check to see if + // we have any new outputs that can be swept into the + // wallet. + kgtnBucket := tx.Bucket(kindergartenBucket) + if kgtnBucket == nil { + return nil + } + + heightBytes := make([]byte, 4) + byteOrder.PutUint32(heightBytes, blockHeight) + + results = kgtnBucket.Get(heightBytes) + + return nil + }) + if err != nil { + return nil, err + } + + if len(results) > 0 { + kgtnOutputs, err := deserializeKidList(bytes.NewBuffer(results)) + if err != nil { + utxnLog.Errorf("error while deserializing list of kidOutputs: %v", err) + } + + for _, kgtnOutput := range kgtnOutputs { + kgtnOutput.witnessFunc = + kgtnOutput.witnessType.generateFunc(&wallet.Signer, kgtnOutput.signDescriptor) + } + + utxnLog.Infof("New block: height=%v, sweeping %v mature outputs", + blockHeight, len(kgtnOutputs)) + + return kgtnOutputs, nil + } + + return nil, nil +} + +// sweepGraduatingOutputs generates and broadcasts the transaction that +// transfers control of funds from a channel commitment transaction to the +// user's wallet. +func sweepGraduatingOutputs(wallet *lnwallet.LightningWallet, kgtnOutputs []*kidOutput) error { + // Create a transation which sweeps all the newly + // mature outputs into a output controlled by the + // wallet. + // TODO(roasbeef): can be more intelligent about + // buffering outputs to be more efficient on-chain. + sweepTx, err := createSweepTx(wallet, kgtnOutputs) + if err != nil { + // TODO(roasbeef): retry logic? + utxnLog.Errorf("unable to create sweep tx: %v", err) + } + + utxnLog.Infof("Sweeping %v time-locked outputs "+ + "with sweep tx: %v", len(kgtnOutputs), + newLogClosure(func() string { + return spew.Sdump(sweepTx) + })) + + // With the sweep transaction fully signed, broadcast + // the transaction to the network. Additionally, we can + // stop tracking these outputs as they've just been + // sweeped. + err = wallet.PublishTransaction(sweepTx) + if err != nil { + utxnLog.Errorf("unable to broadcast sweep tx: %v, %v", + err, spew.Sdump(sweepTx)) + } + + return nil +} + // createSweepTx creates a final sweeping transaction with all witnesses in // place for all inputs. The created transaction has a single output sending // all the funds back to the source wallet. -func (u *utxoNursery) createSweepTx(matureOutputs []*immatureOutput) (*wire.MsgTx, error) { - pkScript, err := newSweepPkScript(u.wallet) +func createSweepTx(wallet *lnwallet.LightningWallet, matureOutputs []*kidOutput) (*wire.MsgTx, error) { + pkScript, err := newSweepPkScript(wallet) if err != nil { return nil, err } @@ -265,65 +620,72 @@ func (u *utxoNursery) createSweepTx(matureOutputs []*immatureOutput) (*wire.MsgT txIn.Witness = witness } - return sweepTx, nil } -// witnessGenerator represents a function which is able to generate the final -// witness for a particular public key script. This function acts as an -// abstraction layer, hiding the details of the underlying script from the -// utxoNursery. -type witnessGenerator func(tx *wire.MsgTx, hc *txscript.TxSigHashes, inputIndex int) ([][]byte, error) +// deleteGraduatedOutputs removes outputs from the kindergarten database bucket +// when six blockchain confirmations have passed since the outputs were swept. +// We wait for six confirmations to ensure that the outputs will be swept if a +// chain reorganization occurs. This is the final step in the output incubation +// process. +func deleteGraduatedOutputs(db *channeldb.DB, deleteHeight uint32) error { + err := db.Update(func(tx *bolt.Tx) error { + kgtnBucket := tx.Bucket(kindergartenBucket) + if kgtnBucket == nil { + return nil + } -// immatureOutput encapsulates an immature output. The struct includes a -// witnessGenerator closure which will be used to generate the witness required -// to sweep the output once it's mature. -// TODO(roasbeef): make into interface? can't gob functions -type immatureOutput struct { - amt btcutil.Amount - outPoint wire.OutPoint + heightBytes := make([]byte, 4) + byteOrder.PutUint32(heightBytes, uint32(deleteHeight)) + results := kgtnBucket.Get(heightBytes) + if results == nil { + return nil + } - witnessFunc witnessGenerator + sweptOutputs, err := deserializeKidList(bytes.NewBuffer(results)) + if err != nil { + return err + } - // TODO(roasbeef): using block timeouts everywhere currently, will need - // to modify logic later to account for MTP based timeouts. - blocksToMaturity uint32 - confHeight uint32 + if err := kgtnBucket.Delete(heightBytes); err != nil { + return err + } + + utxnLog.Info("Deleting %v swept outputs from kindergarten bucket "+ + "at block height: %v", len(sweptOutputs), deleteHeight) + + return nil + }) + if err != nil { + return err + } + + return nil } -// incubationRequest is a request to the utxoNursery to incubate a set of -// outputs until their mature, finally sweeping them into the wallet once -// available. -type incubationRequest struct { - outputs []*immatureOutput -} +// putLastHeightGraduated persists the most recently processed blockheight +// to the database. This blockheight is used during restarts to determine if +// blocks were missed while the UTXO Nursery was offline. +func putLastHeightGraduated(db *channeldb.DB, blockheight uint32) error { + err := db.Update(func(tx *bolt.Tx) error { + kgtnBucket, err := tx.CreateBucketIfNotExists(kindergartenBucket) + if err != nil { + return nil + } -// incubateOutputs sends a request to utxoNursery to incubate the outputs -// defined within the summary of a closed channel. Individually, as all outputs -// reach maturity they'll be swept back into the wallet. -func (u *utxoNursery) incubateOutputs(closeSummary *lnwallet.ForceCloseSummary) { - // TODO(roasbeef): should use factory func here based on an interface - // * interface type stored on disk next to record - // * spend here also assumes delay is blocked bsaed, and in range - witnessFunc := func(tx *wire.MsgTx, hc *txscript.TxSigHashes, inputIndex int) ([][]byte, error) { - desc := closeSummary.SelfOutputSignDesc - desc.SigHashes = hc - desc.InputIndex = inputIndex + heightBytes := make([]byte, 4) + byteOrder.PutUint32(heightBytes, blockheight) + if err := kgtnBucket.Put(lastGraduatedHeightKey, heightBytes); err != nil { + return err + } - return lnwallet.CommitSpendTimeout(u.wallet.Signer, desc, tx) + return nil + }) + if err != nil { + return err } - outputAmt := btcutil.Amount(closeSummary.SelfOutputSignDesc.Output.Value) - selfOutput := &immatureOutput{ - amt: outputAmt, - outPoint: closeSummary.SelfOutpoint, - witnessFunc: witnessFunc, - blocksToMaturity: closeSummary.SelfOutputMaturity, - } - - u.requests <- &incubationRequest{ - outputs: []*immatureOutput{selfOutput}, - } + return nil } // newSweepPkScript creates a new public key script which should be used to @@ -338,3 +700,219 @@ func newSweepPkScript(wallet lnwallet.WalletController) ([]byte, error) { return txscript.PayToAddrScript(sweepAddr) } + +// deserializedKidList takes a sequence of serialized kid outputs and returns a +// slice of kidOutput structs. +func deserializeKidList(r io.Reader) ([]*kidOutput, error) { + var kidOutputs []*kidOutput + + for { + kidOutput, err := deserializeKidOutput(r) + if err != nil { + if err == io.EOF { + break + } else { + return nil, err + } + } + kidOutputs = append(kidOutputs, kidOutput) + } + + return kidOutputs, nil +} + +// serializeKidOutput converts a KidOutput struct into a form +// suitable for on-disk database storage. Note that the signDescriptor +// struct field is included so that the output's witness can be generated +// by createSweepTx() when the output becomes spendable. +func serializeKidOutput(w io.Writer, kid *kidOutput) error { + var scratch [8]byte + byteOrder.PutUint64(scratch[:], uint64(kid.amt)) + if _, err := w.Write(scratch[:]); err != nil { + return err + } + + if err := writeOutpoint(w, &kid.outPoint); err != nil { + return err + } + + byteOrder.PutUint32(scratch[:4], kid.blocksToMaturity) + if _, err := w.Write(scratch[:4]); err != nil { + return err + } + + byteOrder.PutUint32(scratch[:4], kid.confHeight) + if _, err := w.Write(scratch[:4]); err != nil { + return err + } + + byteOrder.PutUint16(scratch[:2], uint16(kid.witnessType)) + if _, err := w.Write(scratch[:2]); err != nil { + return err + } + + serializedPubKey := kid.signDescriptor.PubKey.SerializeCompressed() + if err := wire.WriteVarBytes(w, 0, serializedPubKey); err != nil { + return err + } + + if err := wire.WriteVarBytes(w, 0, kid.signDescriptor.PrivateTweak); err != nil { + return err + } + + if err := wire.WriteVarBytes(w, 0, kid.signDescriptor.WitnessScript); err != nil { + return err + } + + if err := writeTxOut(w, kid.signDescriptor.Output); err != nil { + return err + } + + byteOrder.PutUint32(scratch[:4], uint32(kid.signDescriptor.HashType)) + if _, err := w.Write(scratch[:4]); err != nil { + return err + } + + return nil +} + +// deserializeKidOutput takes a byte array representation of a kidOutput +// and converts it to an struct. Note that the witnessFunc method isn't added +// during deserialization and must be added later based on the value of the +// witnessType field. +func deserializeKidOutput(r io.Reader) (*kidOutput, error) { + scratch := make([]byte, 8) + + kid := &kidOutput{} + + if _, err := r.Read(scratch[:]); err != nil { + return nil, err + } + kid.amt = btcutil.Amount(byteOrder.Uint64(scratch[:])) + + if err := readOutpoint(io.LimitReader(r, 40), &kid.outPoint); err != nil { + return nil, err + } + + if _, err := r.Read(scratch[:4]); err != nil { + return nil, err + } + kid.blocksToMaturity = byteOrder.Uint32(scratch[:4]) + + if _, err := r.Read(scratch[:4]); err != nil { + return nil, err + } + kid.confHeight = byteOrder.Uint32(scratch[:4]) + + if _, err := r.Read(scratch[:2]); err != nil { + return nil, err + } + kid.witnessType = witnessType(byteOrder.Uint16(scratch[:2])) + + kid.signDescriptor = &lnwallet.SignDescriptor{} + + descKeyBytes, err := wire.ReadVarBytes(r, 0, 34, "descKeyBytes") + if err != nil { + return nil, err + } + + descKey, err := btcec.ParsePubKey(descKeyBytes, btcec.S256()) + if err != nil { + return nil, err + } + kid.signDescriptor.PubKey = descKey + + descPrivateTweak, err := wire.ReadVarBytes(r, 0, 32, "privateTweak") + if err != nil { + return nil, err + } + kid.signDescriptor.PrivateTweak = descPrivateTweak + + descWitnessScript, err := wire.ReadVarBytes(r, 0, 100, "witnessScript") + if err != nil { + return nil, err + } + kid.signDescriptor.WitnessScript = descWitnessScript + + descTxOut := &wire.TxOut{} + if err := readTxOut(r, descTxOut); err != nil { + return nil, err + } + kid.signDescriptor.Output = descTxOut + + if _, err := r.Read(scratch[:4]); err != nil { + return nil, err + } + kid.signDescriptor.HashType = txscript.SigHashType(byteOrder.Uint32(scratch[:4])) + + return kid, nil +} + +// TODO(bvu): copied from channeldb, remove repetition +func writeOutpoint(w io.Writer, o *wire.OutPoint) error { + // TODO(roasbeef): make all scratch buffers on the stack + scratch := make([]byte, 4) + + // TODO(roasbeef): write raw 32 bytes instead of wasting the extra + // byte. + if err := wire.WriteVarBytes(w, 0, o.Hash[:]); err != nil { + return err + } + + byteOrder.PutUint32(scratch, o.Index) + if _, err := w.Write(scratch); err != nil { + return err + } + + return nil +} + +// TODO(bvu): copied from channeldb, remove repetition +func readOutpoint(r io.Reader, o *wire.OutPoint) error { + scratch := make([]byte, 4) + + txid, err := wire.ReadVarBytes(r, 0, 32, "prevout") + if err != nil { + return err + } + copy(o.Hash[:], txid) + + if _, err := r.Read(scratch); err != nil { + return err + } + o.Index = byteOrder.Uint32(scratch) + + return nil +} + +func writeTxOut(w io.Writer, txo *wire.TxOut) error { + scratch := make([]byte, 8) + + byteOrder.PutUint64(scratch, uint64(txo.Value)) + if _, err := w.Write(scratch); err != nil { + return err + } + + if err := wire.WriteVarBytes(w, 0, txo.PkScript); err != nil { + return err + } + + return nil +} + +func readTxOut(r io.Reader, txo *wire.TxOut) error { + scratch := make([]byte, 8) + + if _, err := r.Read(scratch); err != nil { + return err + } + txo.Value = int64(byteOrder.Uint64(scratch)) + + pkScript, err := wire.ReadVarBytes(r, 0, 80, "pkScript") + if err != nil { + return err + } + txo.PkScript = pkScript + + return nil +} diff --git a/utxonursery_test.go b/utxonursery_test.go new file mode 100644 index 00000000..18df8826 --- /dev/null +++ b/utxonursery_test.go @@ -0,0 +1,244 @@ +package main + +import ( + "bytes" + "fmt" + "reflect" + "testing" + + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/roasbeef/btcd/btcec" + "github.com/roasbeef/btcd/txscript" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" +) + +var ( + outPoints = []wire.OutPoint{ + wire.OutPoint{ + Hash: [wire.HashSize]byte{ + 0x51, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda, + 0x48, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17, + 0x2d, 0xe7, 0x93, 0xe4, 0xb7, 0x25, 0xb8, 0x4d, + 0x1f, 0xb, 0x4c, 0xf9, 0x9e, 0xc5, 0x8c, 0xe9, + }, + Index: 9, + }, + wire.OutPoint{ + Hash: [wire.HashSize]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, + }, + Index: 49, + }, + wire.OutPoint{ + Hash: [wire.HashSize]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, + }, + Index: 23, + }, + } + + keys = [][]byte{ + []byte{0x04, 0x11, 0xdb, 0x93, 0xe1, 0xdc, 0xdb, 0x8a, + 0x01, 0x6b, 0x49, 0x84, 0x0f, 0x8c, 0x53, 0xbc, 0x1e, + 0xb6, 0x8a, 0x38, 0x2e, 0x97, 0xb1, 0x48, 0x2e, 0xca, + 0xd7, 0xb1, 0x48, 0xa6, 0x90, 0x9a, 0x5c, 0xb2, 0xe0, + 0xea, 0xdd, 0xfb, 0x84, 0xcc, 0xf9, 0x74, 0x44, 0x64, + 0xf8, 0x2e, 0x16, 0x0b, 0xfa, 0x9b, 0x8b, 0x64, 0xf9, + 0xd4, 0xc0, 0x3f, 0x99, 0x9b, 0x86, 0x43, 0xf6, 0x56, + 0xb4, 0x12, 0xa3, + }, + []byte{0x07, 0x11, 0xdb, 0x93, 0xe1, 0xdc, 0xdb, 0x8a, + 0x01, 0x6b, 0x49, 0x84, 0x0f, 0x8c, 0x53, 0xbc, 0x1e, + 0xb6, 0x8a, 0x38, 0x2e, 0x97, 0xb1, 0x48, 0x2e, 0xca, + 0xd7, 0xb1, 0x48, 0xa6, 0x90, 0x9a, 0x5c, 0xb2, 0xe0, + 0xea, 0xdd, 0xfb, 0x84, 0xcc, 0xf9, 0x74, 0x44, 0x64, + 0xf8, 0x2e, 0x16, 0x0b, 0xfa, 0x9b, 0x8b, 0x64, 0xf9, + 0xd4, 0xc0, 0x3f, 0x99, 0x9b, 0x86, 0x43, 0xf6, 0x56, + 0xb4, 0x12, 0xa3, + }, + []byte{0x02, 0xce, 0x0b, 0x14, 0xfb, 0x84, 0x2b, 0x1b, + 0xa5, 0x49, 0xfd, 0xd6, 0x75, 0xc9, 0x80, 0x75, 0xf1, + 0x2e, 0x9c, 0x51, 0x0f, 0x8e, 0xf5, 0x2b, 0xd0, 0x21, + 0xa9, 0xa1, 0xf4, 0x80, 0x9d, 0x3b, 0x4d, + }, + } + + signDescriptors = []lnwallet.SignDescriptor{ + lnwallet.SignDescriptor{ + PrivateTweak: []byte{ + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, + }, + WitnessScript: []byte{ + 0x00, 0x14, 0xee, 0x91, 0x41, 0x7e, 0x85, 0x6c, 0xde, + 0x10, 0xa2, 0x91, 0x1e, 0xdc, 0xbd, 0xbd, 0x69, 0xe2, + 0xef, 0xb5, 0x71, 0x48, + }, + Output: &wire.TxOut{ + Value: 5000000000, + PkScript: []byte{ + 0x41, // OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, + 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, + 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, + 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, + 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, + 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, + 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, + 0xa6, // 65-byte signature + 0xac, // OP_CHECKSIG + }, + }, + HashType: txscript.SigHashAll, + }, + lnwallet.SignDescriptor{ + PrivateTweak: []byte{ + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, + }, + WitnessScript: []byte{ + 0x00, 0x14, 0xee, 0x91, 0x41, 0x7e, 0x85, 0x6c, 0xde, + 0x10, 0xa2, 0x91, 0x1e, 0xdc, 0xbd, 0xbd, 0x69, 0xe2, + 0xef, 0xb5, 0x71, 0x48, + }, + Output: &wire.TxOut{ + Value: 5000000000, + PkScript: []byte{ + 0x41, // OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, + 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, + 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, + 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, + 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, + 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, + 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, + 0xa6, // 65-byte signature + 0xac, // OP_CHECKSIG + }, + }, + HashType: txscript.SigHashAll, + }, + lnwallet.SignDescriptor{ + PrivateTweak: []byte{ + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, + }, + WitnessScript: []byte{ + 0x00, 0x14, 0xee, 0x91, 0x41, 0x7e, 0x85, 0x6c, 0xde, + 0x10, 0xa2, 0x91, 0x1e, 0xdc, 0xbd, 0xbd, 0x69, 0xe2, + 0xef, 0xb5, 0x71, 0x48, + }, + Output: &wire.TxOut{ + Value: 5000000000, + PkScript: []byte{ + 0x41, // OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, + 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, + 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, + 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, + 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, + 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, + 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, + 0xa6, // 65-byte signature + 0xac, // OP_CHECKSIG + }, + }, + HashType: txscript.SigHashAll, + }, + } + + kidOutputs = []kidOutput{ + kidOutput{ + amt: btcutil.Amount(13e7), + outPoint: outPoints[0], + blocksToMaturity: uint32(100), + confHeight: uint32(1770001), + }, + + kidOutput{ + amt: btcutil.Amount(24e7), + outPoint: outPoints[1], + blocksToMaturity: uint32(50), + confHeight: uint32(22342321), + }, + + kidOutput{ + amt: btcutil.Amount(2e5), + outPoint: outPoints[2], + blocksToMaturity: uint32(12), + confHeight: uint32(34241), + }, + } +) + +func TestAddSerializedKidsToList(t *testing.T) { + var b bytes.Buffer + + for i := 0; i < 3; i++ { + kid := &kidOutputs[i] + descriptor := &signDescriptors[i] + pk, err := btcec.ParsePubKey(keys[i], btcec.S256()) + if err != nil { + t.Fatalf("unable to parse pub key: %v", keys[i]) + } + descriptor.PubKey = pk + kid.signDescriptor = descriptor + + if err := serializeKidOutput(&b, &kidOutputs[i]); err != nil { + t.Fatalf("unable to serialize and add kid output to list: %v", err) + } + } + + kidList, err := deserializeKidList(&b) + if err != nil { + t.Fatalf("unable to deserialize kid output list: ", err) + } + + for i := 0; i < 3; i++ { + if !reflect.DeepEqual(&kidOutputs[i], kidList[i]) { + t.Fatalf("kidOutputs don't match \n%+v\n%+v", &kidOutputs[i], kidList[i]) + } + } +} + +func TestSerializeKidOutput(t *testing.T) { + kid := &kidOutputs[0] + descriptor := &signDescriptors[0] + pk, err := btcec.ParsePubKey(keys[0], btcec.S256()) + if err != nil { + t.Fatalf("unable to parse pub key: %v", keys[0]) + } + descriptor.PubKey = pk + kid.signDescriptor = descriptor + + var b bytes.Buffer + + if err := serializeKidOutput(&b, kid); err != nil { + t.Fatalf("unable to serialize kid output: %v", err) + } + + deserializedKid, err := deserializeKidOutput(&b) + if err != nil { + fmt.Printf(err.Error()) + } + + if !reflect.DeepEqual(kid, deserializedKid) { + t.Fatalf("kidOutputs don't match %+v vs %+v", kid, deserializedKid) + } +}