Merge pull request #4779 from halseth/anchor-htlc-aggregation

[anchors] HTLC second level aggregation in the sweeper
This commit is contained in:
Conner Fromknecht 2020-12-11 16:22:17 -08:00 committed by GitHub
commit 4af24158c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 3128 additions and 436 deletions

View File

@ -6,7 +6,9 @@ import (
"fmt"
"io"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/kvdb"
@ -275,6 +277,13 @@ var (
// the full set of resolutions for a channel.
resolutionsKey = []byte("resolutions")
// resolutionsSignDetailsKey is the key under the logScope where we
// will store input.SignDetails for each HTLC resolution. If this is
// not found under the logScope, it means it was written before
// SignDetails was introduced, and should be set nil for each HTLC
// resolution.
resolutionsSignDetailsKey = []byte("resolutions-sign-details")
// anchorResolutionKey is the key under the logScope that we'll use to
// store the anchor resolution, if any.
anchorResolutionKey = []byte("anchor-resolution")
@ -656,6 +665,10 @@ func (b *boltArbitratorLog) LogContractResolutions(c *ContractResolutions) error
}
}
// As we write the HTLC resolutions, we'll serialize the sign
// details for each, to store under a new key.
var signDetailsBuf bytes.Buffer
// With the output for the commitment transaction written, we
// can now write out the resolutions for the incoming and
// outgoing HTLC's.
@ -668,6 +681,11 @@ func (b *boltArbitratorLog) LogContractResolutions(c *ContractResolutions) error
if err != nil {
return err
}
err = encodeSignDetails(&signDetailsBuf, htlc.SignDetails)
if err != nil {
return err
}
}
numOutgoing := uint32(len(c.HtlcResolutions.OutgoingHTLCs))
if err := binary.Write(&b, endian, numOutgoing); err != nil {
@ -678,13 +696,28 @@ func (b *boltArbitratorLog) LogContractResolutions(c *ContractResolutions) error
if err != nil {
return err
}
err = encodeSignDetails(&signDetailsBuf, htlc.SignDetails)
if err != nil {
return err
}
}
// Put the resolutions under the resolutionsKey.
err = scopeBucket.Put(resolutionsKey, b.Bytes())
if err != nil {
return err
}
// We'll put the serialized sign details under its own key to
// stay backwards compatible.
err = scopeBucket.Put(
resolutionsSignDetailsKey, signDetailsBuf.Bytes(),
)
if err != nil {
return err
}
// Write out the anchor resolution if present.
if c.AnchorResolution != nil {
var b bytes.Buffer
@ -779,6 +812,33 @@ func (b *boltArbitratorLog) FetchContractResolutions() (*ContractResolutions, er
}
}
// Now we attempt to get the sign details for our HTLC
// resolutions. If not present the channel is of a type that
// doesn't need them. If present there will be SignDetails
// encoded for each HTLC resolution.
signDetailsBytes := scopeBucket.Get(resolutionsSignDetailsKey)
if signDetailsBytes != nil {
r := bytes.NewReader(signDetailsBytes)
// They will be encoded in the same order as the
// resolutions: firs incoming HTLCs, then outgoing.
for i := uint32(0); i < numIncoming; i++ {
htlc := &c.HtlcResolutions.IncomingHTLCs[i]
htlc.SignDetails, err = decodeSignDetails(r)
if err != nil {
return err
}
}
for i := uint32(0); i < numOutgoing; i++ {
htlc := &c.HtlcResolutions.OutgoingHTLCs[i]
htlc.SignDetails, err = decodeSignDetails(r)
if err != nil {
return err
}
}
}
anchorResBytes := scopeBucket.Get(anchorResolutionKey)
if anchorResBytes != nil {
c.AnchorResolution = &lnwallet.AnchorResolution{}
@ -941,6 +1001,11 @@ func (b *boltArbitratorLog) WipeHistory() error {
return err
}
err = scopeBucket.Delete(resolutionsSignDetailsKey)
if err != nil {
return err
}
// We'll delete any chain actions that are still stored by
// removing the enclosing bucket.
err = scopeBucket.DeleteNestedBucket(actionsBucketKey)
@ -980,6 +1045,79 @@ func (b *boltArbitratorLog) checkpointContract(c ContractResolver,
}, func() {})
}
// encodeSignDetails encodes the gived SignDetails struct to the writer.
// SignDetails is allowed to be nil, in which we will encode that it is not
// present.
func encodeSignDetails(w io.Writer, s *input.SignDetails) error {
// If we don't have sign details, write false and return.
if s == nil {
return binary.Write(w, endian, false)
}
// Otherwise write true, and the contents of the SignDetails.
if err := binary.Write(w, endian, true); err != nil {
return err
}
err := input.WriteSignDescriptor(w, &s.SignDesc)
if err != nil {
return err
}
err = binary.Write(w, endian, uint32(s.SigHashType))
if err != nil {
return err
}
// Write the DER-encoded signature.
b := s.PeerSig.Serialize()
if err := wire.WriteVarBytes(w, 0, b); err != nil {
return err
}
return nil
}
// decodeSignDetails extracts a single SignDetails from the reader. It is
// allowed to return nil in case the SignDetails were empty.
func decodeSignDetails(r io.Reader) (*input.SignDetails, error) {
var present bool
if err := binary.Read(r, endian, &present); err != nil {
return nil, err
}
// Simply return nil if the next SignDetails was not present.
if !present {
return nil, nil
}
// Otherwise decode the elements of the SignDetails.
s := input.SignDetails{}
err := input.ReadSignDescriptor(r, &s.SignDesc)
if err != nil {
return nil, err
}
var sigHash uint32
err = binary.Read(r, endian, &sigHash)
if err != nil {
return nil, err
}
s.SigHashType = txscript.SigHashType(sigHash)
// Read DER-encoded signature.
rawSig, err := wire.ReadVarBytes(r, 0, 200, "signature")
if err != nil {
return nil, err
}
sig, err := btcec.ParseDERSignature(rawSig, btcec.S256())
if err != nil {
return nil, err
}
s.PeerSig = sig
return &s, nil
}
func encodeIncomingResolution(w io.Writer, i *lnwallet.IncomingHtlcResolution) error {
if _, err := w.Write(i.Preimage[:]); err != nil {
return err

View File

@ -102,6 +102,58 @@ var (
},
HashType: txscript.SigHashAll,
}
testTx = &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: testChanPoint2,
SignatureScript: []byte{0x12, 0x34},
Witness: [][]byte{
{
0x00, 0x14, 0xee, 0x91, 0x41,
0x7e, 0x85, 0x6c, 0xde, 0x10,
0xa2, 0x91, 0x1e, 0xdc, 0xbd,
0xbd, 0x69, 0xe2, 0xef, 0xb5,
0x71, 0x48,
},
},
Sequence: 1,
},
},
TxOut: []*wire.TxOut{
{
Value: 5000000000,
PkScript: []byte{
0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2,
0x86, 0x24, 0xe1, 0x81, 0x75, 0xe8,
0x51, 0xc9, 0x6b, 0x97, 0x3d, 0x81,
0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78,
},
},
},
LockTime: 123,
}
// A valid, DER-encoded signature (taken from btcec unit tests).
testSigBytes = []byte{
0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69,
0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3,
0xa1, 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32,
0xe9, 0xd6, 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab,
0x5f, 0xb8, 0xcd, 0x41, 0x02, 0x20, 0x18, 0x15,
0x22, 0xec, 0x8e, 0xca, 0x07, 0xde, 0x48, 0x60,
0xa4, 0xac, 0xdd, 0x12, 0x90, 0x9d, 0x83, 0x1c,
0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, 0x08, 0x22,
0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09,
}
testSig, _ = btcec.ParseDERSignature(testSigBytes, btcec.S256())
testSignDetails = &input.SignDetails{
SignDesc: testSignDesc,
SigHashType: txscript.SigHashSingle,
PeerSig: testSig,
}
)
func makeTestDB() (kvdb.Backend, func(), error) {
@ -219,13 +271,13 @@ func assertResolversEqual(t *testing.T, originalResolver ContractResolver,
case *htlcOutgoingContestResolver:
diskRes := diskResolver.(*htlcOutgoingContestResolver)
assertTimeoutResEqual(
&ogRes.htlcTimeoutResolver, &diskRes.htlcTimeoutResolver,
ogRes.htlcTimeoutResolver, diskRes.htlcTimeoutResolver,
)
case *htlcIncomingContestResolver:
diskRes := diskResolver.(*htlcIncomingContestResolver)
assertSuccessResEqual(
&ogRes.htlcSuccessResolver, &diskRes.htlcSuccessResolver,
ogRes.htlcSuccessResolver, diskRes.htlcSuccessResolver,
)
if ogRes.htlcExpiry != diskRes.htlcExpiry {
@ -323,13 +375,13 @@ func TestContractInsertionRetrieval(t *testing.T) {
contestTimeout := timeoutResolver
contestTimeout.htlcResolution.ClaimOutpoint = randOutPoint()
resolvers = append(resolvers, &htlcOutgoingContestResolver{
htlcTimeoutResolver: contestTimeout,
htlcTimeoutResolver: &contestTimeout,
})
contestSuccess := successResolver
contestSuccess.htlcResolution.ClaimOutpoint = randOutPoint()
resolvers = append(resolvers, &htlcIncomingContestResolver{
htlcExpiry: 100,
htlcSuccessResolver: contestSuccess,
htlcSuccessResolver: &contestSuccess,
})
// For quick lookup during the test, we'll create this map which allow
@ -469,7 +521,7 @@ func TestContractSwapping(t *testing.T) {
// We'll create two resolvers, a regular timeout resolver, and the
// contest resolver that eventually turns into the timeout resolver.
timeoutResolver := htlcTimeoutResolver{
timeoutResolver := &htlcTimeoutResolver{
htlcResolution: lnwallet.OutgoingHtlcResolution{
Expiry: 99,
SignedTimeoutTx: nil,
@ -497,7 +549,7 @@ func TestContractSwapping(t *testing.T) {
// With the resolver inserted, we'll now attempt to atomically swap it
// for its underlying timeout resolver.
err = testLog.SwapContract(contestResolver, &timeoutResolver)
err = testLog.SwapContract(contestResolver, timeoutResolver)
if err != nil {
t.Fatalf("unable to swap contracts: %v", err)
}
@ -514,7 +566,7 @@ func TestContractSwapping(t *testing.T) {
}
// That single contract should be the underlying timeout resolver.
assertResolversEqual(t, &timeoutResolver, dbContracts[0])
assertResolversEqual(t, timeoutResolver, dbContracts[0])
}
// TestContractResolutionsStorage tests that we're able to properly store and
@ -550,8 +602,38 @@ func TestContractResolutionsStorage(t *testing.T) {
ClaimOutpoint: randOutPoint(),
SweepSignDesc: testSignDesc,
},
// We add a resolution with SignDetails.
{
Preimage: testPreimage,
SignedSuccessTx: testTx,
SignDetails: testSignDetails,
CsvDelay: 900,
ClaimOutpoint: randOutPoint(),
SweepSignDesc: testSignDesc,
},
// We add a resolution with a signed tx, but no
// SignDetails.
{
Preimage: testPreimage,
SignedSuccessTx: testTx,
CsvDelay: 900,
ClaimOutpoint: randOutPoint(),
SweepSignDesc: testSignDesc,
},
},
OutgoingHTLCs: []lnwallet.OutgoingHtlcResolution{
// We add a resolution with a signed tx, but no
// SignDetails.
{
Expiry: 103,
SignedTimeoutTx: testTx,
CsvDelay: 923923,
ClaimOutpoint: randOutPoint(),
SweepSignDesc: testSignDesc,
},
// Resolution without signed tx.
{
Expiry: 103,
SignedTimeoutTx: nil,
@ -559,6 +641,15 @@ func TestContractResolutionsStorage(t *testing.T) {
ClaimOutpoint: randOutPoint(),
SweepSignDesc: testSignDesc,
},
// Resolution with SignDetails.
{
Expiry: 103,
SignedTimeoutTx: testTx,
SignDetails: testSignDetails,
CsvDelay: 923923,
ClaimOutpoint: randOutPoint(),
SweepSignDesc: testSignDesc,
},
},
},
AnchorResolution: &lnwallet.AnchorResolution{
@ -585,8 +676,15 @@ func TestContractResolutionsStorage(t *testing.T) {
}
if !reflect.DeepEqual(&res, diskRes) {
t.Fatalf("resolution mismatch: expected %#v\n, got %#v",
&res, diskRes)
for _, h := range res.HtlcResolutions.IncomingHTLCs {
h.SweepSignDesc.KeyDesc.PubKey.Curve = nil
}
for _, h := range diskRes.HtlcResolutions.IncomingHTLCs {
h.SweepSignDesc.KeyDesc.PubKey.Curve = nil
}
t.Fatalf("resolution mismatch: expected %v\n, got %v",
spew.Sdump(&res), spew.Sdump(diskRes))
}
// We'll now delete the state, then attempt to retrieve the set of

View File

@ -10,6 +10,7 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
@ -79,10 +80,12 @@ func (c *commitSweepResolver) ResolverKey() []byte {
// waitForHeight registers for block notifications and waits for the provided
// block height to be reached.
func (c *commitSweepResolver) waitForHeight(waitHeight uint32) error {
func waitForHeight(waitHeight uint32, notifier chainntnfs.ChainNotifier,
quit <-chan struct{}) error {
// Register for block epochs. After registration, the current height
// will be sent on the channel immediately.
blockEpochs, err := c.Notifier.RegisterBlockEpochNtfn(nil)
blockEpochs, err := notifier.RegisterBlockEpochNtfn(nil)
if err != nil {
return err
}
@ -99,12 +102,38 @@ func (c *commitSweepResolver) waitForHeight(waitHeight uint32) error {
return nil
}
case <-c.quit:
case <-quit:
return errResolverShuttingDown
}
}
}
// waitForSpend waits for the given outpoint to be spent, and returns the
// details of the spending tx.
func waitForSpend(op *wire.OutPoint, pkScript []byte, heightHint uint32,
notifier chainntnfs.ChainNotifier, quit <-chan struct{}) (
*chainntnfs.SpendDetail, error) {
spendNtfn, err := notifier.RegisterSpendNtfn(
op, pkScript, heightHint,
)
if err != nil {
return nil, err
}
select {
case spendDetail, ok := <-spendNtfn.Spend:
if !ok {
return nil, errResolverShuttingDown
}
return spendDetail, nil
case <-quit:
return nil, errResolverShuttingDown
}
}
// getCommitTxConfHeight waits for confirmation of the commitment tx and returns
// the confirmation height.
func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) {
@ -169,7 +198,7 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) {
// We only need to wait for the block before the block that
// unlocks the spend path.
err := c.waitForHeight(unlockHeight - 1)
err := waitForHeight(unlockHeight-1, c.Notifier, c.quit)
if err != nil {
return nil, err
}

View File

@ -103,17 +103,19 @@ func (i *commitSweepResolverTestContext) waitForResult() {
}
type mockSweeper struct {
sweptInputs chan input.Input
updatedInputs chan wire.OutPoint
sweepTx *wire.MsgTx
sweepErr error
sweptInputs chan input.Input
updatedInputs chan wire.OutPoint
sweepTx *wire.MsgTx
sweepErr error
createSweepTxChan chan *wire.MsgTx
}
func newMockSweeper() *mockSweeper {
return &mockSweeper{
sweptInputs: make(chan input.Input),
updatedInputs: make(chan wire.OutPoint),
sweepTx: &wire.MsgTx{},
sweptInputs: make(chan input.Input),
updatedInputs: make(chan wire.OutPoint),
sweepTx: &wire.MsgTx{},
createSweepTxChan: make(chan *wire.MsgTx),
}
}
@ -133,7 +135,9 @@ func (s *mockSweeper) SweepInput(input input.Input, params sweep.Params) (
func (s *mockSweeper) CreateSweepTx(inputs []input.Input, feePref sweep.FeePreference,
currentBlockHeight uint32) (*wire.MsgTx, error) {
return nil, nil
// We will wait for the test to supply the sweep tx to return.
sweepTx := <-s.createSweepTxChan
return sweepTx, nil
}
func (s *mockSweeper) RelayFeePerKW() chainfee.SatPerKWeight {

View File

@ -20,6 +20,10 @@ const (
// sweepConfTarget is the default number of blocks that we'll use as a
// confirmation target when sweeping.
sweepConfTarget = 6
// secondLevelConfTarget is the confirmation target we'll use when
// adding fees to our second-level HTLC transactions.
secondLevelConfTarget = 6
)
// ContractResolver is an interface which packages a state machine which is

View File

@ -31,7 +31,7 @@ type htlcIncomingContestResolver struct {
// htlcSuccessResolver is the inner resolver that may be utilized if we
// learn of the preimage.
htlcSuccessResolver
*htlcSuccessResolver
}
// newIncomingContestResolver instantiates a new incoming htlc contest resolver.
@ -45,7 +45,7 @@ func newIncomingContestResolver(
return &htlcIncomingContestResolver{
htlcExpiry: htlc.RefundTimeout,
htlcSuccessResolver: *success,
htlcSuccessResolver: success,
}
}
@ -189,7 +189,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
return nil, err
}
return &h.htlcSuccessResolver, nil
return h.htlcSuccessResolver, nil
// If the htlc was failed, mark the htlc as
// resolved.
@ -293,7 +293,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
return nil, err
}
return &h.htlcSuccessResolver, nil
return h.htlcSuccessResolver, nil
}
witnessUpdates = preimageSubscription.WitnessUpdates
@ -315,7 +315,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
// We've learned of the preimage and this information
// has been added to our inner resolver. We return it so
// it can continue contract resolution.
return &h.htlcSuccessResolver, nil
return h.htlcSuccessResolver, nil
case hodlItem := <-hodlChan:
htlcResolution := hodlItem.(invoices.HtlcResolution)
@ -420,7 +420,7 @@ func newIncomingContestResolverFromReader(r io.Reader, resCfg ResolverConfig) (
if err != nil {
return nil, err
}
h.htlcSuccessResolver = *successResolver
h.htlcSuccessResolver = successResolver
return h, nil
}

View File

@ -342,7 +342,7 @@ func newIncomingResolverTestContext(t *testing.T, isExit bool) *incomingResolver
},
}
resolver := &htlcIncomingContestResolver{
htlcSuccessResolver: htlcSuccessResolver{
htlcSuccessResolver: &htlcSuccessResolver{
contractResolverKit: *newContractResolverKit(cfg),
htlcResolution: lnwallet.IncomingHtlcResolution{},
htlc: channeldb.HTLC{

View File

@ -17,7 +17,7 @@ import (
type htlcOutgoingContestResolver struct {
// htlcTimeoutResolver is the inner solver that this resolver may turn
// into. This only happens if the HTLC expires on-chain.
htlcTimeoutResolver
*htlcTimeoutResolver
}
// newOutgoingContestResolver instantiates a new outgoing contested htlc
@ -31,7 +31,7 @@ func newOutgoingContestResolver(res lnwallet.OutgoingHtlcResolution,
)
return &htlcOutgoingContestResolver{
htlcTimeoutResolver: *timeout,
htlcTimeoutResolver: timeout,
}
}
@ -131,7 +131,7 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
"into timeout resolver", h,
h.htlcResolution.ClaimOutpoint,
newHeight, h.htlcResolution.Expiry)
return &h.htlcTimeoutResolver, nil
return h.htlcTimeoutResolver, nil
}
// The output has been spent! This means the preimage has been
@ -209,7 +209,7 @@ func newOutgoingContestResolverFromReader(r io.Reader, resCfg ResolverConfig) (
if err != nil {
return nil, err
}
h.htlcTimeoutResolver = *timeoutResolver
h.htlcTimeoutResolver = timeoutResolver
return h, nil
}

View File

@ -177,7 +177,7 @@ func newOutgoingResolverTestContext(t *testing.T) *outgoingResolverTestContext {
}
resolver := &htlcOutgoingContestResolver{
htlcTimeoutResolver: htlcTimeoutResolver{
htlcTimeoutResolver: &htlcTimeoutResolver{
contractResolverKit: *newContractResolverKit(cfg),
htlcResolution: outgoingRes,
htlc: channeldb.HTLC{

View File

@ -3,11 +3,13 @@ package contractcourt
import (
"encoding/binary"
"io"
"sync"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"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/input"
"github.com/lightningnetwork/lnd/labels"
@ -29,7 +31,12 @@ type htlcSuccessResolver struct {
htlcResolution lnwallet.IncomingHtlcResolution
// outputIncubating returns true if we've sent the output to the output
// incubator (utxo nursery).
// incubator (utxo nursery). In case the htlcResolution has non-nil
// SignDetails, it means we will let the Sweeper handle broadcasting
// the secondd-level transaction, and sweeping its output. In this case
// we let this field indicate whether we need to broadcast the
// second-level tx (false) or if it has confirmed and we must sweep the
// second-level output (true).
outputIncubating bool
// resolved reflects if the contract has been fully resolved or not.
@ -50,6 +57,15 @@ type htlcSuccessResolver struct {
// htlc contains information on the htlc that we are resolving on-chain.
htlc channeldb.HTLC
// currentReport stores the current state of the resolver for reporting
// over the rpc interface. This should only be reported in case we have
// a non-nil SignDetails on the htlcResolution, otherwise the nursery
// will produce reports.
currentReport ContractReport
// reportLock prevents concurrent access to the resolver report.
reportLock sync.Mutex
contractResolverKit
}
@ -58,12 +74,16 @@ func newSuccessResolver(res lnwallet.IncomingHtlcResolution,
broadcastHeight uint32, htlc channeldb.HTLC,
resCfg ResolverConfig) *htlcSuccessResolver {
return &htlcSuccessResolver{
h := &htlcSuccessResolver{
contractResolverKit: *newContractResolverKit(resCfg),
htlcResolution: res,
broadcastHeight: broadcastHeight,
htlc: htlc,
}
h.initReport()
return h
}
// ResolverKey returns an identifier which should be globally unique for this
@ -105,104 +125,58 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
// 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.htlc.RHash[:])
// Before we can craft out sweeping transaction, we
// need to create an input which contains all the items
// required to add this input to a sweeping transaction,
// and generate a witness.
inp := input.MakeHtlcSucceedInput(
&h.htlcResolution.ClaimOutpoint,
&h.htlcResolution.SweepSignDesc,
h.htlcResolution.Preimage[:],
h.broadcastHeight,
h.htlcResolution.CsvDelay,
)
// With the input created, we can now generate the full
// sweep transaction, that we'll use to move these
// coins back into the backing wallet.
//
// TODO: Set tx lock time to current block height
// instead of zero. Will be taken care of once sweeper
// implementation is complete.
//
// TODO: Use time-based sweeper and result chan.
var err error
h.sweepTx, err = h.Sweeper.CreateSweepTx(
[]input.Input{&inp},
sweep.FeePreference{
ConfTarget: sweepConfTarget,
}, 0,
)
if err != nil {
return nil, err
}
log.Infof("%T(%x): crafted sweep tx=%v", h,
h.htlc.RHash[:], spew.Sdump(h.sweepTx))
// With the sweep transaction signed, we'll now
// Checkpoint our state.
if err := h.Checkpoint(h); err != nil {
log.Errorf("unable to Checkpoint: %v", err)
return nil, err
}
}
// Regardless of whether an existing transaction was found or newly
// constructed, we'll broadcast the sweep transaction to the
// network.
label := labels.MakeLabel(
labels.LabelTypeChannelClose, &h.ShortChanID,
)
err := h.PublishTx(h.sweepTx, label)
if err != nil {
log.Infof("%T(%x): unable to publish tx: %v",
h, h.htlc.RHash[:], err)
return nil, err
}
// With the sweep transaction broadcast, we'll wait for its
// confirmation.
sweepTXID := h.sweepTx.TxHash()
sweepScript := h.sweepTx.TxOut[0].PkScript
confNtfn, err := h.Notifier.RegisterConfirmationsNtfn(
&sweepTXID, sweepScript, 1, h.broadcastHeight,
)
if err != nil {
return nil, err
}
log.Infof("%T(%x): waiting for sweep tx (txid=%v) to be "+
"confirmed", h, h.htlc.RHash[:], sweepTXID)
select {
case _, ok := <-confNtfn.Confirmed:
if !ok {
return nil, errResolverShuttingDown
}
case <-h.quit:
return nil, errResolverShuttingDown
}
// Once the transaction has received a sufficient number of
// confirmations, we'll mark ourselves as fully resolved and exit.
h.resolved = true
// Checkpoint the resolver, and write the outcome to disk.
return nil, h.checkpointClaim(
&sweepTXID,
channeldb.ResolverOutcomeClaimed,
)
return h.resolveRemoteCommitOutput()
}
// Otherwise this an output on our own commitment, and we must start by
// broadcasting the second-level success transaction.
secondLevelOutpoint, err := h.broadcastSuccessTx()
if err != nil {
return nil, err
}
// To wrap this up, we'll wait until the second-level transaction has
// been spent, then fully resolve the contract.
log.Infof("%T(%x): waiting for second-level HTLC output to be spent "+
"after csv_delay=%v", h, h.htlc.RHash[:], h.htlcResolution.CsvDelay)
spend, err := waitForSpend(
secondLevelOutpoint,
h.htlcResolution.SweepSignDesc.Output.PkScript,
h.broadcastHeight, h.Notifier, h.quit,
)
if err != nil {
return nil, err
}
h.reportLock.Lock()
h.currentReport.RecoveredBalance = h.currentReport.LimboBalance
h.currentReport.LimboBalance = 0
h.reportLock.Unlock()
h.resolved = true
return nil, h.checkpointClaim(
spend.SpenderTxHash, channeldb.ResolverOutcomeClaimed,
)
}
// broadcastSuccessTx handles an HTLC output on our local commitment by
// broadcasting the second-level success transaction. It returns the ultimate
// outpoint of the second-level tx, that we must wait to be spent for the
// resolver to be fully resolved.
func (h *htlcSuccessResolver) broadcastSuccessTx() (*wire.OutPoint, error) {
// If we have non-nil SignDetails, this means that have a 2nd level
// HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY
// (the case for anchor type channels). In this case we can re-sign it
// and attach fees at will. We let the sweeper handle this job.
// We use the checkpointed outputIncubating field to determine if we
// already swept the HTLC output into the second level transaction.
if h.htlcResolution.SignDetails != nil {
return h.broadcastReSignedSuccessTx()
}
// Otherwise we'll publish the second-level transaction directly and
// offer the resolution to the nursery to handle.
log.Infof("%T(%x): broadcasting second-layer transition tx: %v",
h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx))
@ -241,35 +215,236 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
}
}
// 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.htlcResolution.SweepSignDesc.Output.PkScript,
h.broadcastHeight,
return &h.htlcResolution.ClaimOutpoint, nil
}
// broadcastReSignedSuccessTx handles the case where we have non-nil
// SignDetails, and offers the second level transaction to the Sweeper, that
// will re-sign it and attach fees at will.
func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() (
*wire.OutPoint, error) {
// Keep track of the tx spending the HTLC output on the commitment, as
// this will be the confirmed second-level tx we'll ultimately sweep.
var commitSpend *chainntnfs.SpendDetail
// We will have to let the sweeper re-sign the success tx and wait for
// it to confirm, if we haven't already.
if !h.outputIncubating {
log.Infof("%T(%x): offering second-layer transition tx to "+
"sweeper: %v", h, h.htlc.RHash[:],
spew.Sdump(h.htlcResolution.SignedSuccessTx))
secondLevelInput := input.MakeHtlcSecondLevelSuccessAnchorInput(
h.htlcResolution.SignedSuccessTx,
h.htlcResolution.SignDetails, h.htlcResolution.Preimage,
h.broadcastHeight,
)
_, err := h.Sweeper.SweepInput(
&secondLevelInput,
sweep.Params{
Fee: sweep.FeePreference{
ConfTarget: secondLevelConfTarget,
},
},
)
if err != nil {
return nil, err
}
log.Infof("%T(%x): waiting for second-level HTLC success "+
"transaction to confirm", h, h.htlc.RHash[:])
// Wait for the second level transaction to confirm.
commitSpend, err = waitForSpend(
&h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint,
h.htlcResolution.SignDetails.SignDesc.Output.PkScript,
h.broadcastHeight, h.Notifier, h.quit,
)
if err != nil {
return nil, err
}
// Now that the second-level transaction has confirmed, we
// checkpoint the state so we'll go to the next stage in case
// of restarts.
h.outputIncubating = true
if err := h.Checkpoint(h); err != nil {
log.Errorf("unable to Checkpoint: %v", err)
return nil, err
}
log.Infof("%T(%x): second-level HTLC success transaction "+
"confirmed!", h, h.htlc.RHash[:])
}
// If we ended up here after a restart, we must again get the
// spend notification.
if commitSpend == nil {
var err error
commitSpend, err = waitForSpend(
&h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint,
h.htlcResolution.SignDetails.SignDesc.Output.PkScript,
h.broadcastHeight, h.Notifier, h.quit,
)
if err != nil {
return nil, err
}
}
// The HTLC success tx has a CSV lock that we must wait for.
waitHeight := uint32(commitSpend.SpendingHeight) +
h.htlcResolution.CsvDelay - 1
// Now that the sweeper has broadcasted the second-level transaction,
// it has confirmed, and we have checkpointed our state, we'll sweep
// the second level output. We report the resolver has moved the next
// stage.
h.reportLock.Lock()
h.currentReport.Stage = 2
h.currentReport.MaturityHeight = waitHeight
h.reportLock.Unlock()
log.Infof("%T(%x): waiting for CSV lock to expire at height %v",
h, h.htlc.RHash[:], waitHeight)
err := waitForHeight(waitHeight, h.Notifier, h.quit)
if err != nil {
return nil, err
}
// We'll use this input index to determine the second-level output
// index on the transaction, as the signatures requires the indexes to
// be the same. We don't look for the second-level output script
// directly, as there might be more than one HTLC output to the same
// pkScript.
op := &wire.OutPoint{
Hash: *commitSpend.SpenderTxHash,
Index: commitSpend.SpenderInputIndex,
}
// Finally, let the sweeper sweep the second-level output.
log.Infof("%T(%x): CSV lock expired, offering second-layer "+
"output to sweeper: %v", h, h.htlc.RHash[:], op)
inp := input.NewCsvInput(
op, input.HtlcAcceptedSuccessSecondLevel,
&h.htlcResolution.SweepSignDesc, h.broadcastHeight,
h.htlcResolution.CsvDelay,
)
_, err = h.Sweeper.SweepInput(
inp,
sweep.Params{
Fee: sweep.FeePreference{
ConfTarget: sweepConfTarget,
},
},
)
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.htlc.RHash[:], h.htlcResolution.CsvDelay)
// Will return this outpoint, when this is spent the resolver is fully
// resolved.
return op, nil
}
// resolveRemoteCommitOutput handles sweeping an HTLC output on the remote
// commitment with the preimage. In this case we can sweep the output directly,
// and don't have to broadcast a second-level transaction.
func (h *htlcSuccessResolver) resolveRemoteCommitOutput() (
ContractResolver, error) {
// 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.htlc.RHash[:])
// Before we can craft out sweeping transaction, we need to
// create an input which contains all the items required to add
// this input to a sweeping transaction, and generate a
// witness.
inp := input.MakeHtlcSucceedInput(
&h.htlcResolution.ClaimOutpoint,
&h.htlcResolution.SweepSignDesc,
h.htlcResolution.Preimage[:],
h.broadcastHeight,
h.htlcResolution.CsvDelay,
)
// With the input created, we can now generate the full sweep
// transaction, that we'll use to move these coins back into
// the backing wallet.
//
// TODO: Set tx lock time to current block height instead of
// zero. Will be taken care of once sweeper implementation is
// complete.
//
// TODO: Use time-based sweeper and result chan.
var err error
h.sweepTx, err = h.Sweeper.CreateSweepTx(
[]input.Input{&inp},
sweep.FeePreference{
ConfTarget: sweepConfTarget,
}, 0,
)
if err != nil {
return nil, err
}
log.Infof("%T(%x): crafted sweep tx=%v", h,
h.htlc.RHash[:], spew.Sdump(h.sweepTx))
// TODO(halseth): should checkpoint sweep tx to DB? Since after
// a restart we might create a different tx, that will conflict
// with the published one.
}
// Regardless of whether an existing transaction was found or newly
// constructed, we'll broadcast the sweep transaction to the network.
label := labels.MakeLabel(
labels.LabelTypeChannelClose, &h.ShortChanID,
)
err := h.PublishTx(h.sweepTx, label)
if err != nil {
log.Infof("%T(%x): unable to publish tx: %v",
h, h.htlc.RHash[:], err)
return nil, err
}
// With the sweep transaction broadcast, we'll wait for its
// confirmation.
sweepTXID := h.sweepTx.TxHash()
sweepScript := h.sweepTx.TxOut[0].PkScript
confNtfn, err := h.Notifier.RegisterConfirmationsNtfn(
&sweepTXID, sweepScript, 1, h.broadcastHeight,
)
if err != nil {
return nil, err
}
log.Infof("%T(%x): waiting for sweep tx (txid=%v) to be "+
"confirmed", h, h.htlc.RHash[:], sweepTXID)
var spendTxid *chainhash.Hash
select {
case spend, ok := <-spendNtfn.Spend:
case _, ok := <-confNtfn.Confirmed:
if !ok {
return nil, errResolverShuttingDown
}
spendTxid = spend.SpenderTxHash
case <-h.quit:
return nil, errResolverShuttingDown
}
// Once the transaction has received a sufficient number of
// confirmations, we'll mark ourselves as fully resolved and exit.
h.resolved = true
// Checkpoint the resolver, and write the outcome to disk.
return nil, h.checkpointClaim(
spendTxid, channeldb.ResolverOutcomeClaimed,
&sweepTXID,
channeldb.ResolverOutcomeClaimed,
)
}
@ -330,6 +505,40 @@ func (h *htlcSuccessResolver) IsResolved() bool {
return h.resolved
}
// report returns a report on the resolution state of the contract.
func (h *htlcSuccessResolver) report() *ContractReport {
// If the sign details are nil, the report will be created by handled
// by the nursery.
if h.htlcResolution.SignDetails == nil {
return nil
}
h.reportLock.Lock()
defer h.reportLock.Unlock()
copy := h.currentReport
return &copy
}
func (h *htlcSuccessResolver) initReport() {
// We create the initial report. This will only be reported for
// resolvers not handled by the nursery.
finalAmt := h.htlc.Amt.ToSatoshis()
if h.htlcResolution.SignedSuccessTx != nil {
finalAmt = btcutil.Amount(
h.htlcResolution.SignedSuccessTx.TxOut[0].Value,
)
}
h.currentReport = ContractReport{
Outpoint: h.htlcResolution.ClaimOutpoint,
Type: ReportOutputIncomingHtlc,
Amount: finalAmt,
MaturityHeight: h.htlcResolution.CsvDelay,
LimboBalance: finalAmt,
Stage: 1,
}
}
// Encode writes an encoded version of the ContractResolver into the passed
// Writer.
//
@ -355,6 +564,12 @@ func (h *htlcSuccessResolver) Encode(w io.Writer) error {
return err
}
// We encode the sign details last for backwards compatibility.
err := encodeSignDetails(w, h.htlcResolution.SignDetails)
if err != nil {
return err
}
return nil
}
@ -388,6 +603,18 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) (
return nil, err
}
// Sign details is a new field that was added to the htlc resolution,
// so it is serialized last for backwards compatibility. We try to read
// it, but don't error out if there are not bytes left.
signDetails, err := decodeSignDetails(r)
if err == nil {
h.htlcResolution.SignDetails = signDetails
} else if err != io.EOF && err != io.ErrUnexpectedEOF {
return nil, err
}
h.initReport()
return h, nil
}

View File

@ -1,13 +1,19 @@
package contractcourt
import (
"bytes"
"fmt"
"reflect"
"testing"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"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/channeldb/kvdb"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
@ -15,33 +21,59 @@ import (
var testHtlcAmt = lnwire.MilliSatoshi(200000)
type htlcSuccessResolverTestContext struct {
resolver *htlcSuccessResolver
type htlcResolverTestContext struct {
resolver ContractResolver
checkpoint func(_ ContractResolver,
_ ...*channeldb.ResolverReport) error
notifier *mock.ChainNotifier
resolverResultChan chan resolveResult
t *testing.T
resolutionChan chan ResolutionMsg
t *testing.T
}
func newHtlcSuccessResolverTextContext(t *testing.T) *htlcSuccessResolverTestContext {
func newHtlcResolverTestContext(t *testing.T,
newResolver func(htlc channeldb.HTLC,
cfg ResolverConfig) ContractResolver) *htlcResolverTestContext {
notifier := &mock.ChainNotifier{
EpochChan: make(chan *chainntnfs.BlockEpoch),
SpendChan: make(chan *chainntnfs.SpendDetail),
ConfChan: make(chan *chainntnfs.TxConfirmation),
EpochChan: make(chan *chainntnfs.BlockEpoch, 1),
SpendChan: make(chan *chainntnfs.SpendDetail, 1),
ConfChan: make(chan *chainntnfs.TxConfirmation, 1),
}
checkPointChan := make(chan struct{}, 1)
testCtx := &htlcSuccessResolverTestContext{
notifier: notifier,
t: t,
testCtx := &htlcResolverTestContext{
checkpoint: nil,
notifier: notifier,
resolutionChan: make(chan ResolutionMsg, 1),
t: t,
}
witnessBeacon := newMockWitnessBeacon()
chainCfg := ChannelArbitratorConfig{
ChainArbitratorConfig: ChainArbitratorConfig{
Notifier: notifier,
Notifier: notifier,
PreimageDB: witnessBeacon,
PublishTx: func(_ *wire.MsgTx, _ string) error {
return nil
},
Sweeper: newMockSweeper(),
IncubateOutputs: func(wire.OutPoint, *lnwallet.OutgoingHtlcResolution,
*lnwallet.IncomingHtlcResolution, uint32) error {
return nil
},
DeliverResolutionMsg: func(msgs ...ResolutionMsg) error {
if len(msgs) != 1 {
return fmt.Errorf("expected 1 "+
"resolution msg, instead got %v",
len(msgs))
}
testCtx.resolutionChan <- msgs[0]
return nil
},
},
PutResolverReport: func(_ kvdb.RwTx,
report *channeldb.ResolverReport) error {
@ -49,31 +81,31 @@ func newHtlcSuccessResolverTextContext(t *testing.T) *htlcSuccessResolverTestCon
return nil
},
}
// Since we want to replace this checkpoint method later in the test,
// we wrap the call to it in a closure. The linter will complain about
// this so set nolint directive.
checkpointFunc := func(c ContractResolver, // nolint
r ...*channeldb.ResolverReport) error {
return testCtx.checkpoint(c, r...)
}
cfg := ResolverConfig{
ChannelArbitratorConfig: chainCfg,
Checkpoint: func(_ ContractResolver,
_ ...*channeldb.ResolverReport) error {
checkPointChan <- struct{}{}
return nil
},
Checkpoint: checkpointFunc,
}
testCtx.resolver = &htlcSuccessResolver{
contractResolverKit: *newContractResolverKit(cfg),
htlcResolution: lnwallet.IncomingHtlcResolution{},
htlc: channeldb.HTLC{
RHash: testResHash,
OnionBlob: testOnionBlob,
Amt: testHtlcAmt,
},
htlc := channeldb.HTLC{
RHash: testResHash,
OnionBlob: testOnionBlob,
Amt: testHtlcAmt,
}
testCtx.resolver = newResolver(htlc, cfg)
return testCtx
}
func (i *htlcSuccessResolverTestContext) resolve() {
func (i *htlcResolverTestContext) resolve() {
// Start resolver.
i.resolverResultChan = make(chan resolveResult, 1)
go func() {
@ -85,7 +117,7 @@ func (i *htlcSuccessResolverTestContext) resolve() {
}()
}
func (i *htlcSuccessResolverTestContext) waitForResult() {
func (i *htlcResolverTestContext) waitForResult() {
i.t.Helper()
result := <-i.resolverResultChan
@ -98,8 +130,9 @@ func (i *htlcSuccessResolverTestContext) waitForResult() {
}
}
// TestSingleStageSuccess tests successful sweep of a single stage htlc claim.
func TestSingleStageSuccess(t *testing.T) {
// TestHtlcSuccessSingleStage tests successful sweep of a single stage htlc
// claim.
func TestHtlcSuccessSingleStage(t *testing.T) {
htlcOutpoint := wire.OutPoint{Index: 3}
sweepTx := &wire.MsgTx{
@ -114,15 +147,6 @@ func TestSingleStageSuccess(t *testing.T) {
ClaimOutpoint: htlcOutpoint,
}
// We send a confirmation for our sweep tx to indicate that our sweep
// succeeded.
resolve := func(ctx *htlcSuccessResolverTestContext) {
ctx.notifier.ConfChan <- &chainntnfs.TxConfirmation{
Tx: ctx.resolver.sweepTx,
BlockHeight: testInitialBlockHeight - 1,
}
}
sweepTxid := sweepTx.TxHash()
claim := &channeldb.ResolverReport{
OutPoint: htlcOutpoint,
@ -131,14 +155,46 @@ func TestSingleStageSuccess(t *testing.T) {
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
SpendTxID: &sweepTxid,
}
checkpoints := []checkpoint{
{
// We send a confirmation for our sweep tx to indicate
// that our sweep succeeded.
preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error {
// The resolver will create and publish a sweep
// tx.
resolver := ctx.resolver.(*htlcSuccessResolver)
resolver.Sweeper.(*mockSweeper).
createSweepTxChan <- sweepTx
// Confirm the sweep, which should resolve it.
ctx.notifier.ConfChan <- &chainntnfs.TxConfirmation{
Tx: sweepTx,
BlockHeight: testInitialBlockHeight - 1,
}
return nil
},
// After the sweep has confirmed, we expect the
// checkpoint to be resolved, and with the above
// report.
resolved: true,
reports: []*channeldb.ResolverReport{
claim,
},
},
}
testHtlcSuccess(
t, singleStageResolution, resolve, sweepTx, claim,
t, singleStageResolution, checkpoints,
)
}
// TestSecondStageResolution tests successful sweep of a second stage htlc
// claim.
func TestSecondStageResolution(t *testing.T) {
// claim, going through the Nursery.
func TestHtlcSuccessSecondStageResolution(t *testing.T) {
commitOutpoint := wire.OutPoint{Index: 2}
htlcOutpoint := wire.OutPoint{Index: 3}
@ -158,20 +214,17 @@ func TestSecondStageResolution(t *testing.T) {
PreviousOutPoint: commitOutpoint,
},
},
TxOut: []*wire.TxOut{},
TxOut: []*wire.TxOut{
{
Value: 111,
PkScript: []byte{0xaa, 0xaa},
},
},
},
ClaimOutpoint: htlcOutpoint,
SweepSignDesc: testSignDesc,
}
// We send a spend notification for our output to resolve our htlc.
resolve := func(ctx *htlcSuccessResolverTestContext) {
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: sweepTx,
SpenderTxHash: &sweepHash,
}
}
successTx := twoStageResolution.SignedSuccessTx.TxHash()
firstStage := &channeldb.ResolverReport{
OutPoint: commitOutpoint,
@ -189,54 +242,373 @@ func TestSecondStageResolution(t *testing.T) {
SpendTxID: &sweepHash,
}
checkpoints := []checkpoint{
{
// The resolver will send the output to the Nursery.
incubating: true,
},
{
// It will then wait for the Nursery to spend the
// output. We send a spend notification for our output
// to resolve our htlc.
preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error {
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: sweepTx,
SpenderTxHash: &sweepHash,
}
return nil
},
incubating: true,
resolved: true,
reports: []*channeldb.ResolverReport{
secondStage,
firstStage,
},
},
}
testHtlcSuccess(
t, twoStageResolution, resolve, sweepTx, secondStage, firstStage,
t, twoStageResolution, checkpoints,
)
}
// testHtlcSuccess tests resolution of a success resolver. It takes a resolve
// function which triggers resolution and the sweeptxid that will resolve it.
// TestHtlcSuccessSecondStageResolutionSweeper test that a resolver with
// non-nil SignDetails will offer the second-level transaction to the sweeper
// for re-signing.
func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) {
commitOutpoint := wire.OutPoint{Index: 2}
htlcOutpoint := wire.OutPoint{Index: 3}
successTx := &wire.MsgTx{
TxIn: []*wire.TxIn{
{
PreviousOutPoint: commitOutpoint,
},
},
TxOut: []*wire.TxOut{
{
Value: 123,
PkScript: []byte{0xff, 0xff},
},
},
}
reSignedSuccessTx := &wire.MsgTx{
TxIn: []*wire.TxIn{
{
PreviousOutPoint: wire.OutPoint{
Hash: chainhash.Hash{0xaa, 0xbb},
Index: 0,
},
},
successTx.TxIn[0],
{
PreviousOutPoint: wire.OutPoint{
Hash: chainhash.Hash{0xaa, 0xbb},
Index: 2,
},
},
},
TxOut: []*wire.TxOut{
{
Value: 111,
PkScript: []byte{0xaa, 0xaa},
},
successTx.TxOut[0],
},
}
reSignedHash := successTx.TxHash()
sweepTx := &wire.MsgTx{
TxIn: []*wire.TxIn{
{
PreviousOutPoint: wire.OutPoint{
Hash: reSignedHash,
Index: 1,
},
},
},
TxOut: []*wire.TxOut{{}},
}
sweepHash := sweepTx.TxHash()
// twoStageResolution is a resolution for htlc on our own commitment
// which is spent from the signed success tx.
twoStageResolution := lnwallet.IncomingHtlcResolution{
Preimage: [32]byte{},
CsvDelay: 4,
SignedSuccessTx: successTx,
SignDetails: &input.SignDetails{
SignDesc: testSignDesc,
PeerSig: testSig,
},
ClaimOutpoint: htlcOutpoint,
SweepSignDesc: testSignDesc,
}
firstStage := &channeldb.ResolverReport{
OutPoint: commitOutpoint,
Amount: testHtlcAmt.ToSatoshis(),
ResolverType: channeldb.ResolverTypeIncomingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
SpendTxID: &reSignedHash,
}
secondStage := &channeldb.ResolverReport{
OutPoint: htlcOutpoint,
Amount: btcutil.Amount(testSignDesc.Output.Value),
ResolverType: channeldb.ResolverTypeIncomingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
SpendTxID: &sweepHash,
}
checkpoints := []checkpoint{
{
// The HTLC output on the commitment should be offered
// to the sweeper. We'll notify that it gets spent.
preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error {
resolver := ctx.resolver.(*htlcSuccessResolver)
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
op := inp.OutPoint()
if *op != commitOutpoint {
return fmt.Errorf("outpoint %v swept, "+
"expected %v", op,
commitOutpoint)
}
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: reSignedSuccessTx,
SpenderTxHash: &reSignedHash,
SpenderInputIndex: 1,
SpendingHeight: 10,
}
return nil
},
// incubating=true is used to signal that the
// second-level transaction was confirmed.
incubating: true,
},
{
// The resolver will wait for the second-level's CSV
// lock to expire.
preCheckpoint: func(ctx *htlcResolverTestContext,
resumed bool) error {
// If we are resuming from a checkpoint, we
// expect the resolver to re-subscribe to a
// spend, hence we must resend it.
if resumed {
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: reSignedSuccessTx,
SpenderTxHash: &reSignedHash,
SpenderInputIndex: 1,
SpendingHeight: 10,
}
}
ctx.notifier.EpochChan <- &chainntnfs.BlockEpoch{
Height: 13,
}
// We expect it to sweep the second-level
// transaction we notfied about above.
resolver := ctx.resolver.(*htlcSuccessResolver)
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
op := inp.OutPoint()
exp := wire.OutPoint{
Hash: reSignedHash,
Index: 1,
}
if *op != exp {
return fmt.Errorf("swept outpoint %v, expected %v",
op, exp)
}
// Notify about the spend, which should resolve
// the resolver.
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: sweepTx,
SpenderTxHash: &sweepHash,
SpendingHeight: 14,
}
return nil
},
incubating: true,
resolved: true,
reports: []*channeldb.ResolverReport{
secondStage,
firstStage,
},
},
}
testHtlcSuccess(t, twoStageResolution, checkpoints)
}
// checkpoint holds expected data we expect the resolver to checkpoint itself
// to the DB next.
type checkpoint struct {
// preCheckpoint is a method that will be called before we reach the
// checkpoint, to carry out any needed operations to drive the resolver
// in this stage.
preCheckpoint func(*htlcResolverTestContext, bool) error
// data we expect the resolver to be checkpointed with next.
incubating bool
resolved bool
reports []*channeldb.ResolverReport
}
// testHtlcSuccess tests resolution of a success resolver. It takes a a list of
// checkpoints that it expects the resolver to go through. And will run the
// resolver all the way through these checkpoints, and also attempt to resume
// the resolver from every checkpoint.
func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution,
resolve func(*htlcSuccessResolverTestContext),
sweepTx *wire.MsgTx, reports ...*channeldb.ResolverReport) {
checkpoints []checkpoint) {
defer timeout(t)()
ctx := newHtlcSuccessResolverTextContext(t)
// We first run the resolver from start to finish, ensuring it gets
// checkpointed at every expected stage. We store the checkpointed data
// for the next portion of the test.
ctx := newHtlcResolverTestContext(t,
func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver {
return &htlcSuccessResolver{
contractResolverKit: *newContractResolverKit(cfg),
htlc: htlc,
htlcResolution: resolution,
}
},
)
// Replace our checkpoint with one which will push reports into a
// channel for us to consume. We replace this function on the resolver
// itself because it is created by the test context.
reportChan := make(chan *channeldb.ResolverReport)
ctx.resolver.Checkpoint = func(_ ContractResolver,
checkpointedState := runFromCheckpoint(t, ctx, checkpoints)
// Now, from every checkpoint created, we re-create the resolver, and
// run the test from that checkpoint.
for i := range checkpointedState {
cp := bytes.NewReader(checkpointedState[i])
ctx := newHtlcResolverTestContext(t,
func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver {
resolver, err := newSuccessResolverFromReader(cp, cfg)
if err != nil {
t.Fatal(err)
}
resolver.Supplement(htlc)
resolver.htlcResolution = resolution
return resolver
},
)
// Run from the given checkpoint, ensuring we'll hit the rest.
_ = runFromCheckpoint(t, ctx, checkpoints[i+1:])
}
}
// runFromCheckpoint executes the Resolve method on the success resolver, and
// asserts that it checkpoints itself according to the expected checkpoints.
func runFromCheckpoint(t *testing.T, ctx *htlcResolverTestContext,
expectedCheckpoints []checkpoint) [][]byte {
defer timeout(t)()
var checkpointedState [][]byte
// Replace our checkpoint method with one which we'll use to assert the
// checkpointed state and reports are equal to what we expect.
nextCheckpoint := 0
checkpointChan := make(chan struct{})
ctx.checkpoint = func(resolver ContractResolver,
reports ...*channeldb.ResolverReport) error {
// Send all of our reports into the channel.
for _, report := range reports {
reportChan <- report
if nextCheckpoint >= len(expectedCheckpoints) {
t.Fatal("did not expect more checkpoints")
}
var resolved, incubating bool
if h, ok := resolver.(*htlcSuccessResolver); ok {
resolved = h.resolved
incubating = h.outputIncubating
}
if h, ok := resolver.(*htlcTimeoutResolver); ok {
resolved = h.resolved
incubating = h.outputIncubating
}
cp := expectedCheckpoints[nextCheckpoint]
if resolved != cp.resolved {
t.Fatalf("expected checkpoint to be resolve=%v, had %v",
cp.resolved, resolved)
}
if !reflect.DeepEqual(incubating, cp.incubating) {
t.Fatalf("expected checkpoint to be have "+
"incubating=%v, had %v", cp.incubating,
incubating)
}
// Check we go the expected reports.
if len(reports) != len(cp.reports) {
t.Fatalf("unexpected number of reports. Expected %v "+
"got %v", len(cp.reports), len(reports))
}
for i, report := range reports {
if !reflect.DeepEqual(report, cp.reports[i]) {
t.Fatalf("expected: %v, got: %v",
spew.Sdump(cp.reports[i]),
spew.Sdump(report))
}
}
// Finally encode the resolver, and store it for later use.
b := bytes.Buffer{}
if err := resolver.Encode(&b); err != nil {
t.Fatal(err)
}
checkpointedState = append(checkpointedState, b.Bytes())
nextCheckpoint++
checkpointChan <- struct{}{}
return nil
}
ctx.resolver.htlcResolution = resolution
// We set the sweepTx to be non-nil and mark the output as already
// incubating so that we do not need to set test values for crafting
// our own sweep transaction.
ctx.resolver.sweepTx = sweepTx
ctx.resolver.outputIncubating = true
// Start the htlc success resolver.
ctx.resolve()
// Trigger and event that will resolve our test context.
resolve(ctx)
// Go through our list of expected checkpoints, so we can run the
// preCheckpoint logic if needed.
resumed := true
for i, cp := range expectedCheckpoints {
if cp.preCheckpoint != nil {
if err := cp.preCheckpoint(ctx, resumed); err != nil {
t.Fatalf("failure at stage %d: %v", i, err)
}
for _, report := range reports {
assertResolverReport(t, reportChan, report)
}
resumed = false
// Wait for the resolver to have checkpointed its state.
<-checkpointChan
}
// Wait for the resolver to fully complete.
ctx.waitForResult()
if nextCheckpoint < len(expectedCheckpoints) {
t.Fatalf("not all checkpoints hit")
}
return checkpointedState
}

View File

@ -4,8 +4,8 @@ import (
"encoding/binary"
"fmt"
"io"
"sync"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/davecgh/go-spew/spew"
@ -15,6 +15,7 @@ import (
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/sweep"
)
// htlcTimeoutResolver is a ContractResolver that's capable of resolving an
@ -46,6 +47,15 @@ type htlcTimeoutResolver struct {
// htlc contains information on the htlc that we are resolving on-chain.
htlc channeldb.HTLC
// currentReport stores the current state of the resolver for reporting
// over the rpc interface. This should only be reported in case we have
// a non-nil SignDetails on the htlcResolution, otherwise the nursery
// will produce reports.
currentReport ContractReport
// reportLock prevents concurrent access to the resolver report.
reportLock sync.Mutex
contractResolverKit
}
@ -54,12 +64,16 @@ func newTimeoutResolver(res lnwallet.OutgoingHtlcResolution,
broadcastHeight uint32, htlc channeldb.HTLC,
resCfg ResolverConfig) *htlcTimeoutResolver {
return &htlcTimeoutResolver{
h := &htlcTimeoutResolver{
contractResolverKit: *newContractResolverKit(resCfg),
htlcResolution: res,
broadcastHeight: broadcastHeight,
htlc: htlc,
}
h.initReport()
return h
}
// ResolverKey returns an identifier which should be globally unique for this
@ -254,9 +268,83 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
return nil, nil
}
// If we haven't already sent the output to the utxo nursery, then
// we'll do so now.
if !h.outputIncubating {
// Start by spending the HTLC output, either by broadcasting the
// second-level timeout transaction, or directly if this is the remote
// commitment.
commitSpend, err := h.spendHtlcOutput()
if err != nil {
return nil, err
}
// If the spend reveals the pre-image, then we'll enter the clean up
// workflow to pass the pre-image back to the incoming link, add it to
// the witness cache, and exit.
if isSuccessSpend(commitSpend, h.htlcResolution.SignedTimeoutTx != nil) {
log.Infof("%T(%v): HTLC has been swept with pre-image by "+
"remote party during timeout flow! Adding pre-image to "+
"witness cache", h.htlcResolution.ClaimOutpoint)
return h.claimCleanUp(commitSpend)
}
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, failing the
// HTLC on the incoming link.
failureMsg := &lnwire.FailPermanentChannelFailure{}
if err := h.DeliverResolutionMsg(ResolutionMsg{
SourceChan: h.ShortChanID,
HtlcIndex: h.htlc.HtlcIndex,
Failure: failureMsg,
}); err != nil {
return nil, err
}
// Depending on whether this was a local or remote commit, we must
// handle the spending transaction accordingly.
return h.handleCommitSpend(commitSpend)
}
// spendHtlcOutput handles the initial spend of an HTLC output via the timeout
// clause. If this is our local commitment, the second-level timeout TX will be
// used to spend the output into the next stage. If this is the remote
// commitment, the output will be swept directly without the timeout
// transaction.
func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) {
switch {
// If we have non-nil SignDetails, this means that have a 2nd level
// HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY
// (the case for anchor type channels). In this case we can re-sign it
// and attach fees at will. We let the sweeper handle this job.
case h.htlcResolution.SignDetails != nil && !h.outputIncubating:
log.Infof("%T(%x): offering second-layer timeout tx to "+
"sweeper: %v", h, h.htlc.RHash[:],
spew.Sdump(h.htlcResolution.SignedTimeoutTx))
inp := input.MakeHtlcSecondLevelTimeoutAnchorInput(
h.htlcResolution.SignedTimeoutTx,
h.htlcResolution.SignDetails,
h.broadcastHeight,
)
_, err := h.Sweeper.SweepInput(
&inp,
sweep.Params{
Fee: sweep.FeePreference{
ConfTarget: secondLevelConfTarget,
},
},
)
if err != nil {
return nil, err
}
// If we have no SignDetails, and we haven't already sent the output to
// the utxo nursery, then we'll do so now.
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
log.Tracef("%T(%v): incubating htlc output", h,
h.htlcResolution.ClaimOutpoint)
@ -276,51 +364,14 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
}
}
var spendTxID *chainhash.Hash
// 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 by a confirmed transaction.
spendNtfn, err := h.Notifier.RegisterSpendNtfn(
&h.htlcResolution.ClaimOutpoint,
h.htlcResolution.SweepSignDesc.Output.PkScript,
h.broadcastHeight,
)
if err != nil {
return err
}
select {
case spendDetail, ok := <-spendNtfn.Spend:
if !ok {
return errResolverShuttingDown
}
spendTxID = spendDetail.SpenderTxHash
case <-h.quit:
return errResolverShuttingDown
}
return nil
}
// Now that we've handed off the HTLC to the nursery, we'll watch for a
// spend of the output, and make our next move off of that. Depending
// on if this is our commitment, or the remote party's commitment,
// we'll be watching a different outpoint and script.
// Now that we've handed off the HTLC to the nursery or sweeper, we'll
// watch for a spend of the output, and make our next move off of that.
// Depending on if this is our commitment, or the remote party's
// commitment, we'll be watching a different outpoint and script.
outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
if err != nil {
return nil, err
}
spendNtfn, err := h.Notifier.RegisterSpendNtfn(
outpointToWatch, scriptToWatch, h.broadcastHeight,
)
if err != nil {
return nil, err
}
log.Infof("%T(%v): waiting for HTLC output %v to be spent"+
"fully confirmed", h, h.htlcResolution.ClaimOutpoint,
@ -328,81 +379,158 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
// We'll block here until either we exit, or the HTLC output on the
// commitment transaction has been spent.
var (
spend *chainntnfs.SpendDetail
ok bool
spend, err := waitForSpend(
outpointToWatch, scriptToWatch, h.broadcastHeight,
h.Notifier, h.quit,
)
select {
case spend, ok = <-spendNtfn.Spend:
if !ok {
return nil, errResolverShuttingDown
}
spendTxID = spend.SpenderTxHash
case <-h.quit:
return nil, errResolverShuttingDown
}
// If the spend reveals the pre-image, then we'll enter the clean up
// workflow to pass the pre-image back to the incoming link, add it to
// the witness cache, and exit.
if isSuccessSpend(spend, h.htlcResolution.SignedTimeoutTx != nil) {
log.Infof("%T(%v): HTLC has been swept with pre-image by "+
"remote party during timeout flow! Adding pre-image to "+
"witness cache", h.htlcResolution.ClaimOutpoint)
return h.claimCleanUp(spend)
}
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, failing the
// HTLC on the incoming link.
failureMsg := &lnwire.FailPermanentChannelFailure{}
if err := h.DeliverResolutionMsg(ResolutionMsg{
SourceChan: h.ShortChanID,
HtlcIndex: h.htlc.HtlcIndex,
Failure: failureMsg,
}); err != nil {
if err != nil {
return nil, err
}
var reports []*channeldb.ResolverReport
// If this was the second level transaction published by the sweeper,
// we can checkpoint the resolver now that it's confirmed.
if h.htlcResolution.SignDetails != nil && !h.outputIncubating {
h.outputIncubating = true
if err := h.Checkpoint(h); err != nil {
log.Errorf("unable to Checkpoint: %v", err)
return nil, err
}
}
return spend, err
}
// handleCommitSpend handles the spend of the HTLC output on the commitment
// transaction. If this was our local commitment, the spend will be he
// confirmed second-level timeout transaction, and we'll sweep that into our
// wallet. If the was a remote commitment, the resolver will resolve
// immetiately.
func (h *htlcTimeoutResolver) handleCommitSpend(
commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) {
var (
// claimOutpoint will be the outpoint of the second level
// transaction, or on the remote commitment directly. It will
// start out as set in the resolution, but we'll update it if
// the second-level goes through the sweeper and changes its
// txid.
claimOutpoint = h.htlcResolution.ClaimOutpoint
// spendTxID will be the ultimate spend of the claimOutpoint.
// We set it to the commit spend for now, as this is the
// ultimate spend in case this is a remote commitment. If we go
// through the second-level transaction, we'll update this
// accordingly.
spendTxID = commitSpend.SpenderTxHash
reports []*channeldb.ResolverReport
)
switch {
// If the sweeper is handling the second level transaction, wait for
// the CSV lock to expire, before sweeping the output on the
// second-level.
case h.htlcResolution.SignDetails != nil:
waitHeight := uint32(commitSpend.SpendingHeight) +
h.htlcResolution.CsvDelay - 1
h.reportLock.Lock()
h.currentReport.Stage = 2
h.currentReport.MaturityHeight = waitHeight
h.reportLock.Unlock()
log.Infof("%T(%x): waiting for CSV lock to expire at height %v",
h, h.htlc.RHash[:], waitHeight)
err := waitForHeight(waitHeight, h.Notifier, h.quit)
if err != nil {
return nil, err
}
// We'll use this input index to determine the second-level
// output index on the transaction, as the signatures requires
// the indexes to be the same. We don't look for the
// second-level output script directly, as there might be more
// than one HTLC output to the same pkScript.
op := &wire.OutPoint{
Hash: *commitSpend.SpenderTxHash,
Index: commitSpend.SpenderInputIndex,
}
// Let the sweeper sweep the second-level output now that the
// CSV delay has passed.
log.Infof("%T(%x): CSV lock expired, offering second-layer "+
"output to sweeper: %v", h, h.htlc.RHash[:], op)
inp := input.NewCsvInput(
op, input.HtlcOfferedTimeoutSecondLevel,
&h.htlcResolution.SweepSignDesc,
h.broadcastHeight,
h.htlcResolution.CsvDelay,
)
_, err = h.Sweeper.SweepInput(
inp,
sweep.Params{
Fee: sweep.FeePreference{
ConfTarget: sweepConfTarget,
},
},
)
if err != nil {
return nil, err
}
// Update the claim outpoint to point to the second-level
// transaction created by the sweeper.
claimOutpoint = *op
fallthrough
// Finally, if this was an output on our commitment transaction, we'll
// wait 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 {
case h.htlcResolution.SignedTimeoutTx != nil:
log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+
"delayed output", h, claimOutpoint)
sweep, err := waitForSpend(
&claimOutpoint,
h.htlcResolution.SweepSignDesc.Output.PkScript,
h.broadcastHeight, h.Notifier, h.quit,
)
if err != nil {
return nil, err
}
// Once our timeout tx has confirmed, we add a resolution for
// our timeoutTx tx first stage transaction.
timeoutTx := h.htlcResolution.SignedTimeoutTx
spendHash := timeoutTx.TxHash()
// Update the spend txid to the hash of the sweep transaction.
spendTxID = sweep.SpenderTxHash
// Once our sweep of the timeout tx has confirmed, we add a
// resolution for our timeoutTx tx first stage transaction.
timeoutTx := commitSpend.SpendingTx
index := commitSpend.SpenderInputIndex
spendHash := commitSpend.SpenderTxHash
reports = append(reports, &channeldb.ResolverReport{
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint,
OutPoint: timeoutTx.TxIn[index].PreviousOutPoint,
Amount: h.htlc.Amt.ToSatoshis(),
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
SpendTxID: &spendHash,
SpendTxID: spendHash,
})
}
// With the clean up message sent, we'll now mark the contract
// resolved, record the timeout and the sweep txid on disk, and wait.
// resolved, update the recovered balance, record the timeout and the
// sweep txid on disk, and wait.
h.resolved = true
h.reportLock.Lock()
h.currentReport.RecoveredBalance = h.currentReport.LimboBalance
h.currentReport.LimboBalance = 0
h.reportLock.Unlock()
amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value)
reports = append(reports, &channeldb.ResolverReport{
OutPoint: h.htlcResolution.ClaimOutpoint,
OutPoint: claimOutpoint,
Amount: amt,
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
@ -428,6 +556,40 @@ func (h *htlcTimeoutResolver) IsResolved() bool {
return h.resolved
}
// report returns a report on the resolution state of the contract.
func (h *htlcTimeoutResolver) report() *ContractReport {
// If the sign details are nil, the report will be created by handled
// by the nursery.
if h.htlcResolution.SignDetails == nil {
return nil
}
h.reportLock.Lock()
defer h.reportLock.Unlock()
copy := h.currentReport
return &copy
}
func (h *htlcTimeoutResolver) initReport() {
// We create the initial report. This will only be reported for
// resolvers not handled by the nursery.
finalAmt := h.htlc.Amt.ToSatoshis()
if h.htlcResolution.SignedTimeoutTx != nil {
finalAmt = btcutil.Amount(
h.htlcResolution.SignedTimeoutTx.TxOut[0].Value,
)
}
h.currentReport = ContractReport{
Outpoint: h.htlcResolution.ClaimOutpoint,
Type: ReportOutputOutgoingHtlc,
Amount: finalAmt,
MaturityHeight: h.htlcResolution.Expiry,
LimboBalance: finalAmt,
Stage: 1,
}
}
// Encode writes an encoded version of the ContractResolver into the passed
// Writer.
//
@ -455,6 +617,12 @@ func (h *htlcTimeoutResolver) Encode(w io.Writer) error {
return err
}
// We encode the sign details last for backwards compatibility.
err := encodeSignDetails(w, h.htlcResolution.SignDetails)
if err != nil {
return err
}
return nil
}
@ -490,6 +658,18 @@ func newTimeoutResolverFromReader(r io.Reader, resCfg ResolverConfig) (
return nil, err
}
// Sign details is a new field that was added to the htlc resolution,
// so it is serialized last for backwards compatibility. We try to read
// it, but don't error out if there are not bytes left.
signDetails, err := decodeSignDetails(r)
if err == nil {
h.htlcResolution.SignDetails = signDetails
} else if err != io.EOF && err != io.ErrUnexpectedEOF {
return nil, err
}
h.initReport()
return h, nil
}

View File

@ -3,10 +3,12 @@ package contractcourt
import (
"bytes"
"fmt"
"reflect"
"sync"
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
@ -17,6 +19,7 @@ import (
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/stretchr/testify/require"
)
type mockWitnessBeacon struct {
@ -127,6 +130,16 @@ func TestHtlcTimeoutResolver(t *testing.T) {
return nil, err
}
// To avoid triggering the race detector by
// setting the witness the second time this
// method is called during tests, we return
// immediately if the witness is already set
// correctly.
if reflect.DeepEqual(
templateTx.TxIn[0].Witness, witness,
) {
return templateTx, nil
}
templateTx.TxIn[0].Witness = witness
return templateTx, nil
},
@ -148,6 +161,17 @@ func TestHtlcTimeoutResolver(t *testing.T) {
return nil, err
}
// To avoid triggering the race detector by
// setting the witness the second time this
// method is called during tests, we return
// immediately if the witness is already set
// correctly.
if reflect.DeepEqual(
templateTx.TxIn[0].Witness, witness,
) {
return templateTx, nil
}
templateTx.TxIn[0].Witness = witness
// Set the outpoint to be on our commitment, since
@ -174,6 +198,17 @@ func TestHtlcTimeoutResolver(t *testing.T) {
return nil, err
}
// To avoid triggering the race detector by
// setting the witness the second time this
// method is called during tests, we return
// immediately if the witness is already set
// correctly.
if reflect.DeepEqual(
templateTx.TxIn[0].Witness, witness,
) {
return templateTx, nil
}
templateTx.TxIn[0].Witness = witness
return templateTx, nil
},
@ -196,6 +231,17 @@ func TestHtlcTimeoutResolver(t *testing.T) {
return nil, err
}
// To avoid triggering the race detector by
// setting the witness the second time this
// method is called during tests, we return
// immediately if the witness is already set
// correctly.
if reflect.DeepEqual(
templateTx.TxIn[0].Witness, witness,
) {
return templateTx, nil
}
templateTx.TxIn[0].Witness = witness
return templateTx, nil
},
@ -282,16 +328,19 @@ func TestHtlcTimeoutResolver(t *testing.T) {
// broadcast, then we'll set the timeout commit to a fake
// transaction to force the code path.
if !testCase.remoteCommit {
resolver.htlcResolution.SignedTimeoutTx = sweepTx
timeoutTx, err := testCase.txToBroadcast()
require.NoError(t, err)
resolver.htlcResolution.SignedTimeoutTx = timeoutTx
if testCase.timeout {
success := sweepTx.TxHash()
timeoutTxID := timeoutTx.TxHash()
reports = append(reports, &channeldb.ResolverReport{
OutPoint: sweepTx.TxIn[0].PreviousOutPoint,
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint,
Amount: testHtlcAmt.ToSatoshis(),
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
SpendTxID: &success,
SpendTxID: &timeoutTxID,
})
}
}
@ -433,3 +482,834 @@ func TestHtlcTimeoutResolver(t *testing.T) {
}
}
}
// NOTE: the following tests essentially checks many of the same scenarios as
// the test above, but they expand on it by checking resuming from checkpoints
// at every stage.
// TestHtlcTimeoutSingleStage tests a remote commitment confirming, and the
// local node sweeping the HTLC output directly after timeout.
func TestHtlcTimeoutSingleStage(t *testing.T) {
commitOutpoint := wire.OutPoint{Index: 3}
sweepTx := &wire.MsgTx{
TxIn: []*wire.TxIn{{}},
TxOut: []*wire.TxOut{{}},
}
// singleStageResolution is a resolution for a htlc on the remote
// party's commitment.
singleStageResolution := lnwallet.OutgoingHtlcResolution{
ClaimOutpoint: commitOutpoint,
SweepSignDesc: testSignDesc,
}
sweepTxid := sweepTx.TxHash()
claim := &channeldb.ResolverReport{
OutPoint: commitOutpoint,
Amount: btcutil.Amount(testSignDesc.Output.Value),
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
SpendTxID: &sweepTxid,
}
checkpoints := []checkpoint{
{
// Output should be handed off to the nursery.
incubating: true,
},
{
// We send a confirmation the sweep tx from published
// by the nursery.
preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error {
// The nursery will create and publish a sweep
// tx.
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: sweepTx,
SpenderTxHash: &sweepTxid,
}
// The resolver should deliver a failure
// resolition message (indicating we
// successfully timed out the HTLC).
select {
case resolutionMsg := <-ctx.resolutionChan:
if resolutionMsg.Failure == nil {
t.Fatalf("expected failure resolution msg")
}
case <-time.After(time.Second * 5):
t.Fatalf("resolution not sent")
}
return nil
},
// After the sweep has confirmed, we expect the
// checkpoint to be resolved, and with the above
// report.
incubating: true,
resolved: true,
reports: []*channeldb.ResolverReport{
claim,
},
},
}
testHtlcTimeout(
t, singleStageResolution, checkpoints,
)
}
// TestHtlcTimeoutSecondStage tests a local commitment being confirmed, and the
// local node claiming the HTLC output using the second-level timeout tx.
func TestHtlcTimeoutSecondStage(t *testing.T) {
commitOutpoint := wire.OutPoint{Index: 2}
htlcOutpoint := wire.OutPoint{Index: 3}
sweepTx := &wire.MsgTx{
TxIn: []*wire.TxIn{{}},
TxOut: []*wire.TxOut{{}},
}
sweepHash := sweepTx.TxHash()
timeoutTx := &wire.MsgTx{
TxIn: []*wire.TxIn{
{
PreviousOutPoint: commitOutpoint,
},
},
TxOut: []*wire.TxOut{
{
Value: 111,
PkScript: []byte{0xaa, 0xaa},
},
},
}
signer := &mock.DummySigner{}
witness, err := input.SenderHtlcSpendTimeout(
&mock.DummySignature{}, txscript.SigHashAll,
signer, &testSignDesc, timeoutTx,
)
require.NoError(t, err)
timeoutTx.TxIn[0].Witness = witness
timeoutTxid := timeoutTx.TxHash()
// twoStageResolution is a resolution for a htlc on the local
// party's commitment.
twoStageResolution := lnwallet.OutgoingHtlcResolution{
ClaimOutpoint: htlcOutpoint,
SignedTimeoutTx: timeoutTx,
SweepSignDesc: testSignDesc,
}
firstStage := &channeldb.ResolverReport{
OutPoint: commitOutpoint,
Amount: testHtlcAmt.ToSatoshis(),
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
SpendTxID: &timeoutTxid,
}
secondState := &channeldb.ResolverReport{
OutPoint: htlcOutpoint,
Amount: btcutil.Amount(testSignDesc.Output.Value),
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
SpendTxID: &sweepHash,
}
checkpoints := []checkpoint{
{
// Output should be handed off to the nursery.
incubating: true,
},
{
// We send a confirmation for our sweep tx to indicate
// that our sweep succeeded.
preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error {
// The nursery will publish the timeout tx.
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: timeoutTx,
SpenderTxHash: &timeoutTxid,
}
// The resolver should deliver a failure
// resolution message (indicating we
// successfully timed out the HTLC).
select {
case resolutionMsg := <-ctx.resolutionChan:
if resolutionMsg.Failure == nil {
t.Fatalf("expected failure resolution msg")
}
case <-time.After(time.Second * 1):
t.Fatalf("resolution not sent")
}
// Deliver spend of timeout tx.
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: sweepTx,
SpenderTxHash: &sweepHash,
}
return nil
},
// After the sweep has confirmed, we expect the
// checkpoint to be resolved, and with the above
// reports.
incubating: true,
resolved: true,
reports: []*channeldb.ResolverReport{
firstStage, secondState,
},
},
}
testHtlcTimeout(
t, twoStageResolution, checkpoints,
)
}
// TestHtlcTimeoutSingleStageRemoteSpend tests that when a local commitment
// confirms, and the remote spends the HTLC output directly, we detect this and
// extract the preimage.
func TestHtlcTimeoutSingleStageRemoteSpend(t *testing.T) {
commitOutpoint := wire.OutPoint{Index: 2}
htlcOutpoint := wire.OutPoint{Index: 3}
spendTx := &wire.MsgTx{
TxIn: []*wire.TxIn{{}},
TxOut: []*wire.TxOut{{}},
}
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
var fakePreimage lntypes.Preimage
copy(fakePreimage[:], fakePreimageBytes)
signer := &mock.DummySigner{}
witness, err := input.SenderHtlcSpendRedeem(
signer, &testSignDesc, spendTx,
fakePreimageBytes,
)
require.NoError(t, err)
spendTx.TxIn[0].Witness = witness
spendTxHash := spendTx.TxHash()
timeoutTx := &wire.MsgTx{
TxIn: []*wire.TxIn{
{
PreviousOutPoint: commitOutpoint,
},
},
TxOut: []*wire.TxOut{
{
Value: 123,
PkScript: []byte{0xff, 0xff},
},
},
}
timeoutWitness, err := input.SenderHtlcSpendTimeout(
&mock.DummySignature{}, txscript.SigHashAll,
signer, &testSignDesc, timeoutTx,
)
require.NoError(t, err)
timeoutTx.TxIn[0].Witness = timeoutWitness
// twoStageResolution is a resolution for a htlc on the local
// party's commitment.
twoStageResolution := lnwallet.OutgoingHtlcResolution{
ClaimOutpoint: htlcOutpoint,
SignedTimeoutTx: timeoutTx,
SweepSignDesc: testSignDesc,
}
claim := &channeldb.ResolverReport{
OutPoint: htlcOutpoint,
Amount: btcutil.Amount(testSignDesc.Output.Value),
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
SpendTxID: &spendTxHash,
}
checkpoints := []checkpoint{
{
// Output should be handed off to the nursery.
incubating: true,
},
{
// We send a spend notification for a remote spend with
// the preimage.
preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error {
witnessBeacon := ctx.resolver.(*htlcTimeoutResolver).PreimageDB.(*mockWitnessBeacon)
// The remote spends the output direcly with
// the preimage.
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: spendTx,
SpenderTxHash: &spendTxHash,
}
// We should extract the preimage.
select {
case newPreimage := <-witnessBeacon.newPreimages:
if newPreimage[0] != fakePreimage {
t.Fatalf("wrong pre-image: "+
"expected %v, got %v",
fakePreimage, newPreimage)
}
case <-time.After(time.Second * 5):
t.Fatalf("pre-image not added")
}
// Finally, we should get a resolution message
// with the pre-image set within the message.
select {
case resolutionMsg := <-ctx.resolutionChan:
if *resolutionMsg.PreImage != fakePreimage {
t.Fatalf("wrong pre-image: "+
"expected %v, got %v",
fakePreimage, resolutionMsg.PreImage)
}
case <-time.After(time.Second * 5):
t.Fatalf("resolution not sent")
}
return nil
},
// After the success tx has confirmed, we expect the
// checkpoint to be resolved, and with the above
// report.
incubating: true,
resolved: true,
reports: []*channeldb.ResolverReport{
claim,
},
},
}
testHtlcTimeout(
t, twoStageResolution, checkpoints,
)
}
// TestHtlcTimeoutSecondStageRemoteSpend tests that when a remite commitment
// confirms, and the remote spends the output using the success tx, we
// properly detect this and extract the preimage.
func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
commitOutpoint := wire.OutPoint{Index: 2}
remoteSuccessTx := &wire.MsgTx{
TxIn: []*wire.TxIn{
{
PreviousOutPoint: commitOutpoint,
},
},
TxOut: []*wire.TxOut{},
}
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
var fakePreimage lntypes.Preimage
copy(fakePreimage[:], fakePreimageBytes)
signer := &mock.DummySigner{}
witness, err := input.ReceiverHtlcSpendRedeem(
&mock.DummySignature{}, txscript.SigHashAll,
fakePreimageBytes, signer,
&testSignDesc, remoteSuccessTx,
)
require.NoError(t, err)
remoteSuccessTx.TxIn[0].Witness = witness
successTxid := remoteSuccessTx.TxHash()
// singleStageResolution allwoing the local node to sweep HTLC output
// directly from the remote commitment after timeout.
singleStageResolution := lnwallet.OutgoingHtlcResolution{
ClaimOutpoint: commitOutpoint,
SweepSignDesc: testSignDesc,
}
claim := &channeldb.ResolverReport{
OutPoint: commitOutpoint,
Amount: btcutil.Amount(testSignDesc.Output.Value),
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
SpendTxID: &successTxid,
}
checkpoints := []checkpoint{
{
// Output should be handed off to the nursery.
incubating: true,
},
{
// We send a confirmation for the remote's second layer
// success transcation.
preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error {
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: remoteSuccessTx,
SpenderTxHash: &successTxid,
}
witnessBeacon := ctx.resolver.(*htlcTimeoutResolver).PreimageDB.(*mockWitnessBeacon)
// We expect the preimage to be extracted,
select {
case newPreimage := <-witnessBeacon.newPreimages:
if newPreimage[0] != fakePreimage {
t.Fatalf("wrong pre-image: "+
"expected %v, got %v",
fakePreimage, newPreimage)
}
case <-time.After(time.Second * 5):
t.Fatalf("pre-image not added")
}
// Finally, we should get a resolution message with the
// pre-image set within the message.
select {
case resolutionMsg := <-ctx.resolutionChan:
if *resolutionMsg.PreImage != fakePreimage {
t.Fatalf("wrong pre-image: "+
"expected %v, got %v",
fakePreimage, resolutionMsg.PreImage)
}
case <-time.After(time.Second * 5):
t.Fatalf("resolution not sent")
}
return nil
},
// After the sweep has confirmed, we expect the
// checkpoint to be resolved, and with the above
// report.
incubating: true,
resolved: true,
reports: []*channeldb.ResolverReport{
claim,
},
},
}
testHtlcTimeout(
t, singleStageResolution, checkpoints,
)
}
// TestHtlcTimeoutSecondStageSweeper tests that for anchor channels, when a
// local commitment confirms, the timeout tx is handed to the sweeper to claim
// the HTLC output.
func TestHtlcTimeoutSecondStageSweeper(t *testing.T) {
commitOutpoint := wire.OutPoint{Index: 2}
htlcOutpoint := wire.OutPoint{Index: 3}
sweepTx := &wire.MsgTx{
TxIn: []*wire.TxIn{{}},
TxOut: []*wire.TxOut{{}},
}
sweepHash := sweepTx.TxHash()
timeoutTx := &wire.MsgTx{
TxIn: []*wire.TxIn{
{
PreviousOutPoint: commitOutpoint,
},
},
TxOut: []*wire.TxOut{
{
Value: 123,
PkScript: []byte{0xff, 0xff},
},
},
}
// We set the timeout witness since the script is used when subscribing
// to spends.
signer := &mock.DummySigner{}
timeoutWitness, err := input.SenderHtlcSpendTimeout(
&mock.DummySignature{}, txscript.SigHashAll,
signer, &testSignDesc, timeoutTx,
)
require.NoError(t, err)
timeoutTx.TxIn[0].Witness = timeoutWitness
reSignedTimeoutTx := &wire.MsgTx{
TxIn: []*wire.TxIn{
{
PreviousOutPoint: wire.OutPoint{
Hash: chainhash.Hash{0xaa, 0xbb},
Index: 0,
},
},
timeoutTx.TxIn[0],
{
PreviousOutPoint: wire.OutPoint{
Hash: chainhash.Hash{0xaa, 0xbb},
Index: 2,
},
},
},
TxOut: []*wire.TxOut{
{
Value: 111,
PkScript: []byte{0xaa, 0xaa},
},
timeoutTx.TxOut[0],
},
}
reSignedHash := reSignedTimeoutTx.TxHash()
reSignedOutPoint := wire.OutPoint{
Hash: reSignedHash,
Index: 1,
}
// twoStageResolution is a resolution for a htlc on the local
// party's commitment, where the timout tx can be re-signed.
twoStageResolution := lnwallet.OutgoingHtlcResolution{
ClaimOutpoint: htlcOutpoint,
SignedTimeoutTx: timeoutTx,
SignDetails: &input.SignDetails{
SignDesc: testSignDesc,
PeerSig: testSig,
},
SweepSignDesc: testSignDesc,
}
firstStage := &channeldb.ResolverReport{
OutPoint: commitOutpoint,
Amount: testHtlcAmt.ToSatoshis(),
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
SpendTxID: &reSignedHash,
}
secondState := &channeldb.ResolverReport{
OutPoint: reSignedOutPoint,
Amount: btcutil.Amount(testSignDesc.Output.Value),
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
SpendTxID: &sweepHash,
}
checkpoints := []checkpoint{
{
// The output should be given to the sweeper.
preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error {
resolver := ctx.resolver.(*htlcTimeoutResolver)
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
op := inp.OutPoint()
if *op != commitOutpoint {
return fmt.Errorf("outpoint %v swept, "+
"expected %v", op,
commitOutpoint)
}
// Emulat the sweeper spending using the
// re-signed timeout tx.
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: reSignedTimeoutTx,
SpenderInputIndex: 1,
SpenderTxHash: &reSignedHash,
SpendingHeight: 10,
}
return nil
},
// incubating=true is used to signal that the
// second-level transaction was confirmed.
incubating: true,
},
{
// We send a confirmation for our sweep tx to indicate
// that our sweep succeeded.
preCheckpoint: func(ctx *htlcResolverTestContext,
resumed bool) error {
// If we are resuming from a checkpoing, we
// expect the resolver to re-subscribe to a
// spend, hence we must resend it.
if resumed {
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: reSignedTimeoutTx,
SpenderInputIndex: 1,
SpenderTxHash: &reSignedHash,
SpendingHeight: 10,
}
}
// The resolver should deliver a failure
// resolution message (indicating we
// successfully timed out the HTLC).
select {
case resolutionMsg := <-ctx.resolutionChan:
if resolutionMsg.Failure == nil {
t.Fatalf("expected failure resolution msg")
}
case <-time.After(time.Second * 1):
t.Fatalf("resolution not sent")
}
// Mimic CSV lock expiring.
ctx.notifier.EpochChan <- &chainntnfs.BlockEpoch{
Height: 13,
}
// The timout tx output should now be given to
// the sweeper.
resolver := ctx.resolver.(*htlcTimeoutResolver)
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
op := inp.OutPoint()
exp := wire.OutPoint{
Hash: reSignedHash,
Index: 1,
}
if *op != exp {
return fmt.Errorf("wrong outpoint swept")
}
// Notify about the spend, which should resolve
// the resolver.
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: sweepTx,
SpenderTxHash: &sweepHash,
SpendingHeight: 14,
}
return nil
},
// After the sweep has confirmed, we expect the
// checkpoint to be resolved, and with the above
// reports.
incubating: true,
resolved: true,
reports: []*channeldb.ResolverReport{
firstStage,
secondState,
},
},
}
testHtlcTimeout(
t, twoStageResolution, checkpoints,
)
}
// TestHtlcTimeoutSecondStageSweeperRemoteSpend tests that if a local timeout
// tx is offered to the sweeper, but the output is swept by the remote node, we
// properly detect this and extract the preimage.
func TestHtlcTimeoutSecondStageSweeperRemoteSpend(t *testing.T) {
commitOutpoint := wire.OutPoint{Index: 2}
htlcOutpoint := wire.OutPoint{Index: 3}
timeoutTx := &wire.MsgTx{
TxIn: []*wire.TxIn{
{
PreviousOutPoint: commitOutpoint,
},
},
TxOut: []*wire.TxOut{
{
Value: 123,
PkScript: []byte{0xff, 0xff},
},
},
}
// We set the timeout witness since the script is used when subscribing
// to spends.
signer := &mock.DummySigner{}
timeoutWitness, err := input.SenderHtlcSpendTimeout(
&mock.DummySignature{}, txscript.SigHashAll,
signer, &testSignDesc, timeoutTx,
)
require.NoError(t, err)
timeoutTx.TxIn[0].Witness = timeoutWitness
spendTx := &wire.MsgTx{
TxIn: []*wire.TxIn{{}},
TxOut: []*wire.TxOut{{}},
}
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
var fakePreimage lntypes.Preimage
copy(fakePreimage[:], fakePreimageBytes)
witness, err := input.SenderHtlcSpendRedeem(
signer, &testSignDesc, spendTx,
fakePreimageBytes,
)
require.NoError(t, err)
spendTx.TxIn[0].Witness = witness
spendTxHash := spendTx.TxHash()
// twoStageResolution is a resolution for a htlc on the local
// party's commitment, where the timout tx can be re-signed.
twoStageResolution := lnwallet.OutgoingHtlcResolution{
ClaimOutpoint: htlcOutpoint,
SignedTimeoutTx: timeoutTx,
SignDetails: &input.SignDetails{
SignDesc: testSignDesc,
PeerSig: testSig,
},
SweepSignDesc: testSignDesc,
}
claim := &channeldb.ResolverReport{
OutPoint: htlcOutpoint,
Amount: btcutil.Amount(testSignDesc.Output.Value),
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
SpendTxID: &spendTxHash,
}
checkpoints := []checkpoint{
{
// The output should be given to the sweeper.
preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error {
resolver := ctx.resolver.(*htlcTimeoutResolver)
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
op := inp.OutPoint()
if *op != commitOutpoint {
return fmt.Errorf("outpoint %v swept, "+
"expected %v", op,
commitOutpoint)
}
// Emulate the remote sweeping the output with the preimage.
// re-signed timeout tx.
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: spendTx,
SpenderTxHash: &spendTxHash,
}
return nil
},
// incubating=true is used to signal that the
// second-level transaction was confirmed.
incubating: true,
},
{
// We send a confirmation for our sweep tx to indicate
// that our sweep succeeded.
preCheckpoint: func(ctx *htlcResolverTestContext,
resumed bool) error {
// If we are resuming from a checkpoing, we
// expect the resolver to re-subscribe to a
// spend, hence we must resend it.
if resumed {
fmt.Println("resumed")
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: spendTx,
SpenderTxHash: &spendTxHash,
}
}
witnessBeacon := ctx.resolver.(*htlcTimeoutResolver).PreimageDB.(*mockWitnessBeacon)
// We should extract the preimage.
select {
case newPreimage := <-witnessBeacon.newPreimages:
if newPreimage[0] != fakePreimage {
t.Fatalf("wrong pre-image: "+
"expected %v, got %v",
fakePreimage, newPreimage)
}
case <-time.After(time.Second * 5):
t.Fatalf("pre-image not added")
}
// Finally, we should get a resolution message
// with the pre-image set within the message.
select {
case resolutionMsg := <-ctx.resolutionChan:
if *resolutionMsg.PreImage != fakePreimage {
t.Fatalf("wrong pre-image: "+
"expected %v, got %v",
fakePreimage, resolutionMsg.PreImage)
}
case <-time.After(time.Second * 5):
t.Fatalf("resolution not sent")
}
return nil
},
// After the sweep has confirmed, we expect the
// checkpoint to be resolved, and with the above
// reports.
incubating: true,
resolved: true,
reports: []*channeldb.ResolverReport{
claim,
},
},
}
testHtlcTimeout(
t, twoStageResolution, checkpoints,
)
}
func testHtlcTimeout(t *testing.T, resolution lnwallet.OutgoingHtlcResolution,
checkpoints []checkpoint) {
defer timeout(t)()
// We first run the resolver from start to finish, ensuring it gets
// checkpointed at every expected stage. We store the checkpointed data
// for the next portion of the test.
ctx := newHtlcResolverTestContext(t,
func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver {
return &htlcTimeoutResolver{
contractResolverKit: *newContractResolverKit(cfg),
htlc: htlc,
htlcResolution: resolution,
}
},
)
checkpointedState := runFromCheckpoint(t, ctx, checkpoints)
// Now, from every checkpoint created, we re-create the resolver, and
// run the test from that checkpoint.
for i := range checkpointedState {
cp := bytes.NewReader(checkpointedState[i])
ctx := newHtlcResolverTestContext(t,
func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver {
resolver, err := newTimeoutResolverFromReader(cp, cfg)
if err != nil {
t.Fatal(err)
}
resolver.Supplement(htlc)
resolver.htlcResolution = resolution
return resolver
},
)
// Run from the given checkpoint, ensuring we'll hit the rest.
_ = runFromCheckpoint(t, ctx, checkpoints[i+1:])
}
}

