lnd.xprv/contractcourt/htlc_outgoing_contest_resolver.go

225 lines
7.3 KiB
Go

package contractcourt
import (
"fmt"
"io"
"github.com/btcsuite/btcutil"
)
// htlcOutgoingContestResolver is a ContractResolver that's able to resolve an
// outgoing HTLC that is still contested. An HTLC is still contested, if at the
// time that we broadcast the commitment transaction, it isn't able to be fully
// resolved. In this case, we'll either wait for the HTLC to timeout, or for
// us to learn of the preimage.
type htlcOutgoingContestResolver struct {
// htlcTimeoutResolver is the inner solver that this resolver may turn
// into. This only happens if the HTLC expires on-chain.
htlcTimeoutResolver
}
// Resolve commences the resolution of this contract. As this contract hasn't
// yet timed out, we'll wait for one of two things to happen
//
// 1. The HTLC expires. In this case, we'll sweep the funds and send a clean
// up cancel message to outside sub-systems.
//
// 2. The remote party sweeps this HTLC on-chain, in which case we'll add the
// pre-image to our global cache, then send a clean up settle message
// backwards.
//
// When either of these two things happens, we'll create a new resolver which
// is able to handle the final resolution of the contract. We're only the pivot
// point.
func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
// If we're already full resolved, then we don't have anything further
// to do.
if h.resolved {
return nil, nil
}
// Otherwise, we'll watch for two external signals to decide if we'll
// morph into another resolver, or fully resolve the contract.
//
// The output we'll be watching for is the *direct* spend from the HTLC
// output. If this isn't our commitment transaction, it'll be right on
// the resolution. Otherwise, we fetch this pointer from the input of
// the time out transaction.
outPointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
if err != nil {
return nil, err
}
// First, we'll register for a spend notification for this output. If
// the remote party sweeps with the pre-image, we'll be notified.
spendNtfn, err := h.Notifier.RegisterSpendNtfn(
outPointToWatch, scriptToWatch, h.broadcastHeight,
)
if err != nil {
return nil, err
}
// We'll quickly check to see if the output has already been spent.
select {
// If the output has already been spent, then we can stop early and
// sweep the pre-image from the output.
case commitSpend, ok := <-spendNtfn.Spend:
if !ok {
return nil, fmt.Errorf("quitting")
}
// TODO(roasbeef): Checkpoint?
return h.claimCleanUp(commitSpend)
// If it hasn't, then we'll watch for both the expiration, and the
// sweeping out this output.
default:
}
// We'll check the current height, if the HTLC has already expired,
// then we'll morph immediately into a resolver that can sweep the
// HTLC.
//
// TODO(roasbeef): use grace period instead?
_, currentHeight, err := h.ChainIO.GetBestBlock()
if err != nil {
return nil, err
}
// If the current height is >= expiry-1, then a spend will be valid to
// be included in the next block, and we can immediately return the
// resolver.
//
// TODO(joostjager): Statement above may not be valid. For CLTV locks,
// the expiry value is the last _invalid_ block. The likely reason that
// this does not create a problem, is that utxonursery is checking the
// expiry again (in the proper way). Same holds for minus one operation
// below.
//
// Source:
// https://github.com/btcsuite/btcd/blob/991d32e72fe84d5fbf9c47cd604d793a0cd3a072/blockchain/validate.go#L154
if uint32(currentHeight) >= h.htlcResolution.Expiry-1 {
log.Infof("%T(%v): HTLC has expired (height=%v, expiry=%v), "+
"transforming into timeout resolver", h,
h.htlcResolution.ClaimOutpoint, currentHeight,
h.htlcResolution.Expiry)
return &h.htlcTimeoutResolver, nil
}
// If we reach this point, then we can't fully act yet, so we'll await
// either of our signals triggering: the HTLC expires, or we learn of
// the preimage.
blockEpochs, err := h.Notifier.RegisterBlockEpochNtfn(nil)
if err != nil {
return nil, err
}
defer blockEpochs.Cancel()
for {
select {
// A new block has arrived, we'll check to see if this leads to
// HTLC expiration.
case newBlock, ok := <-blockEpochs.Epochs:
if !ok {
return nil, fmt.Errorf("quitting")
}
// If this new height expires the HTLC, then we can
// exit early and create a resolver that's capable of
// handling the time locked output.
newHeight := uint32(newBlock.Height)
if newHeight >= h.htlcResolution.Expiry-1 {
log.Infof("%T(%v): HTLC has expired "+
"(height=%v, expiry=%v), transforming "+
"into timeout resolver", h,
h.htlcResolution.ClaimOutpoint,
newHeight, h.htlcResolution.Expiry)
return &h.htlcTimeoutResolver, nil
}
// The output has been spent! This means the preimage has been
// revealed on-chain.
case commitSpend, ok := <-spendNtfn.Spend:
if !ok {
return nil, fmt.Errorf("quitting")
}
// The only way this output can be spent by the remote
// party is by revealing the preimage. So we'll perform
// our duties to clean up the contract once it has been
// claimed.
return h.claimCleanUp(commitSpend)
case <-h.Quit:
return nil, fmt.Errorf("resolver cancelled")
}
}
}
// report returns a report on the resolution state of the contract.
func (h *htlcOutgoingContestResolver) report() *ContractReport {
// No locking needed as these values are read-only.
finalAmt := h.htlcAmt.ToSatoshis()
if h.htlcResolution.SignedTimeoutTx != nil {
finalAmt = btcutil.Amount(
h.htlcResolution.SignedTimeoutTx.TxOut[0].Value,
)
}
return &ContractReport{
Outpoint: h.htlcResolution.ClaimOutpoint,
Incoming: false,
Amount: finalAmt,
MaturityHeight: h.htlcResolution.Expiry,
LimboBalance: finalAmt,
Stage: 1,
}
}
// Stop signals the resolver to cancel any current resolution processes, and
// suspend.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcOutgoingContestResolver) Stop() {
close(h.Quit)
}
// IsResolved returns true if the stored state in the resolve is fully
// resolved. In this case the target output can be forgotten.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcOutgoingContestResolver) IsResolved() bool {
return h.resolved
}
// Encode writes an encoded version of the ContractResolver into the passed
// Writer.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcOutgoingContestResolver) Encode(w io.Writer) error {
return h.htlcTimeoutResolver.Encode(w)
}
// Decode attempts to decode an encoded ContractResolver from the passed Reader
// instance, returning an active ContractResolver instance.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcOutgoingContestResolver) Decode(r io.Reader) error {
return h.htlcTimeoutResolver.Decode(r)
}
// AttachResolverKit should be called once a resolved is successfully decoded
// from its stored format. This struct delivers a generic tool kit that
// resolvers need to complete their duty.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcOutgoingContestResolver) AttachResolverKit(r ResolverKit) {
h.ResolverKit = r
}
// A compile time assertion to ensure htlcOutgoingContestResolver meets the
// ContractResolver interface.
var _ ContractResolver = (*htlcOutgoingContestResolver)(nil)