From 09b6bee8d4f646ccf29a0a020882c3c4a7264a78 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 16 Jan 2018 19:44:45 -0800 Subject: [PATCH] contractcourt: add complete ContractResolver implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this commit, we introduce a new interface, the ContractResolver. The duty of a ContractResolver is to watch a contract on-chain, for all possible transitions, and exit finally when the contract has been fully resolved. Resolvers themselves can be recursive: meaning producing another resolver to hand off the duties require to fully resolve a contract. Each resolver also has a ResolverKit which contains all the function closures and interfaces that the resolver need to properly do its job. The 5 types of resolvers are: * outgoing HTLC timeout * outgoing HTLC contested * incoming HTLC know presage * incoming HTLC contested (don’t yet know) * commitment sweep In the future, more advanced resolver types can be added as required. --- contractcourt/contract_resolvers.go | 1461 ++++++++++++++++++++++ contractcourt/contract_resolvers_test.go | 1 + 2 files changed, 1462 insertions(+) create mode 100644 contractcourt/contract_resolvers.go create mode 100644 contractcourt/contract_resolvers_test.go diff --git a/contractcourt/contract_resolvers.go b/contractcourt/contract_resolvers.go new file mode 100644 index 00000000..f5f79144 --- /dev/null +++ b/contractcourt/contract_resolvers.go @@ -0,0 +1,1461 @@ +package contractcourt + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "fmt" + "io" + "io/ioutil" + + "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/roasbeef/btcd/txscript" + "github.com/roasbeef/btcd/wire" +) + +var ( + endian = binary.BigEndian +) + +// ContractResolver is an interface which packages a state machine which is +// able to carry out the necessary steps required to fully resolve a Bitcoin +// contract on-chain. Resolvers are fully encodable to ensure callers are able +// to persist them properly. A resolver may produce another resolver in the +// case that claiming an HTLC is a multi-stage process. In this case, we may +// partially resolve the contract, then persist, and set up for an additional +// resolution. +type ContractResolver interface { + // ResolverKey returns an identifier which should be globally unique + // for this particular resolver within the chain the original contract + // resides within. + ResolverKey() []byte + + // Resolve instructs the contract resolver to resolve the output + // on-chain. Once the output has been *fully* resolved, the function + // should return immediately with a nil ContractResolver value for the + // first return value. In the case that the contract requires further + // resolution, then another resolve is returned. + // + // NOTE: This function MUST be run as a goroutine. + Resolve() (ContractResolver, error) + + // IsResolved returns true if the stored state in the resolve is fully + // resolved. In this case the target output can be forgotten. + IsResolved() bool + + // Encode writes an encoded version of the ContractResolver into the + // passed Writer. + Encode(w io.Writer) error + + // Decode attempts to decode an encoded ContractResolver from the + // passed Reader instance, returning an active ContractResolver + // instance. + Decode(r io.Reader) error + + // 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. + AttachResolverKit(ResolverKit) + + // Stop signals the resolver to cancel any current resolution + // processes, and suspend. + Stop() +} + +// ResolverKit is meant to be used as a mix-in struct to be embedded within a +// given ContractResolver implementation. It contains all the items that a +// resolver requires to carry out its duties. +type ResolverKit struct { + // ChannelArbiratorConfig contains all the interfaces and closures + // required for the resolver to interact with outside sub-systems. + ChannelArbitratorConfig + + // Checkpoint allows a resolver to check point its state. This function + // should write the state of the resolver to persistent storage, and + // return a non-nil error upon success. + Checkpoint func(ContractResolver) error + + Quit chan struct{} +} + +// htlcTimeoutResolver is a ContractResolver that's capable of resolving an +// outgoing HTLC. The HTLC may be on our commitment transaction, or on the +// commitment transaction of the remote party. An output on our commitment +// transaction is considered fully resolved once the second-level transaction +// has been confirmed (and reached a sufficient depth). An output on the +// commitment transaction of the remote party is resolved once we detect a +// spend of the direct HTLC output using the timeout clause. +type htlcTimeoutResolver struct { + // htlcResolution contains all the information required to properly + // resolve this outgoing HTLC. + htlcResolution lnwallet.OutgoingHtlcResolution + + // outputIncubating returns true if we've sent the output to the output + // incubator (utxo nursery). + outputIncubating bool + + // resolved reflects if the contract has been fully resolved or not. + resolved bool + + // broadcastHeight is the height that the original contract was + // broadcast to the main-chain at. We'll use this value to bound any + // historical queries to the chain for spends/confirmations. + // + // TODO(roasbeef): wrap above into definite resolution embedding? + broadcastHeight uint32 + + // htlcIndex is the index of this HTLC within the trace of the + // additional commitment state machine. + htlcIndex uint64 + + ResolverKit +} + +// ResolverKey returns an identifier which should be globally unique for this +// particular resolver within the chain the original contract resides within. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcTimeoutResolver) ResolverKey() []byte { + // The primary key for this resolver will be the outpoint of the HTLC + // on the commitment transaction itself. If this is our commitment, + // then the output can be found within the signed timeout tx, + // otherwise, it's just the ClaimOutpoint. + var op wire.OutPoint + if h.htlcResolution.SignedTimeoutTx != nil { + op = h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint + } else { + op = h.htlcResolution.ClaimOutpoint + } + + key := newResolverID(op) + return key[:] +} + +// Resolve kicks off full resolution of an outgoing HTLC output. If it's our +// commitment, it isn't resolved until we see the second level HTLC txn +// confirmed. If it's the remote party's commitment, we don't resolve until we +// see a direct sweep via the timeout clause. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { + // If we're already resolved, then we can exit early. + if h.resolved { + return nil, nil + } + + // If we haven't already sent the output to the utxo nursery, then + // we'll do so now. + if !h.outputIncubating { + log.Tracef("%T(%v): incubating htlc output", h, + h.htlcResolution.ClaimOutpoint) + + err := h.IncubateOutputs(h.ChanPoint, nil, &h.htlcResolution, nil) + if err != nil { + return nil, err + } + + h.outputIncubating = true + + if err := h.Checkpoint(h); err != nil { + log.Errorf("unable to Checkpoint: %v", err) + } + } + + // waitForOutputResolution waits for the HTLC output to be fully + // resolved. The output is considered fully resolved once it has been + // spent, and the spending transaction has been fully confirmed. + waitForOutputResolution := func() error { + // We first need to register to see when the HTLC output itself + // has been spent so we can wait for the spending transaction + // to confirm. + spendNtfn, err := h.Notifier.RegisterSpendNtfn( + &h.htlcResolution.ClaimOutpoint, + h.broadcastHeight, + ) + if err != nil { + return err + } + + var spendDetail *chainntnfs.SpendDetail + select { + case s, ok := <-spendNtfn.Spend: + if !ok { + return fmt.Errorf("notifier quit") + } + + spendDetail = s + + case <-h.Quit: + return fmt.Errorf("quitting") + } + + // Now that the output has been spent, we'll also wait for the + // transaction to be confirmed before proceeding. + confNtfn, err := h.Notifier.RegisterConfirmationsNtfn( + spendDetail.SpenderTxHash, 1, + uint32(spendDetail.SpendingHeight-1), + ) + if err != nil { + return err + } + + log.Infof("%T(%v): waiting for spending (txid=%v) to be fully "+ + "confirmed", h, h.htlcResolution.ClaimOutpoint, + spendDetail.SpenderTxHash) + + select { + case _, ok := <-confNtfn.Confirmed: + if !ok { + return fmt.Errorf("notifier quit") + } + + case <-h.Quit: + return fmt.Errorf("quitting") + } + + return nil + } + + // With the output sent to the nursery, we'll now wait until the output + // has been fully resolved before sending the clean up message. + // + // TODO(roasbeef): need to be able to cancel nursery? + // * if they pull on-chain while we're waiting + + // If we don't have a second layer transaction, then this is a remote + // party's commitment, so we'll watch for a direct spend. + if h.htlcResolution.SignedTimeoutTx == nil { + // We'll block until: the HTLC output has been spent, and the + // transaction spending that output is sufficiently confirmed. + log.Infof("%T(%v): waiting for nursery to spend CLTV-locked "+ + "output", h, h.htlcResolution.ClaimOutpoint) + if err := waitForOutputResolution(); err != nil { + return nil, err + } + } else { + // Otherwise, this is our commitment, so we'll watch for the + // second-level transaction to be sufficiently confirmed. + secondLevelTXID := h.htlcResolution.SignedTimeoutTx.TxHash() + confNtfn, err := h.Notifier.RegisterConfirmationsNtfn( + &secondLevelTXID, 1, h.broadcastHeight, + ) + if err != nil { + return nil, err + } + + log.Infof("%T(%v): waiting second-level tx (txid=%v) to be "+ + "fully confirmed", h, h.htlcResolution.ClaimOutpoint, + secondLevelTXID) + + select { + case _, ok := <-confNtfn.Confirmed: + if !ok { + return nil, fmt.Errorf("quitting") + } + + case <-h.Quit: + return nil, fmt.Errorf("quitting") + } + } + + // TODO(roasbeef): need to watch for remote party sweeping with pre-image? + // * have another waiting on spend above, will check the type, if it's + // pre-image, then we'll cancel, and send a clean up back with + // pre-image, also add to preimage cache + + log.Infof("%T(%v): resolving htlc with incoming fail msg, fully "+ + "confirmed", h, h.htlcResolution.ClaimOutpoint) + + // At this point, the second-level transaction is sufficiently + // confirmed, or a transaction directly spending the output is. + // Therefore, we can now send back our clean up message. + failureMsg := &lnwire.FailPermanentChannelFailure{} + if err := h.DeliverResolutionMsg(ResolutionMsg{ + SourceChan: h.ShortChanID, + HtlcIndex: h.htlcIndex, + Failure: failureMsg, + }); err != nil { + return nil, err + } + + // Finally, if this was an output on our commitment transaction, we'll + // for the second-level HTLC output to be spent, and for that + // transaction itself to confirm. + if h.htlcResolution.SignedTimeoutTx != nil { + log.Infof("%T(%v): waiting for nursery to spend CSV delayed "+ + "output", h, h.htlcResolution.ClaimOutpoint) + if err := waitForOutputResolution(); err != nil { + return nil, err + } + } + + // With the clean up message sent, we'll now mark the contract + // resolved, and wait. + h.resolved = true + return nil, h.Checkpoint(h) +} + +// Stop signals the resolver to cancel any current resolution processes, and +// suspend. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcTimeoutResolver) 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 *htlcTimeoutResolver) 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 *htlcTimeoutResolver) Encode(w io.Writer) error { + // First, we'll write out the relevant fields of the + // OutgoingHtlcResolution to the writer. + if err := encodeOutgoingResolution(w, &h.htlcResolution); err != nil { + return err + } + + // With that portion written, we can now write out the fields specific + // to the resolver itself. + if err := binary.Write(w, endian, h.outputIncubating); err != nil { + return err + } + if err := binary.Write(w, endian, h.resolved); err != nil { + return err + } + if err := binary.Write(w, endian, h.broadcastHeight); err != nil { + return err + } + + if err := binary.Write(w, endian, h.htlcIndex); err != nil { + return err + } + + return nil +} + +// 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 *htlcTimeoutResolver) Decode(r io.Reader) error { + // First, we'll read out all the mandatory fields of the + // OutgoingHtlcResolution that we store. + if err := decodeOutgoingResolution(r, &h.htlcResolution); err != nil { + return err + } + + // With those fields read, we can now read back the fields that are + // specific to the resolver itself. + if err := binary.Read(r, endian, &h.outputIncubating); err != nil { + return err + } + if err := binary.Read(r, endian, &h.resolved); err != nil { + return err + } + if err := binary.Read(r, endian, &h.broadcastHeight); err != nil { + return err + } + + if err := binary.Read(r, endian, &h.htlcIndex); err != nil { + return err + } + + return nil +} + +// 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 *htlcTimeoutResolver) AttachResolverKit(r ResolverKit) { + h.ResolverKit = r +} + +// A compile time assertion to ensure htlcTimeoutResolver meets the +// ContractResolver interface. +var _ ContractResolver = (*htlcTimeoutResolver)(nil) + +// htlcSuccessResolver is a resolver that's capable of sweeping an incoming +// HTLC output on-chain. If this is the remote party's commitment, we'll sweep +// it directly from the commitment output *immediately*. If this is our +// commitment, we'll first broadcast the success transaction, then send it to +// the incubator for sweeping. That's it, no need to send any clean up +// messages. +// +// TODO(roasbeef): don't need to broadcast? +type htlcSuccessResolver struct { + // htlcResolution is the incoming HTLC resolution for this HTLC. It + // contains everything we need to properly resolve this HTLC. + htlcResolution lnwallet.IncomingHtlcResolution + + // outputIncubating returns true if we've sent the output to the output + // incubator (utxo nursery). + outputIncubating bool + + // resolved reflects if the contract has been fully resolved or not. + resolved bool + + // broadcastHeight is the height that the original contract was + // broadcast to the main-chain at. We'll use this value to bound any + // historical queries to the chain for spends/confirmations. + broadcastHeight uint32 + + // payHash is the payment hash of the original HTLC extended to us. + payHash [32]byte + + // sweepTx will be non-nil if we've already crafted a transaction to + // sweep a direct HTLC output. This is only a concern if we're sweeping + // from the commitment transaction of the remote party. + // + // TODO(roasbeef): send off to utxobundler + sweepTx *wire.MsgTx + + ResolverKit +} + +// ResolverKey returns an identifier which should be globally unique for this +// particular resolver within the chain the original contract resides within. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcSuccessResolver) ResolverKey() []byte { + // The primary key for this resolver will be the outpoint of the HTLC + // on the commitment transaction itself. If this is our commitment, + // then the output can be found within the signed success tx, + // otherwise, it's just the ClaimOutpoint. + var op wire.OutPoint + if h.htlcResolution.SignedSuccessTx != nil { + op = h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint + } else { + op = h.htlcResolution.ClaimOutpoint + } + + key := newResolverID(op) + return key[:] +} + +// Resolve attempts to resolve an unresolved incoming HTLC that we know the +// preimage to. If the HTLC is on the commitment of the remote party, then +// we'll simply sweep it directly. Otherwise, we'll hand this off to the utxo +// nursery to do its duty. +// +// TODO(roasbeef): create multi to batch +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { + // If we're already resolved, then we can exit early. + if h.resolved { + return nil, nil + } + + // If we don't have a success transaction, then this means that this is + // an output on the remote party's commitment transaction. + if h.htlcResolution.SignedSuccessTx == nil { + // If we don't already have the sweep transaction constructed, + // we'll do so and broadcast it. + if h.sweepTx == nil { + log.Infof("%T(%x): crafting sweep tx for "+ + "incoming+remote htlc confirmed", h, + h.payHash[:]) + + // In this case, we can sweep it directly from the + // commitment output. We'll first grab a fresh address + // from the wallet to sweep the output. + addr, err := h.NewSweepAddr() + if err != nil { + return nil, err + } + + // With out address obtained, we'll query for an + // estimate to be confirmed at ease. + // + // TODO(roasbeef): signal up if fee would be too large + // to sweep singly, need to batch + satWeight, err := h.FeeEstimator.EstimateFeePerWeight(6) + if err != nil { + return nil, err + } + + log.Debugf("%T(%x): using %v sat/weight to sweep htlc", + "incoming+remote htlc confirmed", h, h.payHash[:]) + + // Using a weight estimator, we'll compute the total + // fee required, and from that the value we'll end up + // with. + totalWeight := (&lnwallet.TxWeightEstimator{}). + AddWitnessInput(lnwallet.OfferedHtlcSuccessWitnessSize). + AddP2WKHOutput().Weight() + totalFees := int64(totalWeight) * int64(satWeight) + sweepAmt := h.htlcResolution.SweepSignDesc.Output.Value - totalFees + + // With the fee computation finished, we'll now + // construct the sweep transaction. + htlcPoint := h.htlcResolution.ClaimOutpoint + h.sweepTx = wire.NewMsgTx(2) + h.sweepTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: htlcPoint, + }) + h.sweepTx.AddTxOut(&wire.TxOut{ + PkScript: addr, + Value: sweepAmt, + }) + + log.Infof("%T(%v): crafted sweep tx=%v", h, + h.payHash[:], spew.Sdump(h.sweepTx)) + + // With the transaction fully assembled, we can now + // generate a valid witness for the transaction. + h.htlcResolution.SweepSignDesc.SigHashes = txscript.NewTxSigHashes( + h.sweepTx, + ) + h.sweepTx.TxIn[0].Witness, err = lnwallet.SenderHtlcSpendRedeem( + h.Signer, &h.htlcResolution.SweepSignDesc, h.sweepTx, + h.htlcResolution.Preimage[:], + ) + if err != nil { + return nil, err + } + + // With the sweep transaction confirmed, we'll now + // Checkpoint our state. + if err := h.Checkpoint(h); err != nil { + log.Errorf("unable to Checkpoint: %v", err) + } + + // Finally, we'll broadcast the sweep transaction to + // the network. + // + // TODO(roasbeef): validate first? + if err := h.PublishTx(h.sweepTx); err != nil { + return nil, err + } + } + + // With the sweep transaction broadcast, we'll wait for its + // confirmation. + sweepTXID := h.sweepTx.TxHash() + confNtfn, err := h.Notifier.RegisterConfirmationsNtfn( + &sweepTXID, 1, h.broadcastHeight, + ) + if err != nil { + return nil, err + } + select { + case _, ok := <-confNtfn.Confirmed: + if !ok { + return nil, fmt.Errorf("quitting") + } + + case <-h.Quit: + return nil, fmt.Errorf("quitting") + } + + log.Infof("%T(%x): waiting for sweep tx (txid=%v) to be "+ + "confirmed", h, h.payHash[:], sweepTXID) + + // Once the transaction has received a sufficient number of + // confirmations, we'll mark ourselves as fully resolved and exit. + h.resolved = true + return nil, h.Checkpoint(h) + } + + log.Infof("%T(%x): broadcasting second-layer transition tx: %v", + h, h.payHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx)) + + // We'll now broadcast the second layer transaction so we can kick off + // the claiming process. + // + // TODO(roasbeef): after changing sighashes send to tx bundler + if err := h.PublishTx(h.htlcResolution.SignedSuccessTx); err != nil { + // TODO(roasbeef): detect double spends + return nil, err + } + + // Otherwise, this is an output on our commitment transaction. In this + // case, we'll send it to the incubator, but only if we haven't already + // done so. + if !h.outputIncubating { + log.Infof("%T(%x): incubating incoming htlc output", + h, h.payHash[:]) + + err := h.IncubateOutputs(h.ChanPoint, nil, nil, &h.htlcResolution) + if err != nil { + return nil, err + } + + h.outputIncubating = true + + if err := h.Checkpoint(h); err != nil { + log.Errorf("unable to Checkpoint: %v", err) + } + } + + // To wrap this up, we'll wait until the second-level transaction has + // been spent, then fully resolve the contract. + spendNtfn, err := h.Notifier.RegisterSpendNtfn( + &h.htlcResolution.ClaimOutpoint, h.broadcastHeight, + ) + if err != nil { + return nil, err + } + + log.Infof("%T(%x): waiting for second-level HTLC output to be spent "+ + "after csv_delay=%v", h, h.payHash[:], h.htlcResolution.CsvDelay) + + select { + case _, ok := <-spendNtfn.Spend: + if !ok { + return nil, fmt.Errorf("quitting") + } + + case <-h.Quit: + return nil, fmt.Errorf("quitting") + } + + h.resolved = true + return nil, h.Checkpoint(h) +} + +// Stop signals the resolver to cancel any current resolution processes, and +// suspend. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcSuccessResolver) 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 *htlcSuccessResolver) 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 *htlcSuccessResolver) Encode(w io.Writer) error { + // First we'll encode our inner HTLC resolution. + if err := encodeIncomingResolution(w, &h.htlcResolution); err != nil { + return err + } + + // Next, we'll write out the fields that are specified to the contract + // resolver. + if err := binary.Write(w, endian, h.outputIncubating); err != nil { + return err + } + if err := binary.Write(w, endian, h.resolved); err != nil { + return err + } + if err := binary.Write(w, endian, h.broadcastHeight); err != nil { + return err + } + if _, err := w.Write(h.payHash[:]); err != nil { + return err + } + + return nil +} + +// 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 *htlcSuccessResolver) Decode(r io.Reader) error { + // First we'll decode our inner HTLC resolution. + if err := decodeIncomingResolution(r, &h.htlcResolution); err != nil { + return err + } + + // Next, we'll read all the fields that are specified to the contract + // resolver. + if err := binary.Read(r, endian, &h.outputIncubating); err != nil { + return err + } + if err := binary.Read(r, endian, &h.resolved); err != nil { + return err + } + if err := binary.Read(r, endian, &h.broadcastHeight); err != nil { + return err + } + if _, err := io.ReadFull(r, h.payHash[:]); err != nil { + return err + } + + return nil +} + +// 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 *htlcSuccessResolver) AttachResolverKit(r ResolverKit) { + h.ResolverKit = r +} + +// A compile time assertion to ensure htlcSuccessResolver meets the +// ContractResolver interface. +var _ ContractResolver = (*htlcSuccessResolver)(nil) + +// 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. This 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 + } + + // claimCleanUp is a helper function that's called once the HTLC output + // is spent by the remote party. It'll extract the preimage, add it to + // the global cache, and finally send the appropriate clean up message. + claimCleanUp := func(commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) { + // Depending on if this is our commitment or not, then we'll be + // looking for a different witness pattern. + spenderIndex := commitSpend.SpenderInputIndex + spendingInput := commitSpend.SpendingTx.TxIn[spenderIndex] + + log.Infof("%T(%v): extracting preimage! remote party spent "+ + "HTLC with tx=%v", h, h.htlcResolution.ClaimOutpoint, + spew.Sdump(commitSpend.SpendingTx)) + + // If this is the remote party's commitment, then we'll be + // looking for them to spend using the second-level success + // transaction. + var preimage [32]byte + if h.htlcResolution.SignedTimeoutTx == nil { + // The witness stack when the remote party sweeps the + // output to them looks like: + // + // * + copy(preimage[:], spendingInput.Witness[3]) + } else { + // Otherwise, they'll be spending directly from our + // commitment output. In which case the witness stack + // looks like: + // + // * + copy(preimage[:], spendingInput.Witness[1]) + } + + log.Infof("%T(%v): extracting preimage=%x from on-chain "+ + "spend!", h, h.htlcResolution.ClaimOutpoint, preimage[:]) + + // With the preimage obtained, we can now add it to the global + // cache. + if err := h.PreimageDB.AddPreimage(preimage[:]); err != nil { + log.Errorf("%T(%v): unable to add witness to cache", + h, h.htlcResolution.ClaimOutpoint) + } + + // Finally, we'll send the clean up message, mark ourselves as + // resolved, then exit. + if err := h.DeliverResolutionMsg(ResolutionMsg{ + SourceChan: h.ShortChanID, + HtlcIndex: h.htlcIndex, + PreImage: &preimage, + }); err != nil { + return nil, err + } + h.resolved = true + return nil, h.Checkpoint(h) + } + + // Otherwise, we'll watch for two external signals to decide if we'll + // morph into another resolver, or fully resolve the contract. + + // 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( + &h.htlcResolution.ClaimOutpoint, + 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 + case commitSpend, ok := <-spendNtfn.Spend: + if !ok { + return nil, fmt.Errorf("quitting") + } + + // TODO(roasbeef): Checkpoint? + return 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 uint32(currentHeight) >= h.htlcResolution.Expiry { + log.Infof("%T(%v): HTLC has expired (height=%v, expiry=%v), "+ + "transforming into timeout resolver", h, + h.htlcResolution.ClaimOutpoint) + 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() + if err != nil { + return nil, err + } + 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 claimCleanUp(commitSpend) + + case <-h.Quit: + return nil, fmt.Errorf("resolver cancelled") + } + } +} + +// 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) + +// htlcIncomingContestResolver is a ContractResolver that's able to resolve an +// incoming HTLC that is still contested. An HTLC is still contested, if at the +// time of commitment broadcast, we don't know of the preimage for it yet, and +// it hasn't expired. In this case, we can resolve the HTLC if we learn of the +// preimage, otherwise the remote party will sweep it after it expires. +// +// TODO(roabseef): just embed the other resolver? +type htlcIncomingContestResolver struct { + // htlcExpiry is the absolute expiry of this incoming HTLC. We use this + // value to determine if we can exit early as if the HTLC times out, + // before we learn of the preimage then we can't claim it on chain + // successfully. + htlcExpiry uint32 + + // htlcSuccessResolver is the inner resolver that may be utilized if we + // learn of the preimage. + htlcSuccessResolver +} + +// Resolve attempts to resolve this contract. As we don't yet know of the +// preimage for the contract, we'll wait for one of two things to happen: +// +// 1. We learn of the preimage! In this case, we can sweep the HTLC incoming +// and ensure that if this was a multi-hop HTLC we are made whole. In this +// case, an additional ContractResolver will be returned to finish the +// job. +// +// 2. The HTLC expires. If this happens, then the contract is fully resolved +// as we have no remaining actions left at our disposal. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { + // If we're already full resolved, then we don't have anything further + // to do. + if h.resolved { + return nil, nil + } + + // We'll first check if this HTLC has been timed out, if so, we can + // return now and mark ourselves as resolved. + _, currentHeight, err := h.ChainIO.GetBestBlock() + if err != nil { + return nil, err + } + + // If we're past the point of expiry of the HTLC, then at this point + // the sender can sweep it, so we'll end our lifetime. + if uint32(currentHeight) >= h.htlcExpiry { + // TODO(roasbeef): should also somehow check if outgoing is + // resolved or not + // * may need to hook into the circuit map + // * can't timeout before the outgoing has been + + log.Infof("%T(%v): HTLC has timed out (expiry=%v, height=%v), "+ + "abandoning", h, h.htlcResolution.ClaimOutpoint, + h.htlcExpiry, currentHeight) + h.resolved = true + return nil, h.Checkpoint(h) + } + + // applyPreimage is a helper function that will populate our internal + // resolver with the preimage we learn of. This should be called once + // the preimage is revealed so the inner resolver can properly complete + // its duties. + applyPreimage := func(preimage []byte) { + copy(h.htlcResolution.Preimage[:], preimage) + + log.Infof("%T(%v): extracted preimage=%x from beacon!", h, + h.htlcResolution.ClaimOutpoint, preimage[:]) + + // If this our commitment transaction, then we'll need to + // populate the witness for the second-level HTLC transaction. + if h.htlcResolution.SignedSuccessTx != nil { + // Within the witness for the success transaction, the + // preimage is the 4th element as it looks like: + // + // * + // + // We'll populate it within the witness, as since this + // was a "contest" resolver, we didn't yet know of the + // preimage. + h.htlcResolution.SignedSuccessTx.TxIn[0].Witness[3] = preimage[:] + } + + copy(h.htlcResolution.Preimage[:], preimage[:]) + } + + // If the HTLC hasn't yet expired, then we'll query to see if we + // already know the preimage. + preimage, ok := h.PreimageDB.LookupPreimage(h.payHash[:]) + if ok { + // If we do, then this means we can claim the HTLC! However, + // we don't know how to ourselves, so we'll return our inner + // resolver which has the knowledge to do so. + applyPreimage(preimage[:]) + return &h.htlcSuccessResolver, nil + } + + // If the HTLC hasn't expired yet, then we may still be able to claim + // it if we learn of the pre-image, so we'll wait and see if it pops + // up, or the HTLC times out. + preimageSubscription := h.PreimageDB.SubcribeUpdates() + blockEpochs, err := h.Notifier.RegisterBlockEpochNtfn() + if err != nil { + return nil, err + } + defer preimageSubscription.CancelSubcription() + for { + + select { + case preimage := <-preimageSubscription.WitnessUpdates: + // If this isn't our preimage, then we'll continue + // onwards. + newHash := sha256.Sum256(preimage) + preimageMatches := bytes.Equal(newHash[:], h.payHash[:]) + if !preimageMatches { + continue + } + + // Otherwise, we've learned of the preimage! We'll add + // this information to our inner resolver, then return + // it so it can continue contract resolution. + applyPreimage(preimage) + return &h.htlcSuccessResolver, nil + + case newBlock, ok := <-blockEpochs.Epochs: + if !ok { + return nil, fmt.Errorf("quitting") + } + + // If this new height expires the HTLC, then this means + // we never found out the preimage, so we can mark + // resolved and + // exit. + newHeight := uint32(newBlock.Height) + if newHeight >= h.htlcExpiry { + log.Infof("%T(%v): HTLC has timed out "+ + "(expiry=%v, height=%v), abandoning", h, + h.htlcResolution.ClaimOutpoint, + h.htlcExpiry, currentHeight) + h.resolved = true + return nil, h.Checkpoint(h) + } + + case <-h.Quit: + return nil, fmt.Errorf("resolver stopped") + } + } +} + +// Stop signals the resolver to cancel any current resolution processes, and +// suspend. +// +// NOTE: Part of the ContractResolver interface. +func (h *htlcIncomingContestResolver) 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 *htlcIncomingContestResolver) 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 *htlcIncomingContestResolver) Encode(w io.Writer) error { + // We'll first write out the one field unique to this resolver. + if err := binary.Write(w, endian, h.htlcExpiry); err != nil { + return err + } + + // Then we'll write out our internal resolver. + return h.htlcSuccessResolver.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 *htlcIncomingContestResolver) Decode(r io.Reader) error { + // We'll first read the one field unique to this resolver. + if err := binary.Read(r, endian, &h.htlcExpiry); err != nil { + return err + } + + // Then we'll decode our internal resolver. + return h.htlcSuccessResolver.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 *htlcIncomingContestResolver) AttachResolverKit(r ResolverKit) { + h.ResolverKit = r +} + +// A compile time assertion to ensure htlcIncomingContestResolver meets the +// ContractResolver interface. +var _ ContractResolver = (*htlcIncomingContestResolver)(nil) + +// commitSweepResolver is a resolver that will attempt to sweep the commitment +// output paying to us, in the case that the remote party broadcasts their +// version of the commitment transaction. We can sweep this output immediately, +// as it doesn't have a time-lock delay. +type commitSweepResolver struct { + // commitResolution contains all data required to successfully sweep + // this HTLC on-chain. + commitResolution lnwallet.CommitOutputResolution + + // resolved reflects if the contract has been fully resolved or not. + resolved bool + + // broadcastHeight is the height that the original contract was + // broadcast to the main-chain at. We'll use this value to bound any + // historical queries to the chain for spends/confirmations. + broadcastHeight uint32 + + // chanPoint is the channel point of the original contract. + chanPoint wire.OutPoint + + // sweepTx is the fully signed transaction which when broadcast, will + // sweep the commitment output into an output under control by the + // source wallet. + sweepTx *wire.MsgTx + + ResolverKit +} + +// ResolverKey returns an identifier which should be globally unique for this +// particular resolver within the chain the original contract resides within. +func (c *commitSweepResolver) ResolverKey() []byte { + key := newResolverID(c.commitResolution.SelfOutPoint) + return key[:] +} + +// Resolve instructs the contract resolver to resolve the output on-chain. Once +// the output has been *fully* resolved, the function should return immediately +// with a nil ContractResolver value for the first return value. In the case +// that the contract requires further resolution, then another resolve is +// returned. +// +// NOTE: This function MUST be run as a goroutine. +func (c *commitSweepResolver) Resolve() (ContractResolver, error) { + // If we're already resolved, then we can exit early. + if c.resolved { + return nil, nil + } + + // First, we'll register for a notification once the commitment output + // itself has been confirmed. + // + // TODO(roasbeef): instead sweep asap if remote commit? yeh + commitTXID := c.commitResolution.SelfOutPoint.Hash + confNtfn, err := c.Notifier.RegisterConfirmationsNtfn( + &commitTXID, 1, c.broadcastHeight, + ) + if err != nil { + return nil, err + } + + log.Debugf("%T(%v): waiting for commit tx to confirm", c, c.chanPoint) + + select { + case _, ok := <-confNtfn.Confirmed: + if !ok { + return nil, fmt.Errorf("quitting") + } + + case <-c.Quit: + return nil, fmt.Errorf("quitting") + } + + // TODO(roasbeef): checkpoint tx confirmed? + + // We're dealing with our commitment transaction if the delay on the + // resolution isn't zero. + isLocalCommitTx := c.commitResolution.MaturityDelay != 0 + + switch { + // If the sweep transaction isn't already generated, and the remote + // party broadcast the commitment transaction then we'll create it now. + case c.sweepTx == nil && !isLocalCommitTx: + // Now that the commitment transaction has confirmed, we'll + // craft a transaction to sweep this output into the wallet. + signDesc := c.commitResolution.SelfOutputSignDesc + + // First, we'll estimate the total weight so we can compute + // fees properly. We'll use a lax estimate, as this output is + // in no immediate danger. + satWeight, err := c.FeeEstimator.EstimateFeePerWeight(6) + if err != nil { + return nil, err + } + + log.Debugf("%T(%v): using %v sat/weight for sweep tx", c, + c.chanPoint, int64(satWeight)) + + totalWeight := (&lnwallet.TxWeightEstimator{}). + AddP2PKHInput(). + AddP2WKHOutput().Weight() + totalFees := int64(totalWeight) * int64(satWeight) + sweepAmt := signDesc.Output.Value - totalFees + + c.sweepTx = wire.NewMsgTx(2) + c.sweepTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: c.commitResolution.SelfOutPoint, + }) + sweepAddr, err := c.NewSweepAddr() + if err != nil { + return nil, err + } + c.sweepTx.AddTxOut(&wire.TxOut{ + PkScript: sweepAddr, + Value: sweepAmt, + }) + + // With the transaction fully assembled, we can now generate a + // valid witness for the transaction. + signDesc.SigHashes = txscript.NewTxSigHashes(c.sweepTx) + c.sweepTx.TxIn[0].Witness, err = lnwallet.CommitSpendNoDelay( + c.Signer, &signDesc, c.sweepTx, + ) + if err != nil { + return nil, err + } + + log.Infof("%T(%v): sweeping commit output with tx=%v", c, + c.chanPoint, spew.Sdump(c.sweepTx)) + + // Finally, we'll broadcast the sweep transaction to the + // network. + if err := c.PublishTx(c.sweepTx); err != nil { + log.Errorf("%T(%v): unable to publish sweep tx: %v", + c, c.chanPoint, err) + return nil, err + } + + // With the sweep transaction confirmed, we'll now Checkpoint + // our state. + if err := c.Checkpoint(c); err != nil { + log.Errorf("unable to Checkpoint: %v", err) + } + + // Otherwise, this is our commitment transaction, So we'll obtain the + // sweep transaction once the commitment output has been spent. + case c.sweepTx == nil && isLocalCommitTx: + // Otherwise, if we're dealing with our local commitment + // transaction, then the output we need to sweep has been sent + // to the nursery for incubation. In this case, we'll wait + // until the commitment output has been spent. + spendNtfn, err := c.Notifier.RegisterSpendNtfn( + &c.commitResolution.SelfOutPoint, + c.broadcastHeight, + ) + if err != nil { + return nil, err + } + + log.Infof("%T(%v): waiting for commit output to be swept", c, + c.chanPoint) + + select { + case commitSpend, ok := <-spendNtfn.Spend: + if !ok { + return nil, fmt.Errorf("quitting") + } + + // Once we detect the commitment output has been spent, + // we'll extract the spending transaction itself, as we + // now consider this to be our sweep transaction. + c.sweepTx = commitSpend.SpendingTx + + log.Infof("%T(%v): commit output swept by txid=%v", + c, c.chanPoint, c.sweepTx.TxHash()) + + if err := c.Checkpoint(c); err != nil { + log.Errorf("unable to Checkpoint: %v", err) + } + case <-c.Quit: + return nil, fmt.Errorf("quitting") + } + } + + log.Infof("%T(%v): waiting for commit sweep txid=%v conf", c, c.chanPoint, + c.sweepTx.TxHash()) + + // Now we'll wait until the sweeping transaction has been fully + // confirmed. Once it's confirmed, we can mark this contract resolved. + sweepTXID := c.sweepTx.TxHash() + confNtfn, err = c.Notifier.RegisterConfirmationsNtfn( + &sweepTXID, 1, c.broadcastHeight, + ) + if err != nil { + return nil, err + } + select { + case confInfo, ok := <-confNtfn.Confirmed: + if !ok { + return nil, fmt.Errorf("quitting") + } + + log.Infof("ChannelPoint(%v) is fully closed, at height: %v", + c.chanPoint, confInfo.BlockHeight) + + case <-c.Quit: + return nil, fmt.Errorf("quitting") + } + + // Once the transaction has received a sufficient number of + // confirmations, we'll mark ourselves as fully resolved and exit. + c.resolved = true + return nil, c.Checkpoint(c) +} + +// Stop signals the resolver to cancel any current resolution processes, and +// suspend. +// +// NOTE: Part of the ContractResolver interface. +func (c *commitSweepResolver) Stop() { + close(c.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 (c *commitSweepResolver) IsResolved() bool { + return c.resolved +} + +// Encode writes an encoded version of the ContractResolver into the passed +// Writer. +// +// NOTE: Part of the ContractResolver interface. +func (c *commitSweepResolver) Encode(w io.Writer) error { + if err := encodeCommitResolution(w, &c.commitResolution); err != nil { + return err + } + + if err := binary.Write(w, endian, c.resolved); err != nil { + return err + } + if err := binary.Write(w, endian, c.broadcastHeight); err != nil { + return err + } + if _, err := w.Write(c.chanPoint.Hash[:]); err != nil { + return err + } + err := binary.Write(w, endian, c.chanPoint.Index) + if err != nil { + return err + } + + if c.sweepTx != nil { + return c.sweepTx.Serialize(w) + } + + return nil +} + +// Decode attempts to decode an encoded ContractResolver from the passed Reader +// instance, returning an active ContractResolver instance. +// +// NOTE: Part of the ContractResolver interface. +func (c *commitSweepResolver) Decode(r io.Reader) error { + if err := decodeCommitResolution(r, &c.commitResolution); err != nil { + return err + } + + if err := binary.Read(r, endian, &c.resolved); err != nil { + return err + } + if err := binary.Read(r, endian, &c.broadcastHeight); err != nil { + return err + } + _, err := io.ReadFull(r, c.chanPoint.Hash[:]) + if err != nil { + return err + } + err = binary.Read(r, endian, &c.chanPoint.Index) + if err != nil { + return err + } + + txBytes, err := ioutil.ReadAll(r) + if err != nil { + return err + } + + if len(txBytes) == 0 { + return nil + } + + txReader := bytes.NewReader(txBytes) + tx := &wire.MsgTx{} + if err := tx.Deserialize(txReader); err != nil { + return nil + } + + c.sweepTx = tx + return nil +} + +// 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 (c *commitSweepResolver) AttachResolverKit(r ResolverKit) { + c.ResolverKit = r +} + +// A compile time assertion to ensure commitSweepResolver meets the +// ContractResolver interface. +var _ ContractResolver = (*commitSweepResolver)(nil) diff --git a/contractcourt/contract_resolvers_test.go b/contractcourt/contract_resolvers_test.go new file mode 100644 index 00000000..236a3336 --- /dev/null +++ b/contractcourt/contract_resolvers_test.go @@ -0,0 +1 @@ +package contractcourt