View File

@ -4,6 +4,7 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lntypes"
)
// Input represents an abstract UTXO which is to be spent using a sweeping
@ -67,6 +68,22 @@ type TxInfo struct {
Weight int64
}
// SignDetails is a struct containing information needed to resign certain
// inputs. It is used to re-sign 2nd level HTLC transactions that uses the
// SINGLE|ANYONECANPAY sighash type, as we have a signature provided by our
// peer, but we can aggregate multiple of these 2nd level transactions into a
// new transaction, that needs to be signed by us.
type SignDetails struct {
// SignDesc is the sign descriptor needed for us to sign the input.
SignDesc SignDescriptor
// PeerSig is the peer's signature for this input.
PeerSig Signature
// SigHashType is the sighash signed by the peer.
SigHashType txscript.SigHashType
}
type inputKit struct {
outpoint wire.OutPoint
witnessType WitnessType
@ -241,7 +258,130 @@ func (h *HtlcSucceedInput) CraftInputScript(signer Signer, txn *wire.MsgTx,
}, nil
}
// HtlcsSecondLevelAnchorInput is an input type used to spend HTLC outputs
// using a re-signed second level transaction, either via the timeout or success
// paths.
type HtlcSecondLevelAnchorInput struct {
inputKit
// SignedTx is the original second level transaction signed by the
// channel peer.
SignedTx *wire.MsgTx
// createWitness creates a witness allowing the passed transaction to
// spend the input.
createWitness func(signer Signer, txn *wire.MsgTx,
hashCache *txscript.TxSigHashes, txinIdx int) (wire.TxWitness, error)
}
// RequiredTxOut returns the tx out needed to be present on the sweep tx for
// the spend of the input to be valid.
func (i *HtlcSecondLevelAnchorInput) RequiredTxOut() *wire.TxOut {
return i.SignedTx.TxOut[0]
}
// RequiredLockTime returns the locktime needed for the sweep tx for the spend
// of the input to be valid. For a second level HTLC timeout this will be the
// CLTV expiry, for HTLC success it will be zero.
func (i *HtlcSecondLevelAnchorInput) RequiredLockTime() (uint32, bool) {
return i.SignedTx.LockTime, true
}
// CraftInputScript returns a valid set of input scripts allowing this output
// to be spent. The returns input scripts should target the input at location
// txIndex within the passed transaction. The input scripts generated by this
// method support spending p2wkh, p2wsh, and also nested p2sh outputs.
func (i *HtlcSecondLevelAnchorInput) CraftInputScript(signer Signer,
txn *wire.MsgTx, hashCache *txscript.TxSigHashes,
txinIdx int) (*Script, error) {
witness, err := i.createWitness(signer, txn, hashCache, txinIdx)
if err != nil {
return nil, err
}
return &Script{
Witness: witness,
}, nil
}
// MakeHtlcSecondLevelTimeoutAnchorInput creates an input allowing the sweeper
// to spend the HTLC output on our commit using the second level timeout
// transaction.
func MakeHtlcSecondLevelTimeoutAnchorInput(signedTx *wire.MsgTx,
signDetails *SignDetails, heightHint uint32) HtlcSecondLevelAnchorInput {
// Spend an HTLC output on our local commitment tx using the
// 2nd timeout transaction.
createWitness := func(signer Signer, txn *wire.MsgTx,
hashCache *txscript.TxSigHashes,
txinIdx int) (wire.TxWitness, error) {
desc := signDetails.SignDesc
desc.SigHashes = txscript.NewTxSigHashes(txn)
desc.InputIndex = txinIdx
return SenderHtlcSpendTimeout(
signDetails.PeerSig, signDetails.SigHashType, signer,
&desc, txn,
)
}
return HtlcSecondLevelAnchorInput{
inputKit: inputKit{
outpoint: signedTx.TxIn[0].PreviousOutPoint,
witnessType: HtlcOfferedTimeoutSecondLevelInputConfirmed,
signDesc: signDetails.SignDesc,
heightHint: heightHint,
// CSV delay is always 1 for these inputs.
blockToMaturity: 1,
},
SignedTx: signedTx,
createWitness: createWitness,
}
}
// MakeHtlcSecondLevelSuccessAnchorInput creates an input allowing the sweeper
// to spend the HTLC output on our commit using the second level success
// transaction.
func MakeHtlcSecondLevelSuccessAnchorInput(signedTx *wire.MsgTx,
signDetails *SignDetails, preimage lntypes.Preimage,
heightHint uint32) HtlcSecondLevelAnchorInput {
// Spend an HTLC output on our local commitment tx using the 2nd
// success transaction.
createWitness := func(signer Signer, txn *wire.MsgTx,
hashCache *txscript.TxSigHashes,
txinIdx int) (wire.TxWitness, error) {
desc := signDetails.SignDesc
desc.SigHashes = hashCache
desc.InputIndex = txinIdx
return ReceiverHtlcSpendRedeem(
signDetails.PeerSig, signDetails.SigHashType,
preimage[:], signer, &desc, txn,
)
}
return HtlcSecondLevelAnchorInput{
inputKit: inputKit{
outpoint: signedTx.TxIn[0].PreviousOutPoint,
witnessType: HtlcAcceptedSuccessSecondLevelInputConfirmed,
signDesc: signDetails.SignDesc,
heightHint: heightHint,
// CSV delay is always 1 for these inputs.
blockToMaturity: 1,
},
SignedTx: signedTx,
createWitness: createWitness,
}
}
// Compile-time constraints to ensure each input struct implement the Input
// interface.
var _ Input = (*BaseInput)(nil)
var _ Input = (*HtlcSucceedInput)(nil)
var _ Input = (*HtlcSecondLevelAnchorInput)(nil)

