mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
4dab405623
Sweep txes are currently generated in multiple locations. Moving the sweep tx generation into a shared package will allow us to reuse that logic.
1756 lines
60 KiB
Go
1756 lines
60 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"github.com/lightningnetwork/lnd/sweep"
|
|
"io"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/davecgh/go-spew/spew"
|
|
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
)
|
|
|
|
// SUMMARY OF OUTPUT STATES
|
|
//
|
|
// - CRIB
|
|
// - SerializedType: babyOutput
|
|
// - OriginalOutputType: HTLC
|
|
// - Awaiting: First-stage HTLC CLTV expiry
|
|
// - HeightIndexEntry: Absolute block height of CLTV expiry.
|
|
// - NextState: KNDR
|
|
// - PSCL
|
|
// - SerializedType: kidOutput
|
|
// - OriginalOutputType: Commitment
|
|
// - Awaiting: Confirmation of commitment txn
|
|
// - HeightIndexEntry: None.
|
|
// - NextState: KNDR
|
|
// - KNDR
|
|
// - SerializedType: kidOutput
|
|
// - OriginalOutputType: Commitment or HTLC
|
|
// - Awaiting: Commitment CSV expiry or second-stage HTLC CSV expiry.
|
|
// - HeightIndexEntry: Input confirmation height + relative CSV delay
|
|
// - NextState: GRAD
|
|
// - GRAD:
|
|
// - SerializedType: kidOutput
|
|
// - OriginalOutputType: Commitment or HTLC
|
|
// - Awaiting: All other outputs in channel to become GRAD.
|
|
// - NextState: Mark channel fully closed in channeldb and remove.
|
|
//
|
|
// DESCRIPTION OF OUTPUT STATES
|
|
//
|
|
// TODO(roasbeef): update comment with both new output types
|
|
//
|
|
// - CRIB (babyOutput) outputs are two-stage htlc outputs that are initially
|
|
// locked using a CLTV delay, followed by a CSV delay. The first stage of a
|
|
// crib output requires broadcasting a presigned htlc timeout txn generated
|
|
// by the wallet after an absolute expiry height. Since the timeout txns are
|
|
// predetermined, they cannot be batched after-the-fact, meaning that all
|
|
// CRIB outputs are broadcast and confirmed independently. After the first
|
|
// stage is complete, a CRIB output is moved to the KNDR state, which will
|
|
// finishing sweeping the second-layer CSV delay.
|
|
//
|
|
// - PSCL (kidOutput) outputs are commitment outputs locked under a CSV delay.
|
|
// These outputs are stored temporarily in this state until the commitment
|
|
// transaction confirms, as this solidifies an absolute height that the
|
|
// relative time lock will expire. Once this maturity height is determined,
|
|
// the PSCL output is moved into KNDR.
|
|
//
|
|
// - KNDR (kidOutput) outputs are CSV delayed outputs for which the maturity
|
|
// height has been fully determined. This results from having received
|
|
// confirmation of the UTXO we are trying to spend, contained in either the
|
|
// commitment txn or htlc timeout txn. Once the maturity height is reached,
|
|
// the utxo nursery will sweep all KNDR outputs scheduled for that height
|
|
// using a single txn.
|
|
//
|
|
// NOTE: Due to the fact that KNDR outputs can be dynamically aggregated and
|
|
// swept, we make precautions to finalize the KNDR outputs at a particular
|
|
// height on our first attempt to sweep it. Finalizing involves signing the
|
|
// sweep transaction and persisting it in the nursery store, and recording
|
|
// the last finalized height. Any attempts to replay an already finalized
|
|
// height will result in broadcasting the already finalized txn, ensuring the
|
|
// nursery does not broadcast different txids for the same batch of KNDR
|
|
// outputs. The reason txids may change is due to the probabilistic nature of
|
|
// generating the pkscript in the sweep txn's output, even if the set of
|
|
// inputs remains static across attempts.
|
|
//
|
|
// - GRAD (kidOutput) outputs are KNDR outputs that have successfully been
|
|
// swept into the user's wallet. A channel is considered mature once all of
|
|
// its outputs, including two-stage htlcs, have entered the GRAD state,
|
|
// indicating that it safe to mark the channel as fully closed.
|
|
//
|
|
//
|
|
// OUTPUT STATE TRANSITIONS IN UTXO NURSERY
|
|
//
|
|
// ┌────────────────┐ ┌──────────────┐
|
|
// │ Commit Outputs │ │ HTLC Outputs │
|
|
// └────────────────┘ └──────────────┘
|
|
// │ │
|
|
// │ │
|
|
// │ │ UTXO NURSERY
|
|
// ┌───────────┼────────────────┬───────────┼───────────────────────────────┐
|
|
// │ │ │ │
|
|
// │ │ │ │ │
|
|
// │ │ │ CLTV-Delayed │
|
|
// │ │ │ V babyOutputs │
|
|
// │ │ ┌──────┐ │
|
|
// │ │ │ │ CRIB │ │
|
|
// │ │ └──────┘ │
|
|
// │ │ │ │ │
|
|
// │ │ │ │
|
|
// │ │ │ | │
|
|
// │ │ V Wait CLTV │
|
|
// │ │ │ [ ] + │
|
|
// │ │ | Publish Txn │
|
|
// │ │ │ │ │
|
|
// │ │ │ │
|
|
// │ │ │ V ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┐ │
|
|
// │ │ ( ) waitForTimeoutConf │
|
|
// │ │ │ | └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┘ │
|
|
// │ │ │ │
|
|
// │ │ │ │ │
|
|
// │ │ │ │
|
|
// │ V │ │ │
|
|
// │ ┌──────┐ │ │
|
|
// │ │ PSCL │ └ ── ── ─┼ ── ── ── ── ── ── ── ─┤
|
|
// │ └──────┘ │ │
|
|
// │ │ │ │
|
|
// │ │ │ │
|
|
// │ V ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┐ │ CSV-Delayed │
|
|
// │ ( ) waitForCommitConf │ kidOutputs │
|
|
// │ | └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┘ │ │
|
|
// │ │ │ │
|
|
// │ │ │ │
|
|
// │ │ V │
|
|
// │ │ ┌──────┐ │
|
|
// │ └─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│ KNDR │ │
|
|
// │ └──────┘ │
|
|
// │ │ │
|
|
// │ │ │
|
|
// │ | │
|
|
// │ V Wait CSV │
|
|
// │ [ ] + │
|
|
// │ | Publish Txn │
|
|
// │ │ │
|
|
// │ │ │
|
|
// │ V ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │
|
|
// │ ( ) waitForSweepConf │
|
|
// │ | └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
|
|
// │ │ │
|
|
// │ │ │
|
|
// │ V │
|
|
// │ ┌──────┐ │
|
|
// │ │ GRAD │ │
|
|
// │ └──────┘ │
|
|
// │ │ │
|
|
// │ │ │
|
|
// │ │ │
|
|
// └────────────────────────────────────────┼───────────────────────────────┘
|
|
// │
|
|
// │
|
|
// │
|
|
// │
|
|
// V
|
|
// ┌────────────────┐
|
|
// │ Wallet Outputs │
|
|
// └────────────────┘
|
|
|
|
var byteOrder = binary.BigEndian
|
|
|
|
var (
|
|
// ErrContractNotFound is returned when the nursery is unable to
|
|
// retrieve information about a queried contract.
|
|
ErrContractNotFound = fmt.Errorf("unable to locate contract")
|
|
)
|
|
|
|
// NurseryConfig abstracts the required subsystems used by the utxo nursery. An
|
|
// instance of NurseryConfig is passed to newUtxoNursery during instantiation.
|
|
type NurseryConfig struct {
|
|
// ChainIO is used by the utxo nursery to determine the current block
|
|
// height, which drives the incubation of the nursery's outputs.
|
|
ChainIO lnwallet.BlockChainIO
|
|
|
|
// ConfDepth is the number of blocks the nursery store waits before
|
|
// determining outputs in the chain as confirmed.
|
|
ConfDepth uint32
|
|
|
|
// FetchClosedChannels provides access to a user's channels, such that
|
|
// they can be marked fully closed after incubation has concluded.
|
|
FetchClosedChannels func(pendingOnly bool) (
|
|
[]*channeldb.ChannelCloseSummary, error)
|
|
|
|
// FetchClosedChannel provides access to the close summary to extract a
|
|
// height hint from.
|
|
FetchClosedChannel func(chanID *wire.OutPoint) (
|
|
*channeldb.ChannelCloseSummary, error)
|
|
|
|
// Notifier provides the utxo nursery the ability to subscribe to
|
|
// transaction confirmation events, which advance outputs through their
|
|
// persistence state transitions.
|
|
Notifier chainntnfs.ChainNotifier
|
|
|
|
// PublishTransaction facilitates the process of broadcasting a signed
|
|
// transaction to the appropriate network.
|
|
PublishTransaction func(*wire.MsgTx) error
|
|
|
|
// Store provides access to and modification of the persistent state
|
|
// maintained about the utxo nursery's incubating outputs.
|
|
Store NurseryStore
|
|
|
|
// Sweeper provides functionality to generate sweep transactions.
|
|
// Nursery uses this to sweep final outputs back into the wallet.
|
|
Sweeper *sweep.UtxoSweeper
|
|
}
|
|
|
|
// 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
|
|
// maturity, then sweep the outputs into the source wallet. An output is
|
|
// considered mature after the relative time-lock within the pkScript has
|
|
// passed. As outputs reach their maturity age, they're swept in batches into
|
|
// the source wallet, returning the outputs so they can be used within future
|
|
// channels, or regular Bitcoin transactions.
|
|
type utxoNursery struct {
|
|
started uint32 // To be used atomically.
|
|
stopped uint32 // To be used atomically.
|
|
|
|
cfg *NurseryConfig
|
|
|
|
mu sync.Mutex
|
|
bestHeight uint32
|
|
|
|
quit chan struct{}
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// newUtxoNursery creates a new instance of the utxoNursery from a
|
|
// ChainNotifier and LightningWallet instance.
|
|
func newUtxoNursery(cfg *NurseryConfig) *utxoNursery {
|
|
return &utxoNursery{
|
|
cfg: cfg,
|
|
quit: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start launches all goroutines the utxoNursery needs to properly carry out
|
|
// its duties.
|
|
func (u *utxoNursery) Start() error {
|
|
if !atomic.CompareAndSwapUint32(&u.started, 0, 1) {
|
|
return nil
|
|
}
|
|
|
|
utxnLog.Tracef("Starting UTXO nursery")
|
|
|
|
// 1. Start watching for new blocks, as this will drive the nursery
|
|
// store's state machine.
|
|
|
|
// Register with the notifier to receive notifications for each newly
|
|
// connected block. We register immediately on 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.cfg.Notifier.RegisterBlockEpochNtfn(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 2. Flush all fully-graduated channels from the pipeline.
|
|
|
|
// Load any pending close channels, which represents the super set of
|
|
// all channels that may still be incubating.
|
|
pendingCloseChans, err := u.cfg.FetchClosedChannels(true)
|
|
if err != nil {
|
|
newBlockChan.Cancel()
|
|
return err
|
|
}
|
|
|
|
// Ensure that all mature channels have been marked as fully closed in
|
|
// the channeldb.
|
|
for _, pendingClose := range pendingCloseChans {
|
|
err := u.closeAndRemoveIfMature(&pendingClose.ChanPoint)
|
|
if err != nil {
|
|
newBlockChan.Cancel()
|
|
return err
|
|
}
|
|
}
|
|
|
|
// TODO(conner): check if any fully closed channels can be removed from
|
|
// utxn.
|
|
|
|
// Query the nursery store for the lowest block height we could be
|
|
// incubating, which is taken to be the last height for which the
|
|
// database was purged.
|
|
lastGraduatedHeight, err := u.cfg.Store.LastGraduatedHeight()
|
|
if err != nil {
|
|
newBlockChan.Cancel()
|
|
return err
|
|
}
|
|
|
|
// 2. Restart spend ntfns for any preschool outputs, which are waiting
|
|
// for the force closed commitment txn to confirm, or any second-layer
|
|
// HTLC success transactions.
|
|
//
|
|
// NOTE: The next two steps *may* spawn go routines, thus from this
|
|
// point forward, we must close the nursery's quit channel if we detect
|
|
// any failures during startup to ensure they terminate.
|
|
if err := u.reloadPreschool(); err != nil {
|
|
newBlockChan.Cancel()
|
|
close(u.quit)
|
|
return err
|
|
}
|
|
|
|
// 3. Replay all crib and kindergarten outputs from last pruned to
|
|
// current best height.
|
|
if err := u.reloadClasses(lastGraduatedHeight); err != nil {
|
|
newBlockChan.Cancel()
|
|
close(u.quit)
|
|
return err
|
|
}
|
|
|
|
u.wg.Add(1)
|
|
go u.incubator(newBlockChan)
|
|
|
|
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) {
|
|
return nil
|
|
}
|
|
|
|
utxnLog.Infof("UTXO nursery shutting down")
|
|
|
|
close(u.quit)
|
|
u.wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
// IncubateOutputs sends a request to the utxoNursery to incubate a set of
|
|
// outputs from an existing commitment transaction. Outputs need to incubate if
|
|
// they're CLTV absolute time locked, or if they're CSV relative time locked.
|
|
// Once all outputs reach maturity, they'll be swept back into the wallet.
|
|
func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint,
|
|
commitResolution *lnwallet.CommitOutputResolution,
|
|
outgoingHtlcs []lnwallet.OutgoingHtlcResolution,
|
|
incomingHtlcs []lnwallet.IncomingHtlcResolution,
|
|
broadcastHeight uint32) error {
|
|
|
|
// Add to wait group because nursery might shut down during execution of
|
|
// this function. Otherwise it could happen that nursery thinks it is
|
|
// shut down, but in this function new goroutines were started and stay
|
|
// around.
|
|
u.wg.Add(1)
|
|
defer u.wg.Done()
|
|
|
|
// Check quit channel for the case where the waitgroup wait was finished
|
|
// right before this function's add call was made.
|
|
select {
|
|
case <-u.quit:
|
|
return fmt.Errorf("nursery shutting down")
|
|
default:
|
|
}
|
|
|
|
numHtlcs := len(incomingHtlcs) + len(outgoingHtlcs)
|
|
var (
|
|
hasCommit bool
|
|
|
|
// Kid outputs can be swept after an initial confirmation
|
|
// followed by a maturity period.Baby outputs are two stage and
|
|
// will need to wait for an absolute time out to reach a
|
|
// confirmation, then require a relative confirmation delay.
|
|
kidOutputs = make([]kidOutput, 0, 1+len(incomingHtlcs))
|
|
babyOutputs = make([]babyOutput, 0, len(outgoingHtlcs))
|
|
)
|
|
|
|
// 1. Build all the spendable outputs that we will try to incubate.
|
|
|
|
// It could be that our to-self output was below the dust limit. In
|
|
// that case the commit resolution would be nil and we would not have
|
|
// that output to incubate.
|
|
if commitResolution != nil {
|
|
hasCommit = true
|
|
selfOutput := makeKidOutput(
|
|
&commitResolution.SelfOutPoint,
|
|
&chanPoint,
|
|
commitResolution.MaturityDelay,
|
|
lnwallet.CommitmentTimeLock,
|
|
&commitResolution.SelfOutputSignDesc,
|
|
0,
|
|
)
|
|
|
|
// We'll skip any zero valued outputs as this indicates we
|
|
// don't have a settled balance within the commitment
|
|
// transaction.
|
|
if selfOutput.Amount() > 0 {
|
|
kidOutputs = append(kidOutputs, selfOutput)
|
|
}
|
|
}
|
|
|
|
// TODO(roasbeef): query and see if we already have, if so don't add?
|
|
|
|
// For each incoming HTLC, we'll register a kid output marked as a
|
|
// second-layer HTLC output. We effectively skip the baby stage (as the
|
|
// timelock is zero), and enter the kid stage.
|
|
for _, htlcRes := range incomingHtlcs {
|
|
htlcOutput := makeKidOutput(
|
|
&htlcRes.ClaimOutpoint, &chanPoint, htlcRes.CsvDelay,
|
|
lnwallet.HtlcAcceptedSuccessSecondLevel,
|
|
&htlcRes.SweepSignDesc, 0,
|
|
)
|
|
|
|
if htlcOutput.Amount() > 0 {
|
|
kidOutputs = append(kidOutputs, htlcOutput)
|
|
}
|
|
}
|
|
|
|
// For each outgoing HTLC, we'll create a baby output. If this is our
|
|
// commitment transaction, then we'll broadcast a second-layer
|
|
// transaction to transition to a kid output. Otherwise, we'll directly
|
|
// spend once the CLTV delay us up.
|
|
for _, htlcRes := range outgoingHtlcs {
|
|
// If this HTLC is on our commitment transaction, then it'll be
|
|
// a baby output as we need to go to the second level to sweep
|
|
// it.
|
|
if htlcRes.SignedTimeoutTx != nil {
|
|
htlcOutput := makeBabyOutput(&chanPoint, &htlcRes)
|
|
|
|
if htlcOutput.Amount() > 0 {
|
|
babyOutputs = append(babyOutputs, htlcOutput)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Otherwise, this is actually a kid output as we can sweep it
|
|
// once the commitment transaction confirms, and the absolute
|
|
// CLTV lock has expired. We set the CSV delay to zero to
|
|
// indicate this is actually a CLTV output.
|
|
htlcOutput := makeKidOutput(
|
|
&htlcRes.ClaimOutpoint, &chanPoint, 0,
|
|
lnwallet.HtlcOfferedRemoteTimeout,
|
|
&htlcRes.SweepSignDesc, htlcRes.Expiry,
|
|
)
|
|
kidOutputs = append(kidOutputs, htlcOutput)
|
|
}
|
|
|
|
// TODO(roasbeef): if want to handle outgoing on remote commit
|
|
// * need ability to cancel in the case that we learn of pre-image or
|
|
// remote party pulls
|
|
|
|
utxnLog.Infof("Incubating Channel(%s) has-commit=%v, num-htlcs=%d",
|
|
chanPoint, hasCommit, numHtlcs)
|
|
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
|
|
// 2. Persist the outputs we intended to sweep in the nursery store
|
|
if err := u.cfg.Store.Incubate(kidOutputs, babyOutputs); err != nil {
|
|
utxnLog.Errorf("unable to begin incubation of Channel(%s): %v",
|
|
chanPoint, err)
|
|
return err
|
|
}
|
|
|
|
// As an intermediate step, we'll now check to see if any of the baby
|
|
// outputs has actually _already_ expired. This may be the case if
|
|
// blocks were mined while we processed this message.
|
|
_, bestHeight, err := u.cfg.ChainIO.GetBestBlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We'll examine all the baby outputs just inserted into the database,
|
|
// if the output has already expired, then we'll *immediately* sweep
|
|
// it. This may happen if the caller raced a block to call this method.
|
|
for _, babyOutput := range babyOutputs {
|
|
if uint32(bestHeight) >= babyOutput.expiry {
|
|
err = u.sweepCribOutput(uint32(bestHeight), &babyOutput)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. If we are incubating any preschool outputs, register for a
|
|
// confirmation notification that will transition it to the
|
|
// kindergarten bucket.
|
|
if len(kidOutputs) != 0 {
|
|
for _, kidOutput := range kidOutputs {
|
|
err := u.registerPreschoolConf(
|
|
&kidOutput, broadcastHeight,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NurseryReport attempts to return a nursery report stored for the target
|
|
// outpoint. A nursery report details the maturity/sweeping progress for a
|
|
// contract that was previously force closed. If a report entry for the target
|
|
// chanPoint is unable to be constructed, then an error will be returned.
|
|
func (u *utxoNursery) NurseryReport(
|
|
chanPoint *wire.OutPoint) (*contractMaturityReport, error) {
|
|
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
|
|
utxnLog.Infof("NurseryReport: building nursery report for channel %v",
|
|
chanPoint)
|
|
|
|
report := &contractMaturityReport{
|
|
chanPoint: *chanPoint,
|
|
}
|
|
|
|
if err := u.cfg.Store.ForChanOutputs(chanPoint, func(k, v []byte) error {
|
|
switch {
|
|
case bytes.HasPrefix(k, cribPrefix):
|
|
// Cribs outputs are the only kind currently stored as
|
|
// baby outputs.
|
|
var baby babyOutput
|
|
err := baby.Decode(bytes.NewReader(v))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Each crib output represents a stage one htlc, and
|
|
// will contribute towards the limbo balance.
|
|
report.AddLimboStage1TimeoutHtlc(&baby)
|
|
|
|
case bytes.HasPrefix(k, psclPrefix),
|
|
bytes.HasPrefix(k, kndrPrefix),
|
|
bytes.HasPrefix(k, gradPrefix):
|
|
|
|
// All others states can be deserialized as kid outputs.
|
|
var kid kidOutput
|
|
err := kid.Decode(bytes.NewReader(v))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Now, use the state prefixes to determine how the
|
|
// this output should be represented in the nursery
|
|
// report. An output's funds are always in limbo until
|
|
// reaching the graduate state.
|
|
switch {
|
|
case bytes.HasPrefix(k, psclPrefix):
|
|
// Preschool outputs are awaiting the
|
|
// confirmation of the commitment transaction.
|
|
switch kid.WitnessType() {
|
|
case lnwallet.CommitmentTimeLock:
|
|
report.AddLimboCommitment(&kid)
|
|
|
|
// An HTLC output on our commitment transaction
|
|
// where the second-layer transaction hasn't
|
|
// yet confirmed.
|
|
case lnwallet.HtlcAcceptedSuccessSecondLevel:
|
|
report.AddLimboStage1SuccessHtlc(&kid)
|
|
}
|
|
|
|
case bytes.HasPrefix(k, kndrPrefix):
|
|
// Kindergarten outputs may originate from
|
|
// either the commitment transaction or an htlc.
|
|
// We can distinguish them via their witness
|
|
// types.
|
|
switch kid.WitnessType() {
|
|
case lnwallet.CommitmentTimeLock:
|
|
// The commitment transaction has been
|
|
// confirmed, and we are waiting the CSV
|
|
// delay to expire.
|
|
report.AddLimboCommitment(&kid)
|
|
|
|
case lnwallet.HtlcOfferedRemoteTimeout:
|
|
// This is an HTLC output on the
|
|
// commitment transaction of the remote
|
|
// party. The CLTV timelock has
|
|
// expired, and we only need to sweep
|
|
// it.
|
|
report.AddLimboDirectHtlc(&kid)
|
|
|
|
case lnwallet.HtlcAcceptedSuccessSecondLevel:
|
|
fallthrough
|
|
case lnwallet.HtlcOfferedTimeoutSecondLevel:
|
|
// The htlc timeout or success
|
|
// transaction has confirmed, and the
|
|
// CSV delay has begun ticking.
|
|
report.AddLimboStage2Htlc(&kid)
|
|
}
|
|
|
|
case bytes.HasPrefix(k, gradPrefix):
|
|
// Graduate outputs are those whose funds have
|
|
// been swept back into the wallet. Each output
|
|
// will contribute towards the recovered
|
|
// balance.
|
|
switch kid.WitnessType() {
|
|
case lnwallet.CommitmentTimeLock:
|
|
// The commitment output was
|
|
// successfully swept back into a
|
|
// regular p2wkh output.
|
|
report.AddRecoveredCommitment(&kid)
|
|
|
|
case lnwallet.HtlcAcceptedSuccessSecondLevel:
|
|
fallthrough
|
|
case lnwallet.HtlcOfferedTimeoutSecondLevel:
|
|
fallthrough
|
|
case lnwallet.HtlcOfferedRemoteTimeout:
|
|
// This htlc output successfully
|
|
// resides in a p2wkh output belonging
|
|
// to the user.
|
|
report.AddRecoveredHtlc(&kid)
|
|
}
|
|
}
|
|
|
|
default:
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return report, nil
|
|
}
|
|
|
|
// 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 {
|
|
psclOutputs, err := u.cfg.Store.FetchPreschools()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// For each of the preschool outputs stored in the nursery store, load
|
|
// its close summary from disk so that we can get an accurate height
|
|
// hint from which to start our range for spend notifications.
|
|
for i := range psclOutputs {
|
|
kid := &psclOutputs[i]
|
|
chanPoint := kid.OriginChanPoint()
|
|
|
|
// Load the close summary for this output's channel point.
|
|
closeSummary, err := u.cfg.FetchClosedChannel(chanPoint)
|
|
if err == channeldb.ErrClosedChannelNotFound {
|
|
// This should never happen since the close summary
|
|
// should only be removed after the channel has been
|
|
// swept completely.
|
|
utxnLog.Warnf("Close summary not found for "+
|
|
"chan_point=%v, can't determine height hint"+
|
|
"to sweep commit txn", chanPoint)
|
|
continue
|
|
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Use the close height from the channel summary as our height
|
|
// hint to drive our spend notifications, with our confirmation
|
|
// depth as a buffer for reorgs.
|
|
heightHint := closeSummary.CloseHeight - u.cfg.ConfDepth
|
|
err = u.registerPreschoolConf(kid, heightHint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// reloadClasses reinitializes any height-dependent state transitions for which
|
|
// the utxonursery has not received confirmation, and replays the graduation of
|
|
// all kindergarten and crib outputs for heights that have not been finalized.
|
|
// This allows the nursery to reinitialize all state to continue sweeping
|
|
// outputs, even in the event that we missed blocks while offline.
|
|
// reloadClasses is called during the startup of the UTXO Nursery.
|
|
func (u *utxoNursery) reloadClasses(lastGradHeight uint32) error {
|
|
// Begin by loading all of the still-active heights up to and including
|
|
// the last height we successfully graduated.
|
|
activeHeights, err := u.cfg.Store.HeightsBelowOrEqual(lastGradHeight)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(activeHeights) > 0 {
|
|
utxnLog.Infof("Re-registering confirmations for %d already "+
|
|
"graduated heights below height=%d", len(activeHeights),
|
|
lastGradHeight)
|
|
}
|
|
|
|
// Attempt to re-register notifications for any outputs still at these
|
|
// heights.
|
|
for _, classHeight := range activeHeights {
|
|
utxnLog.Debugf("Attempting to regraduate outputs at height=%v",
|
|
classHeight)
|
|
|
|
if err = u.regraduateClass(classHeight); err != nil {
|
|
utxnLog.Errorf("Failed to regraduate outputs at "+
|
|
"height=%v: %v", classHeight, err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Get the most recently mined block.
|
|
_, bestHeight, err := u.cfg.ChainIO.GetBestBlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If we haven't yet seen any registered force closes, or we're already
|
|
// caught up with the current best chain, then we can exit early.
|
|
if lastGradHeight == 0 || uint32(bestHeight) == lastGradHeight {
|
|
return nil
|
|
}
|
|
|
|
utxnLog.Infof("Processing outputs from missed blocks. Starting with "+
|
|
"blockHeight=%v, to current blockHeight=%v", lastGradHeight,
|
|
bestHeight)
|
|
|
|
// Loop through and check for graduating outputs at each of the missed
|
|
// block heights.
|
|
for curHeight := lastGradHeight + 1; curHeight <= uint32(bestHeight); curHeight++ {
|
|
utxnLog.Debugf("Attempting to graduate outputs at height=%v",
|
|
curHeight)
|
|
|
|
if err := u.graduateClass(curHeight); err != nil {
|
|
utxnLog.Errorf("Failed to graduate outputs at "+
|
|
"height=%v: %v", curHeight, err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
utxnLog.Infof("UTXO Nursery is now fully synced")
|
|
|
|
return nil
|
|
}
|
|
|
|
// regraduateClass handles the steps involved in re-registering for
|
|
// confirmations for all still-active outputs at a particular height. This is
|
|
// used during restarts to ensure that any still-pending state transitions are
|
|
// properly registered, so they can be driven by the chain notifier. No
|
|
// transactions or signing are done as a result of this step.
|
|
func (u *utxoNursery) regraduateClass(classHeight uint32) error {
|
|
// Fetch all information about the crib and kindergarten outputs at
|
|
// this height. In addition to the outputs, we also retrieve the
|
|
// finalized kindergarten sweep txn, which will be nil if we have not
|
|
// attempted this height before, or if no kindergarten outputs exist at
|
|
// this height.
|
|
finalTx, kgtnOutputs, cribOutputs, err := u.cfg.Store.FetchClass(
|
|
classHeight)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if finalTx != nil {
|
|
utxnLog.Infof("Re-registering confirmation for kindergarten "+
|
|
"sweep transaction at height=%d ", classHeight)
|
|
|
|
err = u.sweepMatureOutputs(classHeight, finalTx, kgtnOutputs)
|
|
if err != nil {
|
|
utxnLog.Errorf("Failed to re-register for kindergarten "+
|
|
"sweep transaction at height=%d: %v",
|
|
classHeight, err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
if len(cribOutputs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
utxnLog.Infof("Re-registering confirmation for first-stage HTLC "+
|
|
"outputs at height=%d ", classHeight)
|
|
|
|
// Now, we broadcast all pre-signed htlc txns from the crib outputs at
|
|
// this height. There is no need to finalize these txns, since the txid
|
|
// is predetermined when signed in the wallet.
|
|
for i := range cribOutputs {
|
|
err := u.sweepCribOutput(classHeight, &cribOutputs[i])
|
|
if err != nil {
|
|
utxnLog.Errorf("Failed to re-register first-stage "+
|
|
"HTLC output %v", cribOutputs[i].OutPoint())
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// incubator is tasked with driving all state transitions that are dependent on
|
|
// the current height of the blockchain. As new blocks arrive, the incubator
|
|
// will attempt spend outputs at the latest height. The asynchronous
|
|
// confirmation of these spends will either 1) move a crib output into the
|
|
// kindergarten bucket or 2) move a kindergarten output into the graduated
|
|
// bucket.
|
|
func (u *utxoNursery) incubator(newBlockChan *chainntnfs.BlockEpochEvent) {
|
|
defer u.wg.Done()
|
|
defer newBlockChan.Cancel()
|
|
|
|
for {
|
|
select {
|
|
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
|
|
// ensure the daemon shuts down gracefully, yet
|
|
// swiftly.
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// TODO(roasbeef): if the BlockChainIO is rescanning
|
|
// will give stale data
|
|
|
|
// A new block has just been connected to the main
|
|
// chain, which means we might be able to graduate crib
|
|
// or kindergarten outputs at this height. This involves
|
|
// broadcasting any presigned htlc timeout txns, as well
|
|
// as signing and broadcasting a sweep txn that spends
|
|
// from all kindergarten outputs at this height.
|
|
height := uint32(epoch.Height)
|
|
if err := u.graduateClass(height); err != nil {
|
|
utxnLog.Errorf("error while graduating "+
|
|
"class at height=%d: %v", height, err)
|
|
|
|
// TODO(conner): signal fatal error to daemon
|
|
}
|
|
|
|
case <-u.quit:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// graduateClass handles the steps involved in spending outputs whose CSV or
|
|
// CLTV delay expires at the nursery's current height. This method is called
|
|
// each time a new block arrives, or during startup to catch up on heights we
|
|
// may have missed while the nursery was offline.
|
|
func (u *utxoNursery) graduateClass(classHeight uint32) error {
|
|
// Record this height as the nursery's current best height.
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
|
|
u.bestHeight = classHeight
|
|
|
|
// Fetch all information about the crib and kindergarten outputs at
|
|
// this height. In addition to the outputs, we also retrieve the
|
|
// finalized kindergarten sweep txn, which will be nil if we have not
|
|
// attempted this height before, or if no kindergarten outputs exist at
|
|
// this height.
|
|
finalTx, kgtnOutputs, cribOutputs, err := u.cfg.Store.FetchClass(
|
|
classHeight)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
utxnLog.Infof("Attempting to graduate height=%v: num_kids=%v, "+
|
|
"num_babies=%v", classHeight, len(kgtnOutputs), len(cribOutputs))
|
|
|
|
// Load the last finalized height, so we can determine if the
|
|
// kindergarten sweep txn should be crafted.
|
|
lastFinalizedHeight, err := u.cfg.Store.LastFinalizedHeight()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If we haven't processed this height before, we finalize the
|
|
// graduating kindergarten outputs, by signing a sweep transaction that
|
|
// spends from them. This txn is persisted such that we never broadcast
|
|
// a different txn for the same height. This allows us to recover from
|
|
// failures, and watch for the correct txid.
|
|
if classHeight > lastFinalizedHeight {
|
|
// If this height has never been finalized, we have never
|
|
// generated a sweep txn for this height. Generate one if there
|
|
// are kindergarten outputs or cltv crib outputs to be spent.
|
|
if len(kgtnOutputs) > 0 {
|
|
csvSpendableOutputs := make([]sweep.CsvSpendableOutput,
|
|
len(kgtnOutputs))
|
|
for i := range kgtnOutputs {
|
|
csvSpendableOutputs[i] = &kgtnOutputs[i]
|
|
}
|
|
|
|
finalTx, err = u.cfg.Sweeper.CreateSweepTx(
|
|
csvSpendableOutputs, classHeight)
|
|
|
|
if err != nil {
|
|
utxnLog.Errorf("Failed to create sweep txn at "+
|
|
"height=%d", classHeight)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Persist the kindergarten sweep txn to the nursery store. It
|
|
// is safe to store a nil finalTx, which happens if there are
|
|
// no graduating kindergarten outputs.
|
|
err = u.cfg.Store.FinalizeKinder(classHeight, finalTx)
|
|
if err != nil {
|
|
utxnLog.Errorf("Failed to finalize kindergarten at "+
|
|
"height=%d", classHeight)
|
|
|
|
return err
|
|
}
|
|
|
|
// Log if the finalized transaction is non-trivial.
|
|
if finalTx != nil {
|
|
utxnLog.Infof("Finalized kindergarten at height=%d ",
|
|
classHeight)
|
|
}
|
|
}
|
|
|
|
// Now that the kindergarten sweep txn has either been finalized or
|
|
// restored, broadcast the txn, and set up notifications that will
|
|
// transition the swept kindergarten outputs and cltvCrib into
|
|
// graduated outputs.
|
|
if finalTx != nil {
|
|
err := u.sweepMatureOutputs(classHeight, finalTx, kgtnOutputs)
|
|
if err != nil {
|
|
utxnLog.Errorf("Failed to sweep %d kindergarten "+
|
|
"outputs at height=%d: %v",
|
|
len(kgtnOutputs), classHeight, err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Now, we broadcast all pre-signed htlc txns from the csv crib outputs
|
|
// at this height. There is no need to finalize these txns, since the
|
|
// txid is predetermined when signed in the wallet.
|
|
for i := range cribOutputs {
|
|
err := u.sweepCribOutput(classHeight, &cribOutputs[i])
|
|
if err != nil {
|
|
utxnLog.Errorf("Failed to sweep first-stage HTLC "+
|
|
"(CLTV-delayed) output %v",
|
|
cribOutputs[i].OutPoint())
|
|
return err
|
|
}
|
|
}
|
|
|
|
return u.cfg.Store.GraduateHeight(classHeight)
|
|
}
|
|
|
|
// sweepMatureOutputs generates and broadcasts the transaction that transfers
|
|
// control of funds from a prior channel commitment transaction to the user's
|
|
// wallet. The outputs swept were previously time locked (either absolute or
|
|
// relative), but are not mature enough to sweep into the wallet.
|
|
func (u *utxoNursery) sweepMatureOutputs(classHeight uint32, finalTx *wire.MsgTx,
|
|
kgtnOutputs []kidOutput) error {
|
|
|
|
utxnLog.Infof("Sweeping %v CSV-delayed outputs with sweep tx "+
|
|
"(txid=%v): %v", len(kgtnOutputs),
|
|
finalTx.TxHash(), newLogClosure(func() string {
|
|
return spew.Sdump(finalTx)
|
|
}),
|
|
)
|
|
|
|
// With the sweep transaction fully signed, broadcast the transaction
|
|
// to the network. Additionally, we can stop tracking these outputs as
|
|
// they've just been swept.
|
|
err := u.cfg.PublishTransaction(finalTx)
|
|
if err != nil && err != lnwallet.ErrDoubleSpend {
|
|
utxnLog.Errorf("unable to broadcast sweep tx: %v, %v",
|
|
err, spew.Sdump(finalTx))
|
|
return err
|
|
}
|
|
|
|
return u.registerSweepConf(finalTx, kgtnOutputs, classHeight)
|
|
}
|
|
|
|
// registerSweepConf is responsible for registering a finalized kindergarten
|
|
// sweep transaction for confirmation notifications. If the confirmation was
|
|
// successfully registered, a goroutine will be spawned that waits for the
|
|
// confirmation, and graduates the provided kindergarten class within the
|
|
// nursery store.
|
|
func (u *utxoNursery) registerSweepConf(finalTx *wire.MsgTx,
|
|
kgtnOutputs []kidOutput, heightHint uint32) error {
|
|
|
|
finalTxID := finalTx.TxHash()
|
|
|
|
confChan, err := u.cfg.Notifier.RegisterConfirmationsNtfn(
|
|
&finalTxID, finalTx.TxOut[0].PkScript, u.cfg.ConfDepth,
|
|
heightHint,
|
|
)
|
|
if err != nil {
|
|
utxnLog.Errorf("unable to register notification for "+
|
|
"sweep confirmation: %v", finalTxID)
|
|
return err
|
|
}
|
|
|
|
utxnLog.Infof("Registering sweep tx %v for confs at height=%d",
|
|
finalTxID, heightHint)
|
|
|
|
u.wg.Add(1)
|
|
go u.waitForSweepConf(heightHint, kgtnOutputs, confChan)
|
|
|
|
return nil
|
|
}
|
|
|
|
// waitForSweepConf watches for the confirmation of a sweep transaction
|
|
// containing a batch of kindergarten outputs. Once confirmation has been
|
|
// received, the nursery will mark those outputs as fully graduated, and proceed
|
|
// to mark any mature channels as fully closed in channeldb.
|
|
// NOTE(conner): this method MUST be called as a go routine.
|
|
func (u *utxoNursery) waitForSweepConf(classHeight uint32,
|
|
kgtnOutputs []kidOutput, confChan *chainntnfs.ConfirmationEvent) {
|
|
|
|
defer u.wg.Done()
|
|
|
|
select {
|
|
case _, ok := <-confChan.Confirmed:
|
|
if !ok {
|
|
utxnLog.Errorf("Notification chan closed, can't"+
|
|
" advance %v graduating outputs",
|
|
len(kgtnOutputs))
|
|
return
|
|
}
|
|
|
|
case <-u.quit:
|
|
return
|
|
}
|
|
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
|
|
// TODO(conner): add retry logic?
|
|
|
|
// Mark the confirmed kindergarten outputs as graduated.
|
|
if err := u.cfg.Store.GraduateKinder(classHeight); err != nil {
|
|
utxnLog.Errorf("Unable to graduate %v kindergarten outputs: "+
|
|
"%v", len(kgtnOutputs), err)
|
|
return
|
|
}
|
|
|
|
utxnLog.Infof("Graduated %d kindergarten outputs from height=%d",
|
|
len(kgtnOutputs), classHeight)
|
|
|
|
// Iterate over the kid outputs and construct a set of all channel
|
|
// points to which they belong.
|
|
var possibleCloses = make(map[wire.OutPoint]struct{})
|
|
for _, kid := range kgtnOutputs {
|
|
possibleCloses[*kid.OriginChanPoint()] = struct{}{}
|
|
|
|
}
|
|
|
|
// Attempt to close each channel, only doing so if all of the channel's
|
|
// outputs have been graduated.
|
|
for chanPoint := range possibleCloses {
|
|
if err := u.closeAndRemoveIfMature(&chanPoint); err != nil {
|
|
utxnLog.Errorf("Failed to close and remove channel %v",
|
|
chanPoint)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// sweepCribOutput broadcasts the crib output's htlc timeout txn, and sets up a
|
|
// notification that will advance it to the kindergarten bucket upon
|
|
// confirmation.
|
|
func (u *utxoNursery) sweepCribOutput(classHeight uint32, baby *babyOutput) error {
|
|
utxnLog.Infof("Publishing CLTV-delayed HTLC output using timeout tx "+
|
|
"(txid=%v): %v", baby.timeoutTx.TxHash(),
|
|
newLogClosure(func() string {
|
|
return spew.Sdump(baby.timeoutTx)
|
|
}),
|
|
)
|
|
|
|
// We'll now broadcast the HTLC transaction, then wait for it to be
|
|
// confirmed before transitioning it to kindergarten.
|
|
err := u.cfg.PublishTransaction(baby.timeoutTx)
|
|
if err != nil && err != lnwallet.ErrDoubleSpend {
|
|
utxnLog.Errorf("Unable to broadcast baby tx: "+
|
|
"%v, %v", err, spew.Sdump(baby.timeoutTx))
|
|
return err
|
|
}
|
|
|
|
return u.registerTimeoutConf(baby, classHeight)
|
|
}
|
|
|
|
// registerTimeoutConf is responsible for subscribing to confirmation
|
|
// notification for an htlc timeout transaction. If successful, a goroutine
|
|
// will be spawned that will transition the provided baby output into the
|
|
// kindergarten state within the nursery store.
|
|
func (u *utxoNursery) registerTimeoutConf(baby *babyOutput, heightHint uint32) error {
|
|
|
|
birthTxID := baby.timeoutTx.TxHash()
|
|
|
|
// Register for the confirmation of presigned htlc txn.
|
|
confChan, err := u.cfg.Notifier.RegisterConfirmationsNtfn(
|
|
&birthTxID, baby.timeoutTx.TxOut[0].PkScript, u.cfg.ConfDepth,
|
|
heightHint,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
utxnLog.Infof("Htlc output %v registered for promotion "+
|
|
"notification.", baby.OutPoint())
|
|
|
|
u.wg.Add(1)
|
|
go u.waitForTimeoutConf(baby, confChan)
|
|
|
|
return nil
|
|
}
|
|
|
|
// waitForTimeoutConf watches for the confirmation of an htlc timeout
|
|
// transaction, and attempts to move the htlc output from the crib bucket to the
|
|
// kindergarten bucket upon success.
|
|
func (u *utxoNursery) waitForTimeoutConf(baby *babyOutput,
|
|
confChan *chainntnfs.ConfirmationEvent) {
|
|
|
|
defer u.wg.Done()
|
|
|
|
select {
|
|
case txConfirmation, ok := <-confChan.Confirmed:
|
|
if !ok {
|
|
utxnLog.Errorf("Notification chan "+
|
|
"closed, can't advance baby output %v",
|
|
baby.OutPoint())
|
|
return
|
|
}
|
|
|
|
baby.SetConfHeight(txConfirmation.BlockHeight)
|
|
|
|
case <-u.quit:
|
|
return
|
|
}
|
|
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
|
|
// TODO(conner): add retry logic?
|
|
|
|
err := u.cfg.Store.CribToKinder(baby)
|
|
if err != nil {
|
|
utxnLog.Errorf("Unable to move htlc output from "+
|
|
"crib to kindergarten bucket: %v", err)
|
|
return
|
|
}
|
|
|
|
utxnLog.Infof("Htlc output %v promoted to "+
|
|
"kindergarten", baby.OutPoint())
|
|
}
|
|
|
|
// registerPreschoolConf is responsible for subscribing to the confirmation of
|
|
// a commitment transaction, or an htlc success transaction for an incoming
|
|
// HTLC on our commitment transaction.. If successful, the provided preschool
|
|
// output will be moved persistently into the kindergarten state within the
|
|
// nursery store.
|
|
func (u *utxoNursery) registerPreschoolConf(kid *kidOutput, heightHint uint32) error {
|
|
txID := kid.OutPoint().Hash
|
|
|
|
// TODO(roasbeef): ensure we don't already have one waiting, need to
|
|
// de-duplicate
|
|
// * need to do above?
|
|
|
|
pkScript := kid.signDesc.Output.PkScript
|
|
confChan, err := u.cfg.Notifier.RegisterConfirmationsNtfn(
|
|
&txID, pkScript, u.cfg.ConfDepth, heightHint,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var outputType string
|
|
if kid.isHtlc {
|
|
outputType = "HTLC"
|
|
} else {
|
|
outputType = "Commitment"
|
|
}
|
|
|
|
utxnLog.Infof("%v outpoint %v registered for "+
|
|
"confirmation notification.", outputType, kid.OutPoint())
|
|
|
|
u.wg.Add(1)
|
|
go u.waitForPreschoolConf(kid, confChan)
|
|
|
|
return nil
|
|
}
|
|
|
|
// waitForPreschoolConf is intended to be run as a goroutine that will wait until
|
|
// a channel force close commitment transaction, or a second layer HTLC success
|
|
// transaction has been included in a confirmed block. Once the transaction has
|
|
// been confirmed (as reported by the Chain Notifier), waitForPreschoolConf
|
|
// 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 (u *utxoNursery) waitForPreschoolConf(kid *kidOutput,
|
|
confChan *chainntnfs.ConfirmationEvent) {
|
|
|
|
defer u.wg.Done()
|
|
|
|
select {
|
|
case txConfirmation, ok := <-confChan.Confirmed:
|
|
if !ok {
|
|
utxnLog.Errorf("Notification chan "+
|
|
"closed, can't advance output %v",
|
|
kid.OutPoint())
|
|
return
|
|
}
|
|
|
|
kid.SetConfHeight(txConfirmation.BlockHeight)
|
|
|
|
case <-u.quit:
|
|
return
|
|
}
|
|
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
|
|
// TODO(conner): add retry logic?
|
|
|
|
var outputType string
|
|
if kid.isHtlc {
|
|
outputType = "HTLC"
|
|
} else {
|
|
outputType = "Commitment"
|
|
}
|
|
|
|
err := u.cfg.Store.PreschoolToKinder(kid)
|
|
if err != nil {
|
|
utxnLog.Errorf("Unable to move %v output "+
|
|
"from preschool to kindergarten bucket: %v",
|
|
outputType, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// contractMaturityReport is a report that details the maturity progress of a
|
|
// particular force closed contract.
|
|
type contractMaturityReport struct {
|
|
// chanPoint is the channel point of the original contract that is now
|
|
// awaiting maturity within the utxoNursery.
|
|
chanPoint wire.OutPoint
|
|
|
|
// limboBalance is the total number of frozen coins within this
|
|
// contract.
|
|
limboBalance btcutil.Amount
|
|
|
|
// recoveredBalance is the total value that has been successfully swept
|
|
// back to the user's wallet.
|
|
recoveredBalance btcutil.Amount
|
|
|
|
// localAmount is the local value of the commitment output.
|
|
localAmount btcutil.Amount
|
|
|
|
// confHeight is the block height that this output originally confirmed.
|
|
confHeight uint32
|
|
|
|
// maturityRequirement is the input age required for this output to
|
|
// reach maturity.
|
|
maturityRequirement uint32
|
|
|
|
// maturityHeight is the absolute block height that this output will
|
|
// mature at.
|
|
maturityHeight uint32
|
|
|
|
// htlcs records a maturity report for each htlc output in this channel.
|
|
htlcs []htlcMaturityReport
|
|
}
|
|
|
|
// htlcMaturityReport provides a summary of a single htlc output, and is
|
|
// embedded as party of the overarching contractMaturityReport
|
|
type htlcMaturityReport struct {
|
|
// outpoint is the final output that will be swept back to the wallet.
|
|
outpoint wire.OutPoint
|
|
|
|
// amount is the final value that will be swept in back to the wallet.
|
|
amount btcutil.Amount
|
|
|
|
// confHeight is the block height that this output originally confirmed.
|
|
confHeight uint32
|
|
|
|
// maturityRequirement is the input age required for this output to
|
|
// reach maturity.
|
|
maturityRequirement uint32
|
|
|
|
// maturityHeight is the absolute block height that this output will
|
|
// mature at.
|
|
maturityHeight uint32
|
|
|
|
// stage indicates whether the htlc is in the CLTV-timeout stage (1) or
|
|
// the CSV-delay stage (2). A stage 1 htlc's maturity height will be set
|
|
// to its expiry height, while a stage 2 htlc's maturity height will be
|
|
// set to its confirmation height plus the maturity requirement.
|
|
stage uint32
|
|
}
|
|
|
|
// AddLimboCommitment adds an incubating commitment output to maturity
|
|
// report's htlcs, and contributes its amount to the limbo balance.
|
|
func (c *contractMaturityReport) AddLimboCommitment(kid *kidOutput) {
|
|
c.limboBalance += kid.Amount()
|
|
|
|
c.localAmount += kid.Amount()
|
|
c.confHeight = kid.ConfHeight()
|
|
c.maturityRequirement = kid.BlocksToMaturity()
|
|
|
|
// If the confirmation height is set, then this means the contract has
|
|
// been confirmed, and we know the final maturity height.
|
|
if kid.ConfHeight() != 0 {
|
|
c.maturityHeight = kid.BlocksToMaturity() + kid.ConfHeight()
|
|
}
|
|
}
|
|
|
|
// AddRecoveredCommitment adds a graduated commitment output to maturity
|
|
// report's htlcs, and contributes its amount to the recovered balance.
|
|
func (c *contractMaturityReport) AddRecoveredCommitment(kid *kidOutput) {
|
|
c.recoveredBalance += kid.Amount()
|
|
|
|
c.localAmount += kid.Amount()
|
|
c.confHeight = kid.ConfHeight()
|
|
c.maturityRequirement = kid.BlocksToMaturity()
|
|
c.maturityHeight = kid.BlocksToMaturity() + kid.ConfHeight()
|
|
}
|
|
|
|
// AddLimboStage1TimeoutHtlc adds an htlc crib output to the maturity report's
|
|
// htlcs, and contributes its amount to the limbo balance.
|
|
func (c *contractMaturityReport) AddLimboStage1TimeoutHtlc(baby *babyOutput) {
|
|
c.limboBalance += baby.Amount()
|
|
|
|
// TODO(roasbeef): bool to indicate stage 1 vs stage 2?
|
|
c.htlcs = append(c.htlcs, htlcMaturityReport{
|
|
outpoint: *baby.OutPoint(),
|
|
amount: baby.Amount(),
|
|
confHeight: baby.ConfHeight(),
|
|
maturityHeight: baby.expiry,
|
|
stage: 1,
|
|
})
|
|
}
|
|
|
|
// AddLimboDirectHtlc adds a direct HTLC on the commitment transaction of the
|
|
// remote party to the maturity report. This a CLTV time-locked output that
|
|
// hasn't yet expired.
|
|
func (c *contractMaturityReport) AddLimboDirectHtlc(kid *kidOutput) {
|
|
c.limboBalance += kid.Amount()
|
|
|
|
htlcReport := htlcMaturityReport{
|
|
outpoint: *kid.OutPoint(),
|
|
amount: kid.Amount(),
|
|
confHeight: kid.ConfHeight(),
|
|
maturityHeight: kid.absoluteMaturity,
|
|
stage: 2,
|
|
}
|
|
|
|
c.htlcs = append(c.htlcs, htlcReport)
|
|
}
|
|
|
|
// AddLimboStage1SuccessHtlcHtlc adds an htlc crib output to the maturity
|
|
// report's set of HTLC's. We'll use this to report any incoming HTLC sweeps
|
|
// where the second level transaction hasn't yet confirmed.
|
|
func (c *contractMaturityReport) AddLimboStage1SuccessHtlc(kid *kidOutput) {
|
|
c.limboBalance += kid.Amount()
|
|
|
|
c.htlcs = append(c.htlcs, htlcMaturityReport{
|
|
outpoint: *kid.OutPoint(),
|
|
amount: kid.Amount(),
|
|
confHeight: kid.ConfHeight(),
|
|
maturityRequirement: kid.BlocksToMaturity(),
|
|
stage: 1,
|
|
})
|
|
}
|
|
|
|
// AddLimboStage2Htlc adds an htlc kindergarten output to the maturity report's
|
|
// htlcs, and contributes its amount to the limbo balance.
|
|
func (c *contractMaturityReport) AddLimboStage2Htlc(kid *kidOutput) {
|
|
c.limboBalance += kid.Amount()
|
|
|
|
htlcReport := htlcMaturityReport{
|
|
outpoint: *kid.OutPoint(),
|
|
amount: kid.Amount(),
|
|
confHeight: kid.ConfHeight(),
|
|
maturityRequirement: kid.BlocksToMaturity(),
|
|
stage: 2,
|
|
}
|
|
|
|
// If the confirmation height is set, then this means the first stage
|
|
// has been confirmed, and we know the final maturity height of the CSV
|
|
// delay.
|
|
if kid.ConfHeight() != 0 {
|
|
htlcReport.maturityHeight = kid.ConfHeight() + kid.BlocksToMaturity()
|
|
}
|
|
|
|
c.htlcs = append(c.htlcs, htlcReport)
|
|
}
|
|
|
|
// AddRecoveredHtlc adds a graduate output to the maturity report's htlcs, and
|
|
// contributes its amount to the recovered balance.
|
|
func (c *contractMaturityReport) AddRecoveredHtlc(kid *kidOutput) {
|
|
c.recoveredBalance += kid.Amount()
|
|
|
|
c.htlcs = append(c.htlcs, htlcMaturityReport{
|
|
outpoint: *kid.OutPoint(),
|
|
amount: kid.Amount(),
|
|
confHeight: kid.ConfHeight(),
|
|
maturityRequirement: kid.BlocksToMaturity(),
|
|
maturityHeight: kid.ConfHeight() + kid.BlocksToMaturity(),
|
|
})
|
|
}
|
|
|
|
// closeAndRemoveIfMature removes a particular channel from the channel index
|
|
// if and only if all of its outputs have been marked graduated. If the channel
|
|
// still has ungraduated outputs, the method will succeed without altering the
|
|
// database state.
|
|
func (u *utxoNursery) closeAndRemoveIfMature(chanPoint *wire.OutPoint) error {
|
|
isMature, err := u.cfg.Store.IsMatureChannel(chanPoint)
|
|
if err == ErrContractNotFound {
|
|
return nil
|
|
} else if err != nil {
|
|
utxnLog.Errorf("Unable to determine maturity of "+
|
|
"channel=%s", chanPoint)
|
|
return err
|
|
}
|
|
|
|
// Nothing to do if we are still incubating.
|
|
if !isMature {
|
|
return nil
|
|
}
|
|
|
|
// Now that the channel is fully closed, we remove the channel from the
|
|
// nursery store here. This preserves the invariant that we never remove
|
|
// a channel unless it is mature, as this is the only place the utxo
|
|
// nursery removes a channel.
|
|
if err := u.cfg.Store.RemoveChannel(chanPoint); err != nil {
|
|
utxnLog.Errorf("Unable to remove channel=%s from "+
|
|
"nursery store: %v", chanPoint, err)
|
|
return err
|
|
}
|
|
|
|
utxnLog.Infof("Removed channel %v from nursery store", chanPoint)
|
|
|
|
return nil
|
|
}
|
|
|
|
// newSweepPkScript creates a new public key script which should be used to
|
|
// sweep any time-locked, or contested channel funds into the wallet.
|
|
// Specifically, the script generated is a version 0, pay-to-witness-pubkey-hash
|
|
// (p2wkh) output.
|
|
func newSweepPkScript(wallet lnwallet.WalletController) ([]byte, error) {
|
|
sweepAddr, err := wallet.NewAddress(lnwallet.WitnessPubKey, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return txscript.PayToAddrScript(sweepAddr)
|
|
}
|
|
|
|
// babyOutput represents a two-stage CSV locked output, and is used to track
|
|
// htlc outputs through incubation. The first stage requires broadcasting a
|
|
// presigned timeout txn that spends from the CLTV locked output on the
|
|
// commitment txn. A babyOutput is treated as a subset of CsvSpendableOutputs,
|
|
// with the additional constraint that a transaction must be broadcast before
|
|
// it can be spent. Each baby transaction embeds the kidOutput that can later
|
|
// be used to spend the CSV output contained in the timeout txn.
|
|
//
|
|
// TODO(roasbeef): re-rename to timeout tx
|
|
// * create CltvCsvSpendableOutput
|
|
type babyOutput struct {
|
|
// expiry is the absolute block height at which the secondLevelTx
|
|
// should be broadcast to the network.
|
|
//
|
|
// NOTE: This value will be zero if this is a baby output for a prior
|
|
// incoming HTLC.
|
|
expiry uint32
|
|
|
|
// timeoutTx is a fully-signed transaction that, upon confirmation,
|
|
// transitions the htlc into the delay+claim stage.
|
|
timeoutTx *wire.MsgTx
|
|
|
|
// kidOutput represents the CSV output to be swept from the
|
|
// secondLevelTx after it has been broadcast and confirmed.
|
|
kidOutput
|
|
}
|
|
|
|
// makeBabyOutput constructs a baby output that wraps a future kidOutput. The
|
|
// provided sign descriptors and witness types will be used once the output
|
|
// reaches the delay and claim stage.
|
|
func makeBabyOutput(chanPoint *wire.OutPoint,
|
|
htlcResolution *lnwallet.OutgoingHtlcResolution) babyOutput {
|
|
|
|
htlcOutpoint := htlcResolution.ClaimOutpoint
|
|
blocksToMaturity := htlcResolution.CsvDelay
|
|
witnessType := lnwallet.HtlcOfferedTimeoutSecondLevel
|
|
|
|
kid := makeKidOutput(
|
|
&htlcOutpoint, chanPoint, blocksToMaturity, witnessType,
|
|
&htlcResolution.SweepSignDesc, 0,
|
|
)
|
|
|
|
return babyOutput{
|
|
kidOutput: kid,
|
|
expiry: htlcResolution.Expiry,
|
|
timeoutTx: htlcResolution.SignedTimeoutTx,
|
|
}
|
|
}
|
|
|
|
// Encode writes the baby output to the given io.Writer.
|
|
func (bo *babyOutput) Encode(w io.Writer) error {
|
|
var scratch [4]byte
|
|
byteOrder.PutUint32(scratch[:], bo.expiry)
|
|
if _, err := w.Write(scratch[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := bo.timeoutTx.Serialize(w); err != nil {
|
|
return err
|
|
}
|
|
|
|
return bo.kidOutput.Encode(w)
|
|
}
|
|
|
|
// Decode reconstructs a baby output using the provided io.Reader.
|
|
func (bo *babyOutput) Decode(r io.Reader) error {
|
|
var scratch [4]byte
|
|
if _, err := r.Read(scratch[:]); err != nil {
|
|
return err
|
|
}
|
|
bo.expiry = byteOrder.Uint32(scratch[:])
|
|
|
|
bo.timeoutTx = new(wire.MsgTx)
|
|
if err := bo.timeoutTx.Deserialize(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
return bo.kidOutput.Decode(r)
|
|
}
|
|
|
|
// 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): rename to immatureOutput?
|
|
type kidOutput struct {
|
|
breachedOutput
|
|
|
|
originChanPoint wire.OutPoint
|
|
|
|
// isHtlc denotes if this kid output is an HTLC output or not. This
|
|
// value will be used to determine how to report this output within the
|
|
// nursery report.
|
|
isHtlc bool
|
|
|
|
// blocksToMaturity is the relative CSV delay required after initial
|
|
// confirmation of the commitment transaction before we can sweep this
|
|
// output.
|
|
//
|
|
// NOTE: This will be set for: commitment outputs, and incoming HTLC's.
|
|
// Otherwise, this will be zero.
|
|
blocksToMaturity uint32
|
|
|
|
// absoluteMaturity is the absolute height that this output will be
|
|
// mature at. In order to sweep the output after this height, the
|
|
// locktime of sweep transaction will need to be set to this value.
|
|
//
|
|
// NOTE: This will only be set for: outgoing HTLC's on the commitment
|
|
// transaction of the remote party.
|
|
absoluteMaturity uint32
|
|
|
|
confHeight uint32
|
|
}
|
|
|
|
func makeKidOutput(outpoint, originChanPoint *wire.OutPoint,
|
|
blocksToMaturity uint32, witnessType lnwallet.WitnessType,
|
|
signDescriptor *lnwallet.SignDescriptor,
|
|
absoluteMaturity uint32) kidOutput {
|
|
|
|
// This is an HTLC either if it's an incoming HTLC on our commitment
|
|
// transaction, or is an outgoing HTLC on the commitment transaction of
|
|
// the remote peer.
|
|
isHtlc := (witnessType == lnwallet.HtlcAcceptedSuccessSecondLevel ||
|
|
witnessType == lnwallet.HtlcOfferedRemoteTimeout)
|
|
|
|
return kidOutput{
|
|
breachedOutput: makeBreachedOutput(
|
|
outpoint, witnessType, nil, signDescriptor,
|
|
),
|
|
isHtlc: isHtlc,
|
|
originChanPoint: *originChanPoint,
|
|
blocksToMaturity: blocksToMaturity,
|
|
absoluteMaturity: absoluteMaturity,
|
|
}
|
|
}
|
|
|
|
func (k *kidOutput) OriginChanPoint() *wire.OutPoint {
|
|
return &k.originChanPoint
|
|
}
|
|
|
|
func (k *kidOutput) BlocksToMaturity() uint32 {
|
|
return k.blocksToMaturity
|
|
}
|
|
|
|
func (k *kidOutput) SetConfHeight(height uint32) {
|
|
k.confHeight = height
|
|
}
|
|
|
|
func (k *kidOutput) ConfHeight() uint32 {
|
|
return k.confHeight
|
|
}
|
|
|
|
// Encode 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 (k *kidOutput) Encode(w io.Writer) error {
|
|
var scratch [8]byte
|
|
byteOrder.PutUint64(scratch[:], uint64(k.Amount()))
|
|
if _, err := w.Write(scratch[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := writeOutpoint(w, k.OutPoint()); err != nil {
|
|
return err
|
|
}
|
|
if err := writeOutpoint(w, k.OriginChanPoint()); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := binary.Write(w, byteOrder, k.isHtlc); err != nil {
|
|
return err
|
|
}
|
|
|
|
byteOrder.PutUint32(scratch[:4], k.BlocksToMaturity())
|
|
if _, err := w.Write(scratch[:4]); err != nil {
|
|
return err
|
|
}
|
|
|
|
byteOrder.PutUint32(scratch[:4], k.absoluteMaturity)
|
|
if _, err := w.Write(scratch[:4]); err != nil {
|
|
return err
|
|
}
|
|
|
|
byteOrder.PutUint32(scratch[:4], k.ConfHeight())
|
|
if _, err := w.Write(scratch[:4]); err != nil {
|
|
return err
|
|
}
|
|
|
|
byteOrder.PutUint16(scratch[:2], uint16(k.WitnessType()))
|
|
if _, err := w.Write(scratch[:2]); err != nil {
|
|
return err
|
|
}
|
|
|
|
return lnwallet.WriteSignDescriptor(w, k.SignDesc())
|
|
}
|
|
|
|
// Decode 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 (k *kidOutput) Decode(r io.Reader) error {
|
|
var scratch [8]byte
|
|
|
|
if _, err := r.Read(scratch[:]); err != nil {
|
|
return err
|
|
}
|
|
k.amt = btcutil.Amount(byteOrder.Uint64(scratch[:]))
|
|
|
|
if err := readOutpoint(io.LimitReader(r, 40), &k.outpoint); err != nil {
|
|
return err
|
|
}
|
|
|
|
err := readOutpoint(io.LimitReader(r, 40), &k.originChanPoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := binary.Read(r, byteOrder, &k.isHtlc); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := r.Read(scratch[:4]); err != nil {
|
|
return err
|
|
}
|
|
k.blocksToMaturity = byteOrder.Uint32(scratch[:4])
|
|
|
|
if _, err := r.Read(scratch[:4]); err != nil {
|
|
return err
|
|
}
|
|
k.absoluteMaturity = byteOrder.Uint32(scratch[:4])
|
|
|
|
if _, err := r.Read(scratch[:4]); err != nil {
|
|
return err
|
|
}
|
|
k.confHeight = byteOrder.Uint32(scratch[:4])
|
|
|
|
if _, err := r.Read(scratch[:2]); err != nil {
|
|
return err
|
|
}
|
|
k.witnessType = lnwallet.WitnessType(byteOrder.Uint16(scratch[:2]))
|
|
|
|
return lnwallet.ReadSignDescriptor(r, &k.signDesc)
|
|
}
|
|
|
|
// 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)
|
|
_, err := w.Write(scratch)
|
|
return err
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Compile-time constraint to ensure kidOutput and babyOutput implement the
|
|
// CsvSpendableOutput interface.
|
|
var _ sweep.CsvSpendableOutput = (*kidOutput)(nil)
|
|
var _ sweep.CsvSpendableOutput = (*babyOutput)(nil)
|