View File

@ -328,6 +328,12 @@ const (
AcceptedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 8*1 + 20 + 4*1 +
33 + 5*1 + 4 + 8*1
// AcceptedHtlcScriptSizeConfirmed 143 bytes
//
// TODO(halseth): the non-confirmed version currently includes the
// overhead.
AcceptedHtlcScriptSizeConfirmed = AcceptedHtlcScriptSize // + HtlcConfirmedScriptOverhead
// AcceptedHtlcTimeoutWitnessSize 219
// - number_of_witness_elements: 1 byte
// - sender_sig_length: 1 byte
@ -361,6 +367,12 @@ const (
AcceptedHtlcSuccessWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 32 + 1 +
AcceptedHtlcScriptSize
// AcceptedHtlcSuccessWitnessSizeConfirmed 327 bytes
//
// Input to second level success tx, spending 1 CSV delayed HTLC output.
AcceptedHtlcSuccessWitnessSizeConfirmed = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 32 + 1 +
AcceptedHtlcScriptSizeConfirmed
// OfferedHtlcScriptSize 136 bytes
// - OP_DUP: 1 byte
// - OP_HASH160: 1 byte
@ -398,6 +410,12 @@ const (
// - OP_ENDIF: 1 byte
OfferedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 10*1 + 33 + 5*1 + 20 + 7*1
// OfferedHtlcScriptSizeConfirmed 136 bytes
//
// TODO(halseth): the non-confirmed version currently includes the
// overhead.
OfferedHtlcScriptSizeConfirmed = OfferedHtlcScriptSize // + HtlcConfirmedScriptOverhead
// OfferedHtlcSuccessWitnessSize 245 bytes
// - number_of_witness_elements: 1 byte
// - receiver_sig_length: 1 byte
@ -420,6 +438,12 @@ const (
// - witness_script (offered_htlc_script)
OfferedHtlcTimeoutWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 1 + OfferedHtlcScriptSize
// OfferedHtlcTimeoutWitnessSizeConfirmed 288 bytes
//
// Input to second level timeout tx, spending 1 CSV delayed HTLC output.
OfferedHtlcTimeoutWitnessSizeConfirmed = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 1 +
OfferedHtlcScriptSizeConfirmed
// OfferedHtlcPenaltyWitnessSize 246 bytes
// - number_of_witness_elements: 1 byte
// - revocation_sig_length: 1 byte

View File

@ -80,6 +80,14 @@ const (
// result, we can only spend this after a CSV delay.
HtlcOfferedTimeoutSecondLevel StandardWitnessType = 5
// HtlcOfferedTimeoutSecondLevelInputConfirmed is a witness that allows
// us to sweep an HTLC output that we extended to a party, but was
// never fulfilled. This _is_ the HTLC output directly on our
// commitment transaction, and the input to the second-level HTLC
// tiemout transaction. It can only be spent after CLTV expiry, and
// commitment confirmation.
HtlcOfferedTimeoutSecondLevelInputConfirmed StandardWitnessType = 15
// HtlcAcceptedSuccessSecondLevel is a witness that allows us to sweep
// an HTLC output that was offered to us, and for which we have a
// payment preimage. This HTLC output isn't directly on our commitment
@ -87,6 +95,14 @@ const (
// transaction. As a result, we can only spend this after a CSV delay.
HtlcAcceptedSuccessSecondLevel StandardWitnessType = 6
// HtlcAcceptedSuccessSecondLevelInputConfirmed is a witness that
// allows us to sweep an HTLC output that was offered to us, and for
// which we have a payment preimage. This _is_ the HTLC output directly
// on our commitment transaction, and the input to the second-level
// HTLC success transaction. It can only be spent after the commitment
// has confirmed.
HtlcAcceptedSuccessSecondLevelInputConfirmed StandardWitnessType = 16
// HtlcOfferedRemoteTimeout is a witness that allows us to sweep an
// HTLC that we offered to the remote party which lies in the
// commitment transaction of the remote party. We can spend this output
@ -163,9 +179,15 @@ func (wt StandardWitnessType) String() string {
case HtlcOfferedTimeoutSecondLevel:
return "HtlcOfferedTimeoutSecondLevel"
case HtlcOfferedTimeoutSecondLevelInputConfirmed:
return "HtlcOfferedTimeoutSecondLevelInputConfirmed"
case HtlcAcceptedSuccessSecondLevel:
return "HtlcAcceptedSuccessSecondLevel"
case HtlcAcceptedSuccessSecondLevelInputConfirmed:
return "HtlcAcceptedSuccessSecondLevelInputConfirmed"
case HtlcOfferedRemoteTimeout:
return "HtlcOfferedRemoteTimeout"
@ -375,12 +397,20 @@ func (wt StandardWitnessType) SizeUpperBound() (int, bool, error) {
case HtlcOfferedTimeoutSecondLevel:
return ToLocalTimeoutWitnessSize, false, nil
// Input to the outgoing HTLC second layer timeout transaction.
case HtlcOfferedTimeoutSecondLevelInputConfirmed:
return OfferedHtlcTimeoutWitnessSizeConfirmed, false, nil
// Incoming second layer HTLC's that have confirmed within the
// chain, and the output they produced is now mature enough to
// sweep.
case HtlcAcceptedSuccessSecondLevel:
return ToLocalTimeoutWitnessSize, false, nil
// Input to the incoming second-layer HTLC success transaction.
case HtlcAcceptedSuccessSecondLevelInputConfirmed:
return AcceptedHtlcSuccessWitnessSizeConfirmed, false, nil
// An HTLC on the commitment transaction of the remote party,
// that has had its absolute timelock expire.
case HtlcOfferedRemoteTimeout:

View File

@ -0,0 +1,427 @@
package itest
import (
"context"
"fmt"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
)
// testMultiHopHtlcAggregation tests that in a multi-hop HTLC scenario, if we
// force close a channel with both incoming and outgoing HTLCs, we can properly
// resolve them using the second level timeout and success transactions. In
// case of anchor channels, the second-level spends can also be aggregated and
// properly feebumped, so we'll check that as well.
func testMultiHopHtlcAggregation(net *lntest.NetworkHarness, t *harnessTest,
alice, bob *lntest.HarnessNode, c commitType) {
const finalCltvDelta = 40
ctxb := context.Background()
// First, we'll create a three hop network: Alice -> Bob -> Carol.
aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork(
t, net, alice, bob, false, c,
)
defer shutdownAndAssert(net, t, carol)
// To ensure we have capacity in both directions of the route, we'll
// make a fairly large payment Alice->Carol and settle it.
const reBalanceAmt = 500_000
invoice := &lnrpc.Invoice{
Value: reBalanceAmt,
}
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
resp, err := carol.AddInvoice(ctxt, invoice)
require.NoError(t.t, err)
sendReq := &routerrpc.SendPaymentRequest{
PaymentRequest: resp.PaymentRequest,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
stream, err := alice.RouterClient.SendPaymentV2(ctxt, sendReq)
require.NoError(t.t, err)
result, err := getPaymentResult(stream)
require.NoError(t.t, err)
require.Equal(t.t, result.Status, lnrpc.Payment_SUCCEEDED)
// With the network active, we'll now add a new hodl invoices at both
// Alice's and Carol's end. Make sure the cltv expiry delta is large
// enough, otherwise Bob won't send out the outgoing htlc.
const numInvoices = 5
const invoiceAmt = 50_000
var (
carolInvoices []*invoicesrpc.AddHoldInvoiceResp
aliceInvoices []*invoicesrpc.AddHoldInvoiceResp
alicePreimages []lntypes.Preimage
payHashes [][]byte
alicePayHashes [][]byte
carolPayHashes [][]byte
)
// Add Carol invoices.
for i := 0; i < numInvoices; i++ {
preimage := lntypes.Preimage{1, 1, 1, byte(i)}
payHash := preimage.Hash()
invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
Value: invoiceAmt,
CltvExpiry: finalCltvDelta,
Hash: payHash[:],
}
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
carolInvoice, err := carol.AddHoldInvoice(ctxt, invoiceReq)
require.NoError(t.t, err)
carolInvoices = append(carolInvoices, carolInvoice)
payHashes = append(payHashes, payHash[:])
carolPayHashes = append(carolPayHashes, payHash[:])
}
// We'll give Alice's invoices a longer CLTV expiry, to ensure the
// channel Bob<->Carol will be closed first.
for i := 0; i < numInvoices; i++ {
preimage := lntypes.Preimage{2, 2, 2, byte(i)}
payHash := preimage.Hash()
invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
Value: invoiceAmt,
CltvExpiry: 2 * finalCltvDelta,
Hash: payHash[:],
}
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
aliceInvoice, err := alice.AddHoldInvoice(ctxt, invoiceReq)
require.NoError(t.t, err)
aliceInvoices = append(aliceInvoices, aliceInvoice)
alicePreimages = append(alicePreimages, preimage)
payHashes = append(payHashes, payHash[:])
alicePayHashes = append(alicePayHashes, payHash[:])
}
// Now that we've created the invoices, we'll pay them all from
// Alice<->Carol, going through Bob. We won't wait for the response
// however, as neither will immediately settle the payment.
ctx, cancel := context.WithCancel(ctxb)
defer cancel()
// Alice will pay all of Carol's invoices.
for _, carolInvoice := range carolInvoices {
_, err = alice.RouterClient.SendPaymentV2(
ctx, &routerrpc.SendPaymentRequest{
PaymentRequest: carolInvoice.PaymentRequest,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
},
)
require.NoError(t.t, err)
}
// And Carol will pay Alice's.
for _, aliceInvoice := range aliceInvoices {
_, err = carol.RouterClient.SendPaymentV2(
ctx, &routerrpc.SendPaymentRequest{
PaymentRequest: aliceInvoice.PaymentRequest,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
},
)
require.NoError(t.t, err)
}
// At this point, all 3 nodes should now the HTLCs active on their
// channels.
nodes := []*lntest.HarnessNode{alice, bob, carol}
err = wait.NoError(func() error {
return assertActiveHtlcs(nodes, payHashes...)
}, defaultTimeout)
require.NoError(t.t, err)
// Wait for Alice and Carol to mark the invoices as accepted. There is
// a small gap to bridge between adding the htlc to the channel and
// executing the exit hop logic.
for _, payHash := range carolPayHashes {
h := lntypes.Hash{}
copy(h[:], payHash)
waitForInvoiceAccepted(t, carol, h)
}
for _, payHash := range alicePayHashes {
h := lntypes.Hash{}
copy(h[:], payHash)
waitForInvoiceAccepted(t, alice, h)
}
// Increase the fee estimate so that the following force close tx will
// be cpfp'ed.
net.SetFeeEstimate(30000)
// We'll now mine enough blocks to trigger Bob's broadcast of his
// commitment transaction due to the fact that the Carol's HTLCs are
// about to timeout. With the default outgoing broadcast delta of zero,
// this will be the same height as the htlc expiry height.
numBlocks := padCLTV(
uint32(finalCltvDelta - lncfg.DefaultOutgoingBroadcastDelta),
)
_, err = net.Miner.Node.Generate(numBlocks)
require.NoError(t.t, err)
// Bob's force close transaction should now be found in the mempool. If
// there are anchors, we also expect Bob's anchor sweep.
expectedTxes := 1
if c == commitTypeAnchors {
expectedTxes = 2
}
bobFundingTxid, err := lnd.GetChanPointFundingTxid(bobChanPoint)
require.NoError(t.t, err)
_, err = waitForNTxsInMempool(
net.Miner.Node, expectedTxes, minerMempoolTimeout,
)
require.NoError(t.t, err)
closeTx := getSpendingTxInMempool(
t, net.Miner.Node, minerMempoolTimeout, wire.OutPoint{
Hash: *bobFundingTxid,
Index: bobChanPoint.OutputIndex,
},
)
closeTxid := closeTx.TxHash()
// Go through the closing transaction outputs, and make an index for the HTLC outputs.
successOuts := make(map[wire.OutPoint]struct{})
timeoutOuts := make(map[wire.OutPoint]struct{})
for i, txOut := range closeTx.TxOut {
op := wire.OutPoint{
Hash: closeTxid,
Index: uint32(i),
}
switch txOut.Value {
// If this HTLC goes towards Carol, Bob will claim it with a
// timeout Tx. In this case the value will be the invoice
// amount.
case invoiceAmt:
timeoutOuts[op] = struct{}{}
// If the HTLC has direction towards Alice, Bob will
// claim it with the success TX when he learns the preimage. In
// this case one extra sat will be on the output, because of
// the routing fee.
case invoiceAmt + 1:
successOuts[op] = struct{}{}
}
}
// Mine a block to confirm the closing transaction.
mineBlocks(t, net, 1, expectedTxes)
time.Sleep(1 * time.Second)
// Let Alice settle her invoices. When Bob now gets the preimages, he
// has no other option than to broadcast his second-level transactions
// to claim the money.
for _, preimage := range alicePreimages {
ctx, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
_, err = alice.SettleInvoice(ctx, &invoicesrpc.SettleInvoiceMsg{
Preimage: preimage[:],
})
require.NoError(t.t, err)
}
// With the closing transaction confirmed, we should expect Bob's HTLC
// timeout transactions to be broadcast due to the expiry being reached.
// We will also expect the success transactions, since he learnt the
// preimages from Alice. We also expect Carol to sweep her commitment
// output.
expectedTxes = 2*numInvoices + 1
// In case of anchors, all success transactions will be aggregated into
// one, the same is the case for the timeout transactions. In this case
// Carol will also sweep her anchor output in a separate tx (since it
// will be low fee).
if c == commitTypeAnchors {
expectedTxes = 4
}
txes, err := getNTxsFromMempool(
net.Miner.Node, expectedTxes, minerMempoolTimeout,
)
require.NoError(t.t, err)
// Since Bob can aggregate the transactions, we expect a single
// transaction, that have multiple spends from the commitment.
var (
timeoutTxs []*chainhash.Hash
successTxs []*chainhash.Hash
)
for _, tx := range txes {
txid := tx.TxHash()
for i := range tx.TxIn {
prevOp := tx.TxIn[i].PreviousOutPoint
if _, ok := successOuts[prevOp]; ok {
successTxs = append(successTxs, &txid)
break
}
if _, ok := timeoutOuts[prevOp]; ok {
timeoutTxs = append(timeoutTxs, &txid)
break
}
}
}
// In case of anchor we expect all the timeout and success second
// levels to be aggregated into one tx. For earlier channel types, they
// will be separate transactions.
if c == commitTypeAnchors {
require.Len(t.t, timeoutTxs, 1)
require.Len(t.t, successTxs, 1)
} else {
require.Len(t.t, timeoutTxs, numInvoices)
require.Len(t.t, successTxs, numInvoices)
}
// All mempool transactions should be spending from the commitment
// transaction.
assertAllTxesSpendFrom(t, txes, closeTxid)
// Mine a block to confirm the transactions.
block := mineBlocks(t, net, 1, expectedTxes)[0]
require.Len(t.t, block.Transactions, expectedTxes+1)
// At this point, Bob should have broadcast his second layer success
// transaction, and should have sent it to the nursery for incubation,
// or to the sweeper for sweeping.
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err = waitForNumChannelPendingForceClose(
ctxt, bob, 1, func(c *lnrpcForceCloseChannel) error {
if c.Channel.LocalBalance != 0 {
return nil
}
if len(c.PendingHtlcs) != 1 {
return fmt.Errorf("bob should have pending " +
"htlc but doesn't")
}
if c.PendingHtlcs[0].Stage != 1 {
return fmt.Errorf("bob's htlc should have "+
"advanced to the first stage but was "+
"stage: %v", c.PendingHtlcs[0].Stage)
}
return nil
},
)
require.NoError(t.t, err)
// If we then mine additional blocks, Bob can sweep his commitment
// output.
_, err = net.Miner.Node.Generate(defaultCSV - 2)
require.NoError(t.t, err)
// Find the commitment sweep.
bobCommitSweepHash, err := waitForTxInMempool(net.Miner.Node, minerMempoolTimeout)
require.NoError(t.t, err)
bobCommitSweep, err := net.Miner.Node.GetRawTransaction(bobCommitSweepHash)
require.NoError(t.t, err)
require.Equal(
t.t, closeTxid, bobCommitSweep.MsgTx().TxIn[0].PreviousOutPoint.Hash,
)
// Also ensure it is not spending from any of the HTLC output.
for _, txin := range bobCommitSweep.MsgTx().TxIn {
for _, timeoutTx := range timeoutTxs {
if *timeoutTx == txin.PreviousOutPoint.Hash {
t.Fatalf("found unexpected spend of timeout tx")
}
}
for _, successTx := range successTxs {
if *successTx == txin.PreviousOutPoint.Hash {
t.Fatalf("found unexpected spend of success tx")
}
}
}
switch {
// Mining one additional block, Bob's second level tx is mature, and he
// can sweep the output.
case c == commitTypeAnchors:
_ = mineBlocks(t, net, 1, 1)
// In case this is a non-anchor channel type, we must mine 2 blocks, as
// the nursery waits an extra block before sweeping.
default:
_ = mineBlocks(t, net, 2, 1)
}
bobSweep, err := waitForTxInMempool(net.Miner.Node, minerMempoolTimeout)
require.NoError(t.t, err)
// Make sure it spends from the second level tx.
secondLevelSweep, err := net.Miner.Node.GetRawTransaction(bobSweep)
require.NoError(t.t, err)
// It should be sweeping all the second-level outputs.
var secondLvlSpends int
for _, txin := range secondLevelSweep.MsgTx().TxIn {
for _, timeoutTx := range timeoutTxs {
if *timeoutTx == txin.PreviousOutPoint.Hash {
secondLvlSpends++
}
}
for _, successTx := range successTxs {
if *successTx == txin.PreviousOutPoint.Hash {
secondLvlSpends++
}
}
}
require.Equal(t.t, 2*numInvoices, secondLvlSpends)
// When we mine one additional block, that will confirm Bob's second
// level sweep. Now Bob should have no pending channels anymore, as
// this just resolved it by the confirmation of the sweep transaction.
block = mineBlocks(t, net, 1, 1)[0]
assertTxInBlock(t, block, bobSweep)
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err = waitForNumChannelPendingForceClose(ctxt, bob, 0, nil)
require.NoError(t.t, err)
// THe channel with Alice is still open.
assertNodeNumChannels(t, bob, 1)
// Carol should have no channels left (open nor pending).
err = waitForNumChannelPendingForceClose(ctxt, carol, 0, nil)
require.NoError(t.t, err)
assertNodeNumChannels(t, carol, 0)
// Coop close channel, expect no anchors.
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
closeChannelAndAssertType(
ctxt, t, net, alice, aliceChanPoint, false, false,
)
}

View File

@ -184,8 +184,24 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest,
block = mineBlocks(t, net, 1, expectedTxes)[0]
require.Len(t.t, block.Transactions, expectedTxes+1)
var secondLevelMaturity uint32
switch c {
// If this is a channel of the anchor type, we will subtract one block
// from the default CSV, as the Sweeper will handle the input, and the Sweeper
// sweeps the input as soon as the lock expires.
case commitTypeAnchors:
secondLevelMaturity = defaultCSV - 1
// For non-anchor channel types, the nursery will handle sweeping the
// second level output, and it will wait one extra block before
// sweeping it.
default:
secondLevelMaturity = defaultCSV
}
// Keep track of the second level tx maturity.
carolSecondLevelCSV := uint32(defaultCSV)
carolSecondLevelCSV := secondLevelMaturity
// When Bob notices Carol's second level transaction in the block, he
// will extract the preimage and broadcast a second level tx to claim
@ -236,7 +252,7 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest,
// Keep track of Bob's second level maturity, and decrement our track
// of Carol's.
bobSecondLevelCSV := uint32(defaultCSV)
bobSecondLevelCSV := secondLevelMaturity
carolSecondLevelCSV--
// Now that the preimage from Bob has hit the chain, restart Alice to

View File

@ -43,7 +43,7 @@ func testMultiHopHtlcLocalTimeout(net *lntest.NetworkHarness, t *harnessTest,
// while the second will be a proper fully valued HTLC.
const (
dustHtlcAmt = btcutil.Amount(100)
htlcAmt = btcutil.Amount(30000)
htlcAmt = btcutil.Amount(300_000)
finalCltvDelta = 40
)

View File

@ -36,7 +36,7 @@ func testMultiHopLocalForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness,
// opens up the base for out tests.
const (
finalCltvDelta = 40
htlcAmt = btcutil.Amount(30000)
htlcAmt = btcutil.Amount(300_000)
)
ctx, cancel := context.WithCancel(ctxb)
defer cancel()

View File

@ -61,6 +61,11 @@ func testMultiHopHtlcClaims(net *lntest.NetworkHarness, t *harnessTest) {
name: "remote chain claim",
test: testMultiHopHtlcRemoteChainClaim,
},
{
// bob: outgoing and incoming, sweep all on chain
name: "local htlc aggregation",
test: testMultiHopHtlcAggregation,
},
}
commitTypes := []commitType{

View File

@ -3640,6 +3640,16 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
htlcCsvMaturityHeight = padCLTV(startHeight + defaultCLTV + 1 + defaultCSV)
)
// If we are dealing with an anchor channel type, the sweeper will
// sweep the HTLC second level output one block earlier (than the
// nursery that waits an additional block, and handles non-anchor
// channels). So we set a maturity height that is one less.
if channelType == commitTypeAnchors {
htlcCsvMaturityHeight = padCLTV(
startHeight + defaultCLTV + defaultCSV,
)
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
aliceChan, err := getChanInfo(ctxt, alice)
if err != nil {
@ -4157,79 +4167,125 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
// Since Alice had numInvoices (6) htlcs extended to Carol before force
// closing, we expect Alice to broadcast an htlc timeout txn for each
// one. Wait for them all to show up in the mempool.
htlcTxIDs, err := waitForNTxsInMempool(net.Miner.Node, numInvoices,
minerMempoolTimeout)
// one.
expectedTxes = numInvoices
// In case of anchors, the timeout txs will be aggregated into one.
if channelType == commitTypeAnchors {
expectedTxes = 1
}
// Wait for them all to show up in the mempool.
htlcTxIDs, err := waitForNTxsInMempool(
net.Miner.Node, expectedTxes, minerMempoolTimeout,
)
if err != nil {
t.Fatalf("unable to find htlc timeout txns in mempool: %v", err)
}
// Retrieve each htlc timeout txn from the mempool, and ensure it is
// well-formed. This entails verifying that each only spends from
// output, and that that output is from the commitment txn. We do not
// the sweeper check for these timeout transactions because they are
// not swept by the sweeper; the nursery broadcasts the pre-signed
// transaction.
// output, and that that output is from the commitment txn. In case
// this is an anchor channel, the transactions are aggregated by the
// sweeper into one.
numInputs := 1
if channelType == commitTypeAnchors {
numInputs = numInvoices + 1
}
// Construct a map of the already confirmed htlc timeout outpoints,
// that will count the number of times each is spent by the sweep txn.
// We prepopulate it in this way so that we can later detect if we are
// spending from an output that was not a confirmed htlc timeout txn.
var htlcTxOutpointSet = make(map[wire.OutPoint]int)
var htlcLessFees uint64
for _, htlcTxID := range htlcTxIDs {
// Fetch the sweep transaction, all input it's spending should
// be from the commitment transaction which was broadcast
// on-chain.
// on-chain. In case of an anchor type channel, we expect one
// extra input that is not spending from the commitment, that
// is added for fees.
htlcTx, err := net.Miner.Node.GetRawTransaction(htlcTxID)
if err != nil {
t.Fatalf("unable to fetch sweep tx: %v", err)
}
// Ensure the htlc transaction only has one input.
// Ensure the htlc transaction has the expected number of
// inputs.
inputs := htlcTx.MsgTx().TxIn
if len(inputs) != 1 {
t.Fatalf("htlc transaction should only have one txin, "+
"has %d", len(htlcTx.MsgTx().TxIn))
}
// Ensure the htlc transaction is spending from the commitment
// transaction.
txIn := inputs[0]
if !closingTxID.IsEqual(&txIn.PreviousOutPoint.Hash) {
t.Fatalf("htlc transaction not spending from commit "+
"tx %v, instead spending %v",
closingTxID, txIn.PreviousOutPoint)
if len(inputs) != numInputs {
t.Fatalf("htlc transaction should only have %d txin, "+
"has %d", numInputs, len(htlcTx.MsgTx().TxIn))
}
// The number of outputs should be the same.
outputs := htlcTx.MsgTx().TxOut
if len(outputs) != 1 {
t.Fatalf("htlc transaction should only have one "+
"txout, has: %v", len(outputs))
if len(outputs) != numInputs {
t.Fatalf("htlc transaction should only have %d"+
"txout, has: %v", numInputs, len(outputs))
}
// For each htlc timeout transaction, we expect a resolver
// report recording this on chain resolution for both alice and
// carol.
outpoint := txIn.PreviousOutPoint
resolutionOutpoint := &lnrpc.OutPoint{
TxidBytes: outpoint.Hash[:],
TxidStr: outpoint.Hash.String(),
OutputIndex: outpoint.Index,
}
// Ensure all the htlc transaction inputs are spending from the
// commitment transaction, except if this is an extra input
// added to pay for fees for anchor channels.
nonCommitmentInputs := 0
for i, txIn := range inputs {
if !closingTxID.IsEqual(&txIn.PreviousOutPoint.Hash) {
nonCommitmentInputs++
// We expect alice to have a timeout tx resolution with an
// amount equal to the payment amount.
aliceReports[outpoint.String()] = &lnrpc.Resolution{
ResolutionType: lnrpc.ResolutionType_OUTGOING_HTLC,
Outcome: lnrpc.ResolutionOutcome_FIRST_STAGE,
SweepTxid: htlcTx.Hash().String(),
Outpoint: resolutionOutpoint,
AmountSat: uint64(paymentAmt),
}
if nonCommitmentInputs > 1 {
t.Fatalf("htlc transaction not "+
"spending from commit "+
"tx %v, instead spending %v",
closingTxID,
txIn.PreviousOutPoint)
}
// We expect carol to have a resolution with an incoming htlc
// timeout which reflects the full amount of the htlc. It has
// no spend tx, because carol stops monitoring the htlc once
// it has timed out.
carolReports[outpoint.String()] = &lnrpc.Resolution{
ResolutionType: lnrpc.ResolutionType_INCOMING_HTLC,
Outcome: lnrpc.ResolutionOutcome_TIMEOUT,
SweepTxid: "",
Outpoint: resolutionOutpoint,
AmountSat: uint64(paymentAmt),
// This was an extra input added to pay fees,
// continue to the next one.
continue
}
// For each htlc timeout transaction, we expect a
// resolver report recording this on chain resolution
// for both alice and carol.
outpoint := txIn.PreviousOutPoint
resolutionOutpoint := &lnrpc.OutPoint{
TxidBytes: outpoint.Hash[:],
TxidStr: outpoint.Hash.String(),
OutputIndex: outpoint.Index,
}
// We expect alice to have a timeout tx resolution with
// an amount equal to the payment amount.
aliceReports[outpoint.String()] = &lnrpc.Resolution{
ResolutionType: lnrpc.ResolutionType_OUTGOING_HTLC,
Outcome: lnrpc.ResolutionOutcome_FIRST_STAGE,
SweepTxid: htlcTx.Hash().String(),
Outpoint: resolutionOutpoint,
AmountSat: uint64(paymentAmt),
}
// We expect carol to have a resolution with an
// incoming htlc timeout which reflects the full amount
// of the htlc. It has no spend tx, because carol stops
// monitoring the htlc once it has timed out.
carolReports[outpoint.String()] = &lnrpc.Resolution{
ResolutionType: lnrpc.ResolutionType_INCOMING_HTLC,
Outcome: lnrpc.ResolutionOutcome_TIMEOUT,
SweepTxid: "",
Outpoint: resolutionOutpoint,
AmountSat: uint64(paymentAmt),
}
// Recorf the HTLC outpoint, such that we can later
// check whether it gets swept
op := wire.OutPoint{
Hash: *htlcTxID,
Index: uint32(i),
}
htlcTxOutpointSet[op] = 0
}
// We record the htlc amount less fees here, so that we know
@ -4260,7 +4316,13 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
}
// Advance the chain until just before the 2nd-layer CSV delays expire.
blockHash, err = net.Miner.Node.Generate(defaultCSV - 1)
// For anchor channels thhis is one block earlier.
numBlocks := uint32(defaultCSV - 1)
if channelType == commitTypeAnchors {
numBlocks = defaultCSV - 2
}
_, err = net.Miner.Node.Generate(numBlocks)
if err != nil {
t.Fatalf("unable to generate block: %v", err)
}
@ -4327,15 +4389,6 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
t.Fatalf("failed to get sweep tx from mempool: %v", err)
}
// Construct a map of the already confirmed htlc timeout txids, that
// will count the number of times each is spent by the sweep txn. We
// prepopulate it in this way so that we can later detect if we are
// spending from an output that was not a confirmed htlc timeout txn.
var htlcTxIDSet = make(map[chainhash.Hash]int)
for _, htlcTxID := range htlcTxIDs {
htlcTxIDSet[*htlcTxID] = 0
}
// Fetch the htlc sweep transaction from the mempool.
htlcSweepTx, err := net.Miner.Node.GetRawTransaction(htlcSweepTxID)
if err != nil {
@ -4353,19 +4406,19 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
"%v", outputCount)
}
// Ensure that each output spends from exactly one htlc timeout txn.
// Ensure that each output spends from exactly one htlc timeout output.
for _, txIn := range htlcSweepTx.MsgTx().TxIn {
outpoint := txIn.PreviousOutPoint.Hash
outpoint := txIn.PreviousOutPoint
// Check that the input is a confirmed htlc timeout txn.
if _, ok := htlcTxIDSet[outpoint]; !ok {
if _, ok := htlcTxOutpointSet[outpoint]; !ok {
t.Fatalf("htlc sweep output not spending from htlc "+
"tx, instead spending output %v", outpoint)
}
// Increment our count for how many times this output was spent.
htlcTxIDSet[outpoint]++
htlcTxOutpointSet[outpoint]++
// Check that each is only spent once.
if htlcTxIDSet[outpoint] > 1 {
if htlcTxOutpointSet[outpoint] > 1 {
t.Fatalf("htlc sweep tx has multiple spends from "+
"outpoint %v", outpoint)
}
@ -4386,6 +4439,13 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
}
}
// Check that each HTLC output was spent exactly onece.
for op, num := range htlcTxOutpointSet {
if num != 1 {
t.Fatalf("HTLC outpoint %v was spent %v times", op, num)
}
}
// Check that we can find the htlc sweep in our set of sweeps using
// the verbose output of the listsweeps output.
assertSweepFound(ctxb, t.t, alice, htlcSweepTx.Hash().String(), true)
@ -11019,11 +11079,12 @@ func assertActiveHtlcs(nodes []*lntest.HarnessNode, payHashes ...[]byte) error {
// Record all payment hashes active for this channel.
htlcHashes := make(map[string]struct{})
for _, htlc := range channel.PendingHtlcs {
_, ok := htlcHashes[string(htlc.HashLock)]
h := hex.EncodeToString(htlc.HashLock)
_, ok := htlcHashes[h]
if ok {
return fmt.Errorf("duplicate HashLock")
}
htlcHashes[string(htlc.HashLock)] = struct{}{}
htlcHashes[h] = struct{}{}
}
// Channel should have exactly the payHashes active.
@ -11035,12 +11096,13 @@ func assertActiveHtlcs(nodes []*lntest.HarnessNode, payHashes ...[]byte) error {
// Make sure all the payHashes are active.
for _, payHash := range payHashes {
if _, ok := htlcHashes[string(payHash)]; ok {
h := hex.EncodeToString(payHash)
if _, ok := htlcHashes[h]; ok {
continue
}
return fmt.Errorf("node %x didn't have the "+
"payHash %v active", node.PubKey[:],
payHash)
h)
}
}
}

View File

@ -3034,16 +3034,15 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
// Finally, we'll generate a sign descriptor to generate a
// signature to give to the remote party for this commitment
// transaction. Note we use the raw HTLC amount.
txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex]
sigJob.SignDesc = input.SignDescriptor{
KeyDesc: localChanCfg.HtlcBasePoint,
SingleTweak: keyRing.LocalHtlcKeyTweak,
WitnessScript: htlc.theirWitnessScript,
Output: &wire.TxOut{
Value: int64(htlc.Amount.ToSatoshis()),
},
HashType: sigHashType,
SigHashes: txscript.NewTxSigHashes(sigJob.Tx),
InputIndex: 0,
Output: txOut,
HashType: sigHashType,
SigHashes: txscript.NewTxSigHashes(sigJob.Tx),
InputIndex: 0,
}
sigJob.OutputIndex = htlc.remoteOutputIndex
@ -3087,16 +3086,15 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
// Finally, we'll generate a sign descriptor to generate a
// signature to give to the remote party for this commitment
// transaction. Note we use the raw HTLC amount.
txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex]
sigJob.SignDesc = input.SignDescriptor{
KeyDesc: localChanCfg.HtlcBasePoint,
SingleTweak: keyRing.LocalHtlcKeyTweak,
WitnessScript: htlc.theirWitnessScript,
Output: &wire.TxOut{
Value: int64(htlc.Amount.ToSatoshis()),
},
HashType: sigHashType,
SigHashes: txscript.NewTxSigHashes(sigJob.Tx),
InputIndex: 0,
Output: txOut,
HashType: sigHashType,
SigHashes: txscript.NewTxSigHashes(sigJob.Tx),
InputIndex: 0,
}
sigJob.OutputIndex = htlc.remoteOutputIndex
@ -5396,7 +5394,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si
htlcResolutions, err := extractHtlcResolutions(
chainfee.SatPerKWeight(remoteCommit.FeePerKw), false, signer,
remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg,
&chanState.RemoteChanCfg, *commitSpend.SpenderTxHash,
&chanState.RemoteChanCfg, commitSpend.SpendingTx,
chanState.ChanType,
)
if err != nil {
@ -5519,6 +5517,14 @@ type IncomingHtlcResolution struct {
// claimed directly from the outpoint listed below.
SignedSuccessTx *wire.MsgTx
// SignDetails is non-nil if SignedSuccessTx is non-nil, and the
// channel is of the anchor type. As the above HTLC transaction will be
// signed by the channel peer using SINGLE|ANYONECANPAY for such
// channels, we can use the sign details to add the input-output pair
// of the HTLC transaction to another transaction, thereby aggregating
// multiple HTLC transactions together, and adding fees as needed.
SignDetails *input.SignDetails
// CsvDelay is the relative time lock (expressed in blocks) that must
// pass after the SignedSuccessTx is confirmed in the chain before the
// output can be swept.
@ -5560,6 +5566,14 @@ type OutgoingHtlcResolution struct {
// claimed directly from the outpoint listed below.
SignedTimeoutTx *wire.MsgTx
// SignDetails is non-nil if SignedTimeoutTx is non-nil, and the
// channel is of the anchor type. As the above HTLC transaction will be
// signed by the channel peer using SINGLE|ANYONECANPAY for such
// channels, we can use the sign details to add the input-output pair
// of the HTLC transaction to another transaction, thereby aggregating
// multiple HTLC transactions together, and adding fees as needed.
SignDetails *input.SignDetails
// CsvDelay is the relative time lock (expressed in blocks) that must
// pass after the SignedTimeoutTx is confirmed in the chain before the
// output can be swept.
@ -5599,13 +5613,13 @@ type HtlcResolutions struct {
// allowing the caller to sweep an outgoing HTLC present on either their, or
// the remote party's commitment transaction.
func newOutgoingHtlcResolution(signer input.Signer,
localChanCfg *channeldb.ChannelConfig, commitHash chainhash.Hash,
localChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx,
htlc *channeldb.HTLC, keyRing *CommitmentKeyRing,
feePerKw chainfee.SatPerKWeight, csvDelay uint32,
localCommit bool, chanType channeldb.ChannelType) (*OutgoingHtlcResolution, error) {
op := wire.OutPoint{
Hash: commitHash,
Hash: commitTx.TxHash(),
Index: uint32(htlc.OutputIndex),
}
@ -5664,16 +5678,15 @@ func newOutgoingHtlcResolution(signer input.Signer,
// With the transaction created, we can generate a sign descriptor
// that's capable of generating the signature required to spend the
// HTLC output using the timeout transaction.
txOut := commitTx.TxOut[htlc.OutputIndex]
timeoutSignDesc := input.SignDescriptor{
KeyDesc: localChanCfg.HtlcBasePoint,
SingleTweak: keyRing.LocalHtlcKeyTweak,
WitnessScript: htlcScript,
Output: &wire.TxOut{
Value: int64(htlc.Amt.ToSatoshis()),
},
HashType: txscript.SigHashAll,
SigHashes: txscript.NewTxSigHashes(timeoutTx),
InputIndex: 0,
Output: txOut,
HashType: txscript.SigHashAll,
SigHashes: txscript.NewTxSigHashes(timeoutTx),
InputIndex: 0,
}
htlcSig, err := btcec.ParseDERSignature(htlc.Signature, btcec.S256())
@ -5692,6 +5705,12 @@ func newOutgoingHtlcResolution(signer input.Signer,
}
timeoutTx.TxIn[0].Witness = timeoutWitness
// If this is an anchor type channel, the sign details will let us
// re-sign an aggregated tx later.
txSignDetails := HtlcSignDetails(
chanType, timeoutSignDesc, sigHashType, htlcSig,
)
// Finally, we'll generate the script output that the timeout
// transaction creates so we can generate the signDesc required to
// complete the claim process after a delay period.
@ -5712,6 +5731,7 @@ func newOutgoingHtlcResolution(signer input.Signer,
return &OutgoingHtlcResolution{
Expiry: htlc.RefundTimeout,
SignedTimeoutTx: timeoutTx,
SignDetails: txSignDetails,
CsvDelay: csvDelay,
ClaimOutpoint: wire.OutPoint{
Hash: timeoutTx.TxHash(),
@ -5738,13 +5758,13 @@ func newOutgoingHtlcResolution(signer input.Signer,
//
// TODO(roasbeef) consolidate code with above func
func newIncomingHtlcResolution(signer input.Signer,
localChanCfg *channeldb.ChannelConfig, commitHash chainhash.Hash,
localChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx,
htlc *channeldb.HTLC, keyRing *CommitmentKeyRing,
feePerKw chainfee.SatPerKWeight, csvDelay uint32, localCommit bool,
chanType channeldb.ChannelType) (*IncomingHtlcResolution, error) {
op := wire.OutPoint{
Hash: commitHash,
Hash: commitTx.TxHash(),
Index: uint32(htlc.OutputIndex),
}
@ -5795,16 +5815,15 @@ func newIncomingHtlcResolution(signer input.Signer,
// Once we've created the second-level transaction, we'll generate the
// SignDesc needed spend the HTLC output using the success transaction.
txOut := commitTx.TxOut[htlc.OutputIndex]
successSignDesc := input.SignDescriptor{
KeyDesc: localChanCfg.HtlcBasePoint,
SingleTweak: keyRing.LocalHtlcKeyTweak,
WitnessScript: htlcScript,
Output: &wire.TxOut{
Value: int64(htlc.Amt.ToSatoshis()),
},
HashType: txscript.SigHashAll,
SigHashes: txscript.NewTxSigHashes(successTx),
InputIndex: 0,
Output: txOut,
HashType: txscript.SigHashAll,
SigHashes: txscript.NewTxSigHashes(successTx),
InputIndex: 0,
}
htlcSig, err := btcec.ParseDERSignature(htlc.Signature, btcec.S256())
@ -5825,6 +5844,12 @@ func newIncomingHtlcResolution(signer input.Signer,
}
successTx.TxIn[0].Witness = successWitness
// If this is an anchor type channel, the sign details will let us
// re-sign an aggregated tx later.
txSignDetails := HtlcSignDetails(
chanType, successSignDesc, sigHashType, htlcSig,
)
// Finally, we'll generate the script that the second-level transaction
// creates so we can generate the proper signDesc to sweep it after the
// CSV delay has passed.
@ -5844,6 +5869,7 @@ func newIncomingHtlcResolution(signer input.Signer,
)
return &IncomingHtlcResolution{
SignedSuccessTx: successTx,
SignDetails: txSignDetails,
CsvDelay: csvDelay,
ClaimOutpoint: wire.OutPoint{
Hash: successTx.TxHash(),
@ -5892,7 +5918,7 @@ func (r *OutgoingHtlcResolution) HtlcPoint() wire.OutPoint {
func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool,
signer input.Signer, htlcs []channeldb.HTLC, keyRing *CommitmentKeyRing,
localChanCfg, remoteChanCfg *channeldb.ChannelConfig,
commitHash chainhash.Hash, chanType channeldb.ChannelType) (
commitTx *wire.MsgTx, chanType channeldb.ChannelType) (
*HtlcResolutions, error) {
// TODO(roasbeef): don't need to swap csv delay?
@ -5924,7 +5950,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool,
// Otherwise, we'll create an incoming HTLC resolution
// as we can satisfy the contract.
ihr, err := newIncomingHtlcResolution(
signer, localChanCfg, commitHash, &htlc,
signer, localChanCfg, commitTx, &htlc,
keyRing, feePerKw, uint32(csvDelay), ourCommit,
chanType,
)
@ -5937,7 +5963,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool,
}
ohr, err := newOutgoingHtlcResolution(
signer, localChanCfg, commitHash, &htlc, keyRing,
signer, localChanCfg, commitTx, &htlc, keyRing,
feePerKw, uint32(csvDelay), ourCommit, chanType,
)
if err != nil {
@ -6138,12 +6164,11 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
// outgoing HTLC's that we'll need to claim as well. If this is after
// recovery there is not much we can do with HTLCs, so we'll always
// use what we have in our latest state when extracting resolutions.
txHash := commitTx.TxHash()
localCommit := chanState.LocalCommitment
htlcResolutions, err := extractHtlcResolutions(
chainfee.SatPerKWeight(localCommit.FeePerKw), true, signer,
localCommit.Htlcs, keyRing, &chanState.LocalChanCfg,
&chanState.RemoteChanCfg, txHash, chanState.ChanType,
&chanState.RemoteChanCfg, commitTx, chanState.ChanType,
)
if err != nil {
return nil, err

View File

@ -235,6 +235,24 @@ func HtlcSigHashType(chanType channeldb.ChannelType) txscript.SigHashType {
return txscript.SigHashAll
}
// HtlcSignDetails converts the passed parameters to a SignDetails valid for
// this channel type. For non-anchor channels this will return nil.
func HtlcSignDetails(chanType channeldb.ChannelType, signDesc input.SignDescriptor,
sigHash txscript.SigHashType, peerSig input.Signature) *input.SignDetails {
// Non-anchor channels don't need sign details, as the HTLC second
// level cannot be altered.
if !chanType.HasAnchors() {
return nil
}
return &input.SignDetails{
SignDesc: signDesc,
SigHashType: sigHash,
PeerSig: peerSig,
}
}
// HtlcSecondLevelInputSequence dictates the sequence number we must use on the
// input to a second level HTLC transaction.
func HtlcSecondLevelInputSequence(chanType channeldb.ChannelType) uint32 {

View File

@ -13,5 +13,5 @@ var (
//
// To speed up integration tests waiting for a sweep to happen, the
// batch window is shortened.
DefaultBatchWindowDuration = 2 * time.Second
DefaultBatchWindowDuration = 8 * time.Second
)

View File

@ -293,10 +293,10 @@ func (t *txInputSet) add(input input.Input, constraints addConstraints) bool {
// minimizing any negative externalities we cause for the Bitcoin system as a
// whole.
func (t *txInputSet) addPositiveYieldInputs(sweepableInputs []txInput) {
for _, input := range sweepableInputs {
for i, inp := range sweepableInputs {
// Apply relaxed constraints for force sweeps.
constraints := constraintsRegular
if input.parameters().Force {
if inp.parameters().Force {
constraints = constraintsForce
}
@ -304,16 +304,26 @@ func (t *txInputSet) addPositiveYieldInputs(sweepableInputs []txInput) {
// succeed because it wouldn't increase the output value,
// return. Assuming inputs are sorted by yield, any further
// inputs wouldn't increase the output value either.
if !t.add(input, constraints) {
if !t.add(inp, constraints) {
var rem []input.Input
for j := i; j < len(sweepableInputs); j++ {
rem = append(rem, sweepableInputs[j])
}
log.Debugf("%d negative yield inputs not added to "+
"input set: %v", len(rem),
inputTypeSummary(rem))
return
}
log.Debugf("Added positive yield input %v to input set",
inputTypeSummary([]input.Input{inp}))
}
// We managed to add all inputs to the set.
}
// tryAddWalletInputsIfNeeded retrieves utxos from the wallet and tries adding as
// many as required to bring the tx output value above the given minimum.
// tryAddWalletInputsIfNeeded retrieves utxos from the wallet and tries adding
// as many as required to bring the tx output value above the given minimum.
func (t *txInputSet) tryAddWalletInputsIfNeeded() error {
// If we've already have enough to pay the transaction fees and have at
// least one output materialize, no action is needed.

View File

@ -222,6 +222,9 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte,
PkScript: outputPkScript,
Value: int64(changeAmt),
})
} else {
log.Infof("Change amt %v below dustlimit %v, not adding "+
"change output", changeAmt, dustLimit)
}
// We'll default to using the current block height as locktime, if none