diff --git a/breacharbiter.go b/breacharbiter.go index a7ad89a4..6aaf00fb 100644 --- a/breacharbiter.go +++ b/breacharbiter.go @@ -25,6 +25,21 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) +const ( + // justiceTxConfTarget is the number of blocks we'll use as a + // confirmation target when creating the justice transaction. We'll + // choose an aggressive target, since we want to be sure it confirms + // quickly. + justiceTxConfTarget = 2 + + // blocksPassedSplitPublish is the number of blocks without + // confirmation of the justice tx we'll wait before starting to publish + // smaller variants of the justice tx. We do this to mitigate an attack + // the channel peer can do by pinning the HTLC outputs of the + // commitment with low-fee HTLC transactions. + blocksPassedSplitPublish = 4 +) + var ( // retributionBucket stores retribution state on disk between detecting // a contract breach, broadcasting a justice transaction that sweeps the @@ -318,24 +333,22 @@ func convertToSecondLevelRevoke(bo *breachedOutput, breachInfo *retributionInfo, bo.outpoint) } +// spend is used to wrap the index of the retributionInfo output that gets +// spent together with the spend details. +type spend struct { + index int + detail *chainntnfs.SpendDetail +} + // waitForSpendEvent waits for any of the breached outputs to get spent, and -// mutates the breachInfo to be able to sweep it. This method should be used -// when we fail to publish the justice tx because of a double spend, indicating -// that the counter party has taken one of the breached outputs to the second -// level. The spendNtfns map is a cache used to store registered spend -// subscriptions, in case we must call this method multiple times. +// returns the spend details for those outputs. The spendNtfns map is a cache +// used to store registered spend subscriptions, in case we must call this +// method multiple times. func (b *breachArbiter) waitForSpendEvent(breachInfo *retributionInfo, - spendNtfns map[wire.OutPoint]*chainntnfs.SpendEvent) error { + spendNtfns map[wire.OutPoint]*chainntnfs.SpendEvent) ([]spend, error) { inputs := breachInfo.breachedOutputs - // spend is used to wrap the index of the output that gets spent - // together with the spend details. - type spend struct { - index int - detail *chainntnfs.SpendDetail - } - // We create a channel the first goroutine that gets a spend event can // signal. We make it buffered in case multiple spend events come in at // the same time. @@ -378,7 +391,7 @@ func (b *breachArbiter) waitForSpendEvent(breachInfo *retributionInfo, // to avoid entering an infinite loop. select { case <-b.quit: - return errBrarShuttingDown + return nil, errBrarShuttingDown default: continue } @@ -438,62 +451,115 @@ func (b *breachArbiter) waitForSpendEvent(breachInfo *retributionInfo, // channel before ranging over its content. close(allSpends) - doneOutputs := make(map[int]struct{}) + // Gather all detected spends and return them. + var spends []spend for s := range allSpends { breachedOutput := &inputs[s.index] delete(spendNtfns, breachedOutput.outpoint) - switch breachedOutput.witnessType { - case input.HtlcAcceptedRevoke: - fallthrough - case input.HtlcOfferedRevoke: - brarLog.Infof("Spend on second-level"+ - "%s(%v) for ChannelPoint(%v) "+ - "transitions to second-level output", - breachedOutput.witnessType, - breachedOutput.outpoint, - breachInfo.chanPoint) + spends = append(spends, s) + } - // In this case we'll morph our initial revoke - // spend to instead point to the second level - // output, and update the sign descriptor in the - // process. - convertToSecondLevelRevoke( - breachedOutput, breachInfo, s.detail, - ) + return spends, nil - continue + case <-b.quit: + return nil, errBrarShuttingDown + } +} + +// updateBreachInfo mutates the passed breachInfo by removing or converting any +// outputs among the spends. It also counts the total and revoked funds swept +// by our justice spends. +func updateBreachInfo(breachInfo *retributionInfo, spends []spend) ( + btcutil.Amount, btcutil.Amount) { + + inputs := breachInfo.breachedOutputs + doneOutputs := make(map[int]struct{}) + + var totalFunds, revokedFunds btcutil.Amount + for _, s := range spends { + breachedOutput := &inputs[s.index] + txIn := s.detail.SpendingTx.TxIn[s.detail.SpenderInputIndex] + + switch breachedOutput.witnessType { + case input.HtlcAcceptedRevoke: + fallthrough + case input.HtlcOfferedRevoke: + // If the HTLC output was spent using the revocation + // key, it is our own spend, and we can forget the + // output. Otherwise it has been taken to the second + // level. + signDesc := &breachedOutput.signDesc + ok, err := input.IsHtlcSpendRevoke(txIn, signDesc) + if err != nil { + brarLog.Errorf("Unable to determine if "+ + "revoke spend: %v", err) + break } - brarLog.Infof("Spend on %s(%v) for ChannelPoint(%v) "+ - "transitions output to terminal state, "+ - "removing input from justice transaction", + if ok { + brarLog.Debugf("HTLC spend was our own " + + "revocation spend") + break + } + + brarLog.Infof("Spend on second-level "+ + "%s(%v) for ChannelPoint(%v) "+ + "transitions to second-level output", breachedOutput.witnessType, breachedOutput.outpoint, breachInfo.chanPoint) - doneOutputs[s.index] = struct{}{} + // In this case we'll morph our initial revoke + // spend to instead point to the second level + // output, and update the sign descriptor in the + // process. + convertToSecondLevelRevoke( + breachedOutput, breachInfo, s.detail, + ) + + continue } - // Filter the inputs for which we can no longer proceed. - var nextIndex int - for i := range inputs { - if _, ok := doneOutputs[i]; ok { - continue - } + // Now that we have determined the spend is done by us, we + // count the total and revoked funds swept depending on the + // input type. + switch breachedOutput.witnessType { - inputs[nextIndex] = inputs[i] - nextIndex++ + // If the output being revoked is the remote commitment + // output or an offered HTLC output, it's amount + // contributes to the value of funds being revoked from + // the counter party. + case input.CommitmentRevoke, input.HtlcSecondLevelRevoke, + input.HtlcOfferedRevoke: + + revokedFunds += breachedOutput.Amount() } - // Update our remaining set of outputs before continuing with - // another attempt at publication. - breachInfo.breachedOutputs = inputs[:nextIndex] + totalFunds += breachedOutput.Amount() + brarLog.Infof("Spend on %s(%v) for ChannelPoint(%v) "+ + "transitions output to terminal state, "+ + "removing input from justice transaction", + breachedOutput.witnessType, + breachedOutput.outpoint, breachInfo.chanPoint) - case <-b.quit: - return errBrarShuttingDown + doneOutputs[s.index] = struct{}{} } - return nil + // Filter the inputs for which we can no longer proceed. + var nextIndex int + for i := range inputs { + if _, ok := doneOutputs[i]; ok { + continue + } + + inputs[nextIndex] = inputs[i] + nextIndex++ + } + + // Update our remaining set of outputs before continuing with + // another attempt at publication. + breachInfo.breachedOutputs = inputs[:nextIndex] + return totalFunds, revokedFunds } // exactRetribution is a goroutine which is executed once a contract breach has @@ -508,17 +574,14 @@ func (b *breachArbiter) exactRetribution(confChan *chainntnfs.ConfirmationEvent, defer b.wg.Done() // TODO(roasbeef): state needs to be checkpointed here - var breachConfHeight uint32 select { - case breachConf, ok := <-confChan.Confirmed: + case _, ok := <-confChan.Confirmed: // If the second value is !ok, then the channel has been closed // signifying a daemon shutdown, so we exit. if !ok { return } - breachConfHeight = breachConf.BlockHeight - // Otherwise, if this is a real confirmation notification, then // we fall through to complete our duty. case <-b.quit: @@ -533,37 +596,20 @@ func (b *breachArbiter) exactRetribution(confChan *chainntnfs.ConfirmationEvent, // SpendEvents between each attempt to not re-register uneccessarily. spendNtfns := make(map[wire.OutPoint]*chainntnfs.SpendEvent) - finalTx, err := b.cfg.Store.GetFinalizedTxn(&breachInfo.chanPoint) + // Compute both the total value of funds being swept and the + // amount of funds that were revoked from the counter party. + var totalFunds, revokedFunds btcutil.Amount + +justiceTxBroadcast: + // With the breach transaction confirmed, we now create the + // justice tx which will claim ALL the funds within the + // channel. + justiceTxs, err := b.createJusticeTx(breachInfo.breachedOutputs) if err != nil { - brarLog.Errorf("Unable to get finalized txn for"+ - "chanid=%v: %v", &breachInfo.chanPoint, err) + brarLog.Errorf("Unable to create justice tx: %v", err) return } - - // If this retribution has not been finalized before, we will first - // construct a sweep transaction and write it to disk. This will allow - // the breach arbiter to re-register for notifications for the justice - // txid. -justiceTxBroadcast: - if finalTx == nil { - // With the breach transaction confirmed, we now create the - // justice tx which will claim ALL the funds within the - // channel. - finalTx, err = b.createJusticeTx(breachInfo) - if err != nil { - brarLog.Errorf("Unable to create justice tx: %v", err) - return - } - - // Persist our finalized justice transaction before making an - // attempt to broadcast. - err := b.cfg.Store.Finalize(&breachInfo.chanPoint, finalTx) - if err != nil { - brarLog.Errorf("Unable to finalize justice tx for "+ - "chanid=%v: %v", &breachInfo.chanPoint, err) - return - } - } + finalTx := justiceTxs.spendAll brarLog.Debugf("Broadcasting justice tx: %v", newLogClosure(func() string { return spew.Sdump(finalTx) @@ -575,31 +621,63 @@ justiceTxBroadcast: err = b.cfg.PublishTransaction(finalTx, label) if err != nil { brarLog.Errorf("Unable to broadcast justice tx: %v", err) + } - if err == lnwallet.ErrDoubleSpend { - // Broadcasting the transaction failed because of a - // conflict either in the mempool or in chain. We'll - // now create spend subscriptions for all HTLC outputs - // on the commitment transaction that could possibly - // have been spent, and wait for any of them to - // trigger. - brarLog.Infof("Waiting for a spend event before " + - "attempting to craft new justice tx.") - finalTx = nil + // Regardless of publication succeeded or not, we now wait for any of + // the inputs to be spent. If any input got spent by the remote, we + // must recreate our justice transaction. + var ( + spendChan = make(chan []spend, 1) + errChan = make(chan error, 1) + wg sync.WaitGroup + ) - err := b.waitForSpendEvent(breachInfo, spendNtfns) - if err != nil { - if err != errBrarShuttingDown { - brarLog.Errorf("error waiting for "+ - "spend event: %v", err) - } - return - } + wg.Add(1) + go func() { + defer wg.Done() + + spends, err := b.waitForSpendEvent(breachInfo, spendNtfns) + if err != nil { + errChan <- err + return + } + spendChan <- spends + }() + + // We'll also register for block notifications, such that in case our + // justice tx doesn't confirm within a reasonable timeframe, we can + // start to more aggressively sweep the time sensitive outputs. + newBlockChan, err := b.cfg.Notifier.RegisterBlockEpochNtfn(nil) + if err != nil { + brarLog.Errorf("Unable to register for block notifications: %v", + err) + return + } + defer newBlockChan.Cancel() + +Loop: + for { + + select { + case spends := <-spendChan: + // Update the breach info with the new spends. + t, r := updateBreachInfo(breachInfo, spends) + totalFunds += t + revokedFunds += r + + brarLog.Infof("%v spends from breach tx for "+ + "ChannelPoint(%v) has been detected, %v "+ + "revoked funds (%v total) have been claimed", + len(spends), breachInfo.chanPoint, + revokedFunds, totalFunds) if len(breachInfo.breachedOutputs) == 0 { - brarLog.Debugf("No more outputs to sweep for "+ - "breach, marking ChannelPoint(%v) "+ - "fully resolved", breachInfo.chanPoint) + brarLog.Infof("Justice for ChannelPoint(%v) "+ + "has been served, %v revoked funds "+ + "(%v total) have been claimed. No "+ + "more outputs to sweep, marking fully "+ + "resolved", breachInfo.chanPoint, + revokedFunds, totalFunds) err = b.cleanupBreach(&breachInfo.chanPoint) if err != nil { @@ -607,78 +685,101 @@ justiceTxBroadcast: "breached ChannelPoint(%v): %v", breachInfo.chanPoint, err) } - return + + // TODO(roasbeef): add peer to blacklist? + + // TODO(roasbeef): close other active channels + // with offending peer + break Loop } brarLog.Infof("Attempting another justice tx "+ "with %d inputs", len(breachInfo.breachedOutputs)) + wg.Wait() goto justiceTxBroadcast - } - } - // As a conclusionary step, we register for a notification to be - // dispatched once the justice tx is confirmed. After confirmation we - // notify the caller that initiated the retribution workflow that the - // deed has been done. - justiceTXID := finalTx.TxHash() - justiceScript := finalTx.TxOut[0].PkScript - confChan, err = b.cfg.Notifier.RegisterConfirmationsNtfn( - &justiceTXID, justiceScript, 1, breachConfHeight, - ) - if err != nil { - brarLog.Errorf("Unable to register for conf for txid(%v): %v", - justiceTXID, err) - return - } - - select { - case _, ok := <-confChan.Confirmed: - if !ok { - return - } - - // Compute both the total value of funds being swept and the - // amount of funds that were revoked from the counter party. - var totalFunds, revokedFunds btcutil.Amount - for _, inp := range breachInfo.breachedOutputs { - totalFunds += inp.Amount() - - // If the output being revoked is the remote commitment - // output or an offered HTLC output, it's amount - // contributes to the value of funds being revoked from - // the counter party. - switch inp.WitnessType() { - case input.CommitmentRevoke: - revokedFunds += inp.Amount() - case input.HtlcOfferedRevoke: - revokedFunds += inp.Amount() - default: + // On every new block, we check whether we should republish the + // transactions. + case epoch, ok := <-newBlockChan.Epochs: + if !ok { + return } + + // If less than four blocks have passed since the + // breach confirmed, we'll continue waiting. It was + // published with a 2-block fee estimate, so it's not + // unexpected that four blocks without confirmation can + // pass. + splitHeight := breachInfo.breachHeight + + blocksPassedSplitPublish + if uint32(epoch.Height) < splitHeight { + continue Loop + } + + brarLog.Warnf("Block height %v arrived without "+ + "justice tx confirming (breached at "+ + "height %v), splitting justice tx.", + epoch.Height, breachInfo.breachHeight) + + // Otherwise we'll attempt to publish the two separate + // justice transactions that sweeps the commitment + // outputs and the HTLC outputs separately. This is to + // mitigate the case where our "spend all" justice TX + // doesn't propagate because the HTLC outputs have been + // pinned by low fee HTLC txs. + label := labels.MakeLabel( + labels.LabelTypeJusticeTransaction, nil, + ) + if justiceTxs.spendCommitOuts != nil { + tx := justiceTxs.spendCommitOuts + + brarLog.Debugf("Broadcasting justice tx "+ + "spending commitment outs: %v", + newLogClosure(func() string { + return spew.Sdump(tx) + })) + + err = b.cfg.PublishTransaction(tx, label) + if err != nil { + brarLog.Warnf("Unable to broadcast "+ + "commit out spending justice "+ + "tx: %v", err) + } + } + + if justiceTxs.spendHTLCs != nil { + tx := justiceTxs.spendHTLCs + + brarLog.Debugf("Broadcasting justice tx "+ + "spending HTLC outs: %v", + newLogClosure(func() string { + return spew.Sdump(tx) + })) + + err = b.cfg.PublishTransaction(tx, label) + if err != nil { + brarLog.Warnf("Unable to broadcast "+ + "HTLC out spending justice "+ + "tx: %v", err) + } + } + + case err := <-errChan: + if err != errBrarShuttingDown { + brarLog.Errorf("error waiting for "+ + "spend event: %v", err) + } + break Loop + + case <-b.quit: + break Loop } - - brarLog.Infof("Justice for ChannelPoint(%v) has "+ - "been served, %v revoked funds (%v total) "+ - "have been claimed", breachInfo.chanPoint, - revokedFunds, totalFunds) - - err = b.cleanupBreach(&breachInfo.chanPoint) - if err != nil { - brarLog.Errorf("Failed to cleanup breached "+ - "ChannelPoint(%v): %v", breachInfo.chanPoint, - err) - } - - // TODO(roasbeef): add peer to blacklist? - - // TODO(roasbeef): close other active channels with offending - // peer - - return - case <-b.quit: - return } + + // Wait for our go routine to exit. + wg.Wait() } // cleanupBreach marks the given channel point as fully resolved and removes the @@ -1045,12 +1146,87 @@ func newRetributionInfo(chanPoint *wire.OutPoint, } } -// createJusticeTx creates a transaction which exacts "justice" by sweeping ALL +// justiceTxVariants is a struct that holds transactions which exacts "justice" +// by sweeping ALL the funds within the channel which we are now entitled to +// due to a breach of the channel's contract by the counterparty. There are +// three variants of the justice transaction: +// +// 1. The "normal" justice tx that spends all breached outputs +// 2. A tx that spends only the breached to_local output and to_remote output +// (can be nil if none of these exist) +// 3. A tx that spends all the breached HTLC outputs, and second-level HTLC +// outputs (can be nil if no HTLC outputs exist). +// +// The reason we create these three variants, is that in certain cases (like +// with the anchor output HTLC malleability), the channel counter party can pin +// the HTLC outputs with low fee children, hindering our normal justice tx that +// attempts to spend these outputs from propagating. In this case we want to +// spend the to_local output separately, before the CSV lock expires. +type justiceTxVariants struct { + spendAll *wire.MsgTx + spendCommitOuts *wire.MsgTx + spendHTLCs *wire.MsgTx +} + +// createJusticeTx creates transactions which exacts "justice" by sweeping ALL // the funds within the channel which we are now entitled to due to a breach of // the channel's contract by the counterparty. This function returns a *fully* // signed transaction with the witness for each input fully in place. func (b *breachArbiter) createJusticeTx( - r *retributionInfo) (*wire.MsgTx, error) { + breachedOutputs []breachedOutput) (*justiceTxVariants, error) { + + var ( + allInputs []input.Input + commitInputs []input.Input + htlcInputs []input.Input + ) + + for i := range breachedOutputs { + // Grab locally scoped reference to breached output. + inp := &breachedOutputs[i] + allInputs = append(allInputs, inp) + + // Check if the input is from an HTLC or a commitment output. + if inp.WitnessType() == input.HtlcAcceptedRevoke || + inp.WitnessType() == input.HtlcOfferedRevoke || + inp.WitnessType() == input.HtlcSecondLevelRevoke { + + htlcInputs = append(htlcInputs, inp) + } else { + commitInputs = append(commitInputs, inp) + } + } + + var ( + txs = &justiceTxVariants{} + err error + ) + + // For each group of inputs, create a tx that spends them. + txs.spendAll, err = b.createSweepTx(allInputs) + if err != nil { + return nil, err + } + + txs.spendCommitOuts, err = b.createSweepTx(commitInputs) + if err != nil { + return nil, err + } + + txs.spendHTLCs, err = b.createSweepTx(htlcInputs) + if err != nil { + return nil, err + } + + return txs, nil +} + +// createSweepTx creates a tx that sweeps the passed inputs back to our wallet. +func (b *breachArbiter) createSweepTx(inputs []input.Input) (*wire.MsgTx, + error) { + if len(inputs) == 0 { + return nil, nil + } // We will assemble the breached outputs into a slice of spendable // outputs, while simultaneously computing the estimated weight of the @@ -1062,7 +1238,7 @@ func (b *breachArbiter) createJusticeTx( // Allocate enough space to potentially hold each of the breached // outputs in the retribution info. - spendableOutputs = make([]input.Input, 0, len(r.breachedOutputs)) + spendableOutputs = make([]input.Input, 0, len(inputs)) // The justice transaction we construct will be a segwit transaction // that pays to a p2wkh output. Components such as the version, @@ -1071,15 +1247,15 @@ func (b *breachArbiter) createJusticeTx( // Next, we iterate over the breached outputs contained in the // retribution info. For each, we switch over the witness type such - // that we contribute the appropriate weight for each input and witness, - // finally adding to our list of spendable outputs. - for i := range r.breachedOutputs { + // that we contribute the appropriate weight for each input and + // witness, finally adding to our list of spendable outputs. + for i := range inputs { // Grab locally scoped reference to breached output. - inp := &r.breachedOutputs[i] + inp := inputs[i] - // First, determine the appropriate estimated witness weight for - // the give witness type of this breached output. If the witness - // weight cannot be estimated, we will omit it from the + // First, determine the appropriate estimated witness weight + // for the give witness type of this breached output. If the + // witness weight cannot be estimated, we will omit it from the // transaction. witnessWeight, _, err := inp.WitnessType().SizeUpperBound() if err != nil { @@ -1120,7 +1296,7 @@ func (b *breachArbiter) sweepSpendableOutputsTxn(txWeight int64, // We'll actually attempt to target inclusion within the next two // blocks as we'd like to sweep these funds back into our wallet ASAP. - feePerKw, err := b.cfg.Estimator.EstimateFeePerKW(2) + feePerKw, err := b.cfg.Estimator.EstimateFeePerKW(justiceTxConfTarget) if err != nil { return nil, err } @@ -1214,15 +1390,6 @@ type RetributionStore interface { // is aware of any breaches for the provided channel point. IsBreached(chanPoint *wire.OutPoint) (bool, error) - // Finalize persists the finalized justice transaction for a particular - // channel. - Finalize(chanPoint *wire.OutPoint, finalTx *wire.MsgTx) error - - // GetFinalizedTxn loads the finalized justice transaction, if any, from - // the retribution store. The finalized transaction will be nil if - // Finalize has not yet been called for this channel point. - GetFinalizedTxn(chanPoint *wire.OutPoint) (*wire.MsgTx, error) - // Remove deletes the retributionInfo from disk, if any exists, under // the given key. An error should be re raised if the removal fails. Remove(key *wire.OutPoint) error @@ -1273,68 +1440,6 @@ func (rs *retributionStore) Add(ret *retributionInfo) error { }, func() {}) } -// Finalize writes a signed justice transaction to the retribution store. This -// is done before publishing the transaction, so that we can recover the txid on -// startup and re-register for confirmation notifications. -func (rs *retributionStore) Finalize(chanPoint *wire.OutPoint, - finalTx *wire.MsgTx) error { - return kvdb.Update(rs.db, func(tx kvdb.RwTx) error { - justiceBkt, err := tx.CreateTopLevelBucket(justiceTxnBucket) - if err != nil { - return err - } - - var chanBuf bytes.Buffer - if err := writeOutpoint(&chanBuf, chanPoint); err != nil { - return err - } - - var txBuf bytes.Buffer - if err := finalTx.Serialize(&txBuf); err != nil { - return err - } - - return justiceBkt.Put(chanBuf.Bytes(), txBuf.Bytes()) - }, func() {}) -} - -// GetFinalizedTxn loads the finalized justice transaction for the provided -// channel point. The finalized transaction will be nil if Finalize has yet to -// be called for this channel point. -func (rs *retributionStore) GetFinalizedTxn( - chanPoint *wire.OutPoint) (*wire.MsgTx, error) { - - var finalTxBytes []byte - if err := kvdb.View(rs.db, func(tx kvdb.RTx) error { - justiceBkt := tx.ReadBucket(justiceTxnBucket) - if justiceBkt == nil { - return nil - } - - var chanBuf bytes.Buffer - if err := writeOutpoint(&chanBuf, chanPoint); err != nil { - return err - } - - finalTxBytes = justiceBkt.Get(chanBuf.Bytes()) - - return nil - }, func() { - finalTxBytes = nil - }); err != nil { - return nil, err - } - - if finalTxBytes == nil { - return nil, nil - } - - finalTx := &wire.MsgTx{} - err := finalTx.Deserialize(bytes.NewReader(finalTxBytes)) - - return finalTx, err -} - // IsBreached queries the retribution store to discern if this channel was // previously breached. This is used when connecting to a peer to determine if // it is safe to add a link to the htlcswitch, as we should never add a channel diff --git a/breacharbiter_test.go b/breacharbiter_test.go index 0abdab2e..358a826a 100644 --- a/breacharbiter_test.go +++ b/breacharbiter_test.go @@ -36,6 +36,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/shachain" + "github.com/stretchr/testify/require" ) var ( @@ -407,24 +408,6 @@ func (frs *failingRetributionStore) IsBreached(chanPoint *wire.OutPoint) (bool, return frs.rs.IsBreached(chanPoint) } -func (frs *failingRetributionStore) Finalize(chanPoint *wire.OutPoint, - finalTx *wire.MsgTx) error { - - frs.mu.Lock() - defer frs.mu.Unlock() - - return frs.rs.Finalize(chanPoint, finalTx) -} - -func (frs *failingRetributionStore) GetFinalizedTxn( - chanPoint *wire.OutPoint) (*wire.MsgTx, error) { - - frs.mu.Lock() - defer frs.mu.Unlock() - - return frs.rs.GetFinalizedTxn(chanPoint) -} - func (frs *failingRetributionStore) Remove(key *wire.OutPoint) error { frs.mu.Lock() defer frs.mu.Unlock() @@ -1220,8 +1203,74 @@ func TestBreachHandoffFail(t *testing.T) { assertArbiterBreach(t, brar, chanPoint) } -type publAssertion func(*testing.T, map[wire.OutPoint]*wire.MsgTx, - chan *wire.MsgTx) +// TestBreachCreateJusticeTx tests that we create three different variants of +// the justice tx. +func TestBreachCreateJusticeTx(t *testing.T) { + brar, _, _, _, _, cleanUpChans, cleanUpArb := initBreachedState(t) + defer cleanUpChans() + defer cleanUpArb() + + // In this test we just want to check that the correct inputs are added + // to the justice tx, not that we create a valid spend, so we just set + // some params making the script generation succeed. + aliceKeyPriv, _ := btcec.PrivKeyFromBytes( + btcec.S256(), channels.AlicesPrivKey, + ) + alicePubKey := aliceKeyPriv.PubKey() + + signDesc := &breachedOutputs[0].signDesc + signDesc.KeyDesc.PubKey = alicePubKey + signDesc.DoubleTweak = aliceKeyPriv + + // We'll test all the different types of outputs we'll sweep with the + // justice tx. + outputTypes := []input.StandardWitnessType{ + input.CommitmentNoDelay, + input.CommitSpendNoDelayTweakless, + input.CommitmentToRemoteConfirmed, + input.CommitmentRevoke, + input.HtlcAcceptedRevoke, + input.HtlcOfferedRevoke, + input.HtlcSecondLevelRevoke, + } + + breachedOutputs := make([]breachedOutput, len(outputTypes)) + for i, wt := range outputTypes { + // Create a fake breached output for each type, ensuring they + // have different outpoints for our logic to accept them. + op := breachedOutputs[0].outpoint + op.Index = uint32(i) + breachedOutputs[i] = makeBreachedOutput( + &op, + wt, + // Second level scripts doesn't matter in this test. + nil, + signDesc, + 1, + ) + } + + // Create the justice transactions. + justiceTxs, err := brar.createJusticeTx(breachedOutputs) + require.NoError(t, err) + require.NotNil(t, justiceTxs) + + // The spendAll tx should be spending all the outputs. This is the + // "regular" justice transaction type. + require.Len(t, justiceTxs.spendAll.TxIn, len(breachedOutputs)) + + // The spendCommitOuts tx should be spending the 4 typed of commit outs + // (note that in practice there will be at most two commit outputs per + // commmit, but we test all 4 types here). + require.Len(t, justiceTxs.spendCommitOuts.TxIn, 4) + + // Finally check that the spendHTLCs tx are spending the two revoked + // HTLC types, and the second level type. + require.Len(t, justiceTxs.spendHTLCs.TxIn, 3) +} + +type publAssertion func(*testing.T, map[wire.OutPoint]struct{}, + chan *wire.MsgTx, chainhash.Hash) *wire.MsgTx type breachTest struct { name string @@ -1231,6 +1280,10 @@ type breachTest struct { // htlc is in effect "readded" to the set of inputs. spend2ndLevel bool + // sweepHtlc tests that the HTLC output is swept using the revocation + // path in a separate tx. + sweepHtlc bool + // sendFinalConf informs the test to send a confirmation for the justice // transaction before asserting the arbiter is cleaned up. sendFinalConf bool @@ -1244,35 +1297,119 @@ type breachTest struct { whenZeroInputs publAssertion } -var ( +type spendTxs struct { + commitSpendTx *wire.MsgTx + htlc2ndLevlTx *wire.MsgTx + htlc2ndLevlSpend *wire.MsgTx + htlcSweep *wire.MsgTx +} + +func getSpendTransactions(signer input.Signer, chanPoint *wire.OutPoint, + retribution *lnwallet.BreachRetribution) (*spendTxs, error) { + + localOutpoint := retribution.LocalOutpoint + remoteOutpoint := retribution.RemoteOutpoint + htlcOutpoint := retribution.HtlcRetributions[0].OutPoint + // commitSpendTx is used to spend commitment outputs. - commitSpendTx = &wire.MsgTx{ + commitSpendTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: localOutpoint, + }, + { + PreviousOutPoint: remoteOutpoint, + }, + }, TxOut: []*wire.TxOut{ {Value: 500000000}, }, } + // htlc2ndLevlTx is used to transition an htlc output on the commitment // transaction to a second level htlc. - htlc2ndLevlTx = &wire.MsgTx{ + htlc2ndLevlTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: htlcOutpoint, + }, + }, TxOut: []*wire.TxOut{ {Value: 20000}, }, } + + secondLvlOp := wire.OutPoint{ + Hash: htlc2ndLevlTx.TxHash(), + Index: 0, + } + // htlcSpendTx is used to spend from a second level htlc. - htlcSpendTx = &wire.MsgTx{ + htlcSpendTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: secondLvlOp, + }, + }, + TxOut: []*wire.TxOut{ {Value: 10000}, }, } -) + + // htlcSweep is used to spend the HTLC output directly using the + // revocation key. + htlcSweep := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: htlcOutpoint, + }, + }, + TxOut: []*wire.TxOut{ + {Value: 21000}, + }, + } + + // In order for the breacharbiter to detect that it is being spent + // using the revocation key, it will inspect the witness. Therefore + // sign and add the witness to the HTLC sweep. + retInfo := newRetributionInfo(chanPoint, retribution) + + hashCache := txscript.NewTxSigHashes(htlcSweep) + for i := range retInfo.breachedOutputs { + inp := &retInfo.breachedOutputs[i] + + // Find the HTLC output. so we can add the witness. + switch inp.witnessType { + case input.HtlcAcceptedRevoke: + fallthrough + case input.HtlcOfferedRevoke: + inputScript, err := inp.CraftInputScript( + signer, htlcSweep, hashCache, 0, + ) + if err != nil { + return nil, err + } + + htlcSweep.TxIn[0].Witness = inputScript.Witness + } + } + + return &spendTxs{ + commitSpendTx: commitSpendTx, + htlc2ndLevlTx: htlc2ndLevlTx, + htlc2ndLevlSpend: htlcSpendTx, + htlcSweep: htlcSweep, + }, nil +} var breachTests = []breachTest{ { name: "all spends", spend2ndLevel: true, whenNonZeroInputs: func(t *testing.T, - inputs map[wire.OutPoint]*wire.MsgTx, - publTx chan *wire.MsgTx) { + inputs map[wire.OutPoint]struct{}, + publTx chan *wire.MsgTx, _ chainhash.Hash) *wire.MsgTx { var tx *wire.MsgTx select { @@ -1281,7 +1418,7 @@ var breachTests = []breachTest{ t.Fatalf("tx was not published") } - // The justice transaction should have thee same number + // The justice transaction should have the same number // of inputs as we are tracking in the test. if len(tx.TxIn) != len(inputs) { t.Fatalf("expected justice txn to have %d "+ @@ -1295,10 +1432,11 @@ var breachTests = []breachTest{ findInputIndex(t, in, tx) } + return tx }, whenZeroInputs: func(t *testing.T, - inputs map[wire.OutPoint]*wire.MsgTx, - publTx chan *wire.MsgTx) { + inputs map[wire.OutPoint]struct{}, + publTx chan *wire.MsgTx, _ chainhash.Hash) *wire.MsgTx { // Sanity check to ensure the brar doesn't try to // broadcast another sweep, since all outputs have been @@ -1308,6 +1446,8 @@ var breachTests = []breachTest{ t.Fatalf("tx published unexpectedly") case <-time.After(50 * time.Millisecond): } + + return nil }, }, { @@ -1315,18 +1455,36 @@ var breachTests = []breachTest{ spend2ndLevel: false, sendFinalConf: true, whenNonZeroInputs: func(t *testing.T, - inputs map[wire.OutPoint]*wire.MsgTx, - publTx chan *wire.MsgTx) { + inputs map[wire.OutPoint]struct{}, + publTx chan *wire.MsgTx, _ chainhash.Hash) *wire.MsgTx { + var tx *wire.MsgTx select { - case <-publTx: + case tx = <-publTx: case <-time.After(5 * time.Second): t.Fatalf("tx was not published") } + + // The justice transaction should have the same number + // of inputs as we are tracking in the test. + if len(tx.TxIn) != len(inputs) { + t.Fatalf("expected justice txn to have %d "+ + "inputs, found %d", len(inputs), + len(tx.TxIn)) + } + + // Ensure that each input exists on the justice + // transaction. + for in := range inputs { + findInputIndex(t, in, tx) + } + + return tx }, whenZeroInputs: func(t *testing.T, - inputs map[wire.OutPoint]*wire.MsgTx, - publTx chan *wire.MsgTx) { + inputs map[wire.OutPoint]struct{}, + publTx chan *wire.MsgTx, + htlc2ndLevlTxHash chainhash.Hash) *wire.MsgTx { // Now a transaction attempting to spend from the second // level tx should be published instead. Let this @@ -1355,10 +1513,60 @@ var breachTests = []breachTest{ // ensuring we aren't mistaking this for a different // output type. onlyInput := tx.TxIn[0].PreviousOutPoint.Hash - if onlyInput != htlc2ndLevlTx.TxHash() { + if onlyInput != htlc2ndLevlTxHash { t.Fatalf("tx not attempting to spend second "+ "level tx, %v", tx.TxIn[0]) } + + return tx + }, + }, + { // nolint: dupl + // Test that if the HTLC output is swept via the revoke path + // (by us) in a separate tx, it will be handled correctly. + name: "sweep htlc", + sweepHtlc: true, + whenNonZeroInputs: func(t *testing.T, + inputs map[wire.OutPoint]struct{}, + publTx chan *wire.MsgTx, _ chainhash.Hash) *wire.MsgTx { + + var tx *wire.MsgTx + select { + case tx = <-publTx: + case <-time.After(5 * time.Second): + t.Fatalf("tx was not published") + } + + // The justice transaction should have the same number + // of inputs as we are tracking in the test. + if len(tx.TxIn) != len(inputs) { + t.Fatalf("expected justice txn to have %d "+ + "inputs, found %d", len(inputs), + len(tx.TxIn)) + } + + // Ensure that each input exists on the justice + // transaction. + for in := range inputs { + findInputIndex(t, in, tx) + } + + return tx + }, + whenZeroInputs: func(t *testing.T, + inputs map[wire.OutPoint]struct{}, + publTx chan *wire.MsgTx, _ chainhash.Hash) *wire.MsgTx { + + // Sanity check to ensure the brar doesn't try to + // broadcast another sweep, since all outputs have been + // spent externally. + select { + case <-publTx: + t.Fatalf("tx published unexpectedly") + case <-time.After(50 * time.Millisecond): + } + + return nil }, }, } @@ -1417,7 +1625,11 @@ func testBreachSpends(t *testing.T, test breachTest) { }, BreachRetribution: retribution, } - contractBreaches <- breach + select { + case contractBreaches <- breach: + case <-time.After(15 * time.Second): + t.Fatalf("breach not delivered") + } // We'll also wait to consume the ACK back from the breach arbiter. select { @@ -1458,7 +1670,12 @@ func testBreachSpends(t *testing.T, test breachTest) { // Notify that the breaching transaction is confirmed, to trigger the // retribution logic. notifier := brar.cfg.Notifier.(*mock.SpendNotifier) - notifier.ConfChan <- &chainntnfs.TxConfirmation{} + + select { + case notifier.ConfChan <- &chainntnfs.TxConfirmation{}: + case <-time.After(15 * time.Second): + t.Fatalf("conf not delivered") + } // The breach arbiter should attempt to sweep all outputs on the // breached commitment. We'll pretend that the HTLC output has been @@ -1482,59 +1699,283 @@ func testBreachSpends(t *testing.T, test breachTest) { remoteOutpoint := retribution.RemoteOutpoint htlcOutpoint := retribution.HtlcRetributions[0].OutPoint + spendTxs, err := getSpendTransactions( + brar.cfg.Signer, chanPoint, retribution, + ) + require.NoError(t, err) + // Construct a map from outpoint on the force close to the transaction // we want it to be spent by. As the test progresses, this map will be // updated to contain only the set of commitment or second level // outpoints that remain to be spent. - inputs := map[wire.OutPoint]*wire.MsgTx{ - htlcOutpoint: htlc2ndLevlTx, - localOutpoint: commitSpendTx, - remoteOutpoint: commitSpendTx, + spentBy := map[wire.OutPoint]*wire.MsgTx{ + htlcOutpoint: spendTxs.htlc2ndLevlTx, + localOutpoint: spendTxs.commitSpendTx, + remoteOutpoint: spendTxs.commitSpendTx, + } + + // We also keep a map of those remaining outputs we expect the + // breacharbiter to try and sweep. + inputsToSweep := map[wire.OutPoint]struct{}{ + htlcOutpoint: {}, + localOutpoint: {}, + remoteOutpoint: {}, + } + + htlc2ndLevlTx := spendTxs.htlc2ndLevlTx + htlcSpendTx := spendTxs.htlc2ndLevlSpend + + // If the test is checking sweep of the HTLC directly without the + // second level, insert the sweep tx instead. + if test.sweepHtlc { + spentBy[htlcOutpoint] = spendTxs.htlcSweep } // Until no more inputs to spend remain, deliver the spend events and // process the assertions prescribed by the test case. - for len(inputs) > 0 { + var justiceTx *wire.MsgTx + for len(spentBy) > 0 { var ( op wire.OutPoint spendTx *wire.MsgTx ) // Pick an outpoint at random from the set of inputs. - for op, spendTx = range inputs { - delete(inputs, op) + for op, spendTx = range spentBy { + delete(spentBy, op) break } // Deliver the spend notification for the chosen transaction. notifier.Spend(&op, 2, spendTx) - // When the second layer transfer is detected, add back the - // outpoint of the second layer tx so that we can spend it - // again. Only do so if the test requests this behavior. + // Since the remote just swept this input, we expect our next + // justice transaction to not include them. + delete(inputsToSweep, op) + + // If this is the second-level spend, we must add the new + // outpoint to our expected sweeps. spendTxID := spendTx.TxHash() - if test.spend2ndLevel && spendTxID == htlc2ndLevlTx.TxHash() { - // Create the second level outpoint that will be spent, - // the index is always zero for these 1-in-1-out txns. + if spendTxID == htlc2ndLevlTx.TxHash() { + // Create the second level outpoint that will + // be spent, the index is always zero for these + // 1-in-1-out txns. spendOp := wire.OutPoint{Hash: spendTxID} - inputs[spendOp] = htlcSpendTx + inputsToSweep[spendOp] = struct{}{} + + // When the second layer transfer is detected, add back + // the outpoint of the second layer tx so that we can + // spend it again. Only do so if the test requests this + // behavior. + if test.spend2ndLevel { + spentBy[spendOp] = htlcSpendTx + } } - if len(inputs) > 0 { - test.whenNonZeroInputs(t, inputs, publTx) + if len(spentBy) > 0 { + justiceTx = test.whenNonZeroInputs(t, inputsToSweep, publTx, htlc2ndLevlTx.TxHash()) } else { // Reset the publishing error so that any publication, // made by the breach arbiter, if any, will succeed. publMtx.Lock() publErr = nil publMtx.Unlock() - test.whenZeroInputs(t, inputs, publTx) + justiceTx = test.whenZeroInputs(t, inputsToSweep, publTx, htlc2ndLevlTx.TxHash()) } } - // Deliver confirmation of sweep if the test expects it. + // Deliver confirmation of sweep if the test expects it. Since we are + // looking for the final justice tx to confirme, we deliver a spend of + // all its inputs. if test.sendFinalConf { - notifier.ConfChan <- &chainntnfs.TxConfirmation{} + for _, txin := range justiceTx.TxIn { + op := txin.PreviousOutPoint + notifier.Spend(&op, 3, justiceTx) + } + } + + // Assert that the channel is fully resolved. + assertBrarCleanup(t, brar, alice.ChanPoint, alice.State().Db) +} + +// TestBreachDelayedJusticeConfirmation tests that the breach arbiter will +// "split" the justice tx in case the first justice tx doesn't confirm within +// a reasonable time. +func TestBreachDelayedJusticeConfirmation(t *testing.T) { + brar, alice, _, bobClose, contractBreaches, + cleanUpChans, cleanUpArb := initBreachedState(t) + defer cleanUpChans() + defer cleanUpArb() + + var ( + height = bobClose.ChanSnapshot.CommitHeight + blockHeight = int32(10) + forceCloseTx = bobClose.CloseTx + chanPoint = alice.ChanPoint + publTx = make(chan *wire.MsgTx) + ) + + // Make PublishTransaction always return succeed. + brar.cfg.PublishTransaction = func(tx *wire.MsgTx, _ string) error { + publTx <- tx + return nil + } + + // Notify the breach arbiter about the breach. + retribution, err := lnwallet.NewBreachRetribution( + alice.State(), height, uint32(blockHeight), + ) + if err != nil { + t.Fatalf("unable to create breach retribution: %v", err) + } + + processACK := make(chan error, 1) + breach := &ContractBreachEvent{ + ChanPoint: *chanPoint, + ProcessACK: func(brarErr error) { + processACK <- brarErr + }, + BreachRetribution: retribution, + } + + select { + case contractBreaches <- breach: + case <-time.After(15 * time.Second): + t.Fatalf("breach not delivered") + } + + // We'll also wait to consume the ACK back from the breach arbiter. + select { + case err := <-processACK: + if err != nil { + t.Fatalf("handoff failed: %v", err) + } + case <-time.After(time.Second * 15): + t.Fatalf("breach arbiter didn't send ack back") + } + + state := alice.State() + err = state.CloseChannel(&channeldb.ChannelCloseSummary{ + ChanPoint: state.FundingOutpoint, + ChainHash: state.ChainHash, + RemotePub: state.IdentityPub, + CloseType: channeldb.BreachClose, + Capacity: state.Capacity, + IsPending: true, + ShortChanID: state.ShortChanID(), + RemoteCurrentRevocation: state.RemoteCurrentRevocation, + RemoteNextRevocation: state.RemoteNextRevocation, + LocalChanConfig: state.LocalChanCfg, + }) + if err != nil { + t.Fatalf("unable to close channel: %v", err) + } + + // After exiting, the breach arbiter should have persisted the + // retribution information and the channel should be shown as pending + // force closed. + assertArbiterBreach(t, brar, chanPoint) + + // Assert that the database sees the channel as pending close, otherwise + // the breach arbiter won't be able to fully close it. + assertPendingClosed(t, alice) + + // Notify that the breaching transaction is confirmed, to trigger the + // retribution logic. + notifier := brar.cfg.Notifier.(*mock.SpendNotifier) + + select { + case notifier.ConfChan <- &chainntnfs.TxConfirmation{}: + case <-time.After(15 * time.Second): + t.Fatalf("conf not delivered") + } + + // The breach arbiter should attempt to sweep all outputs on the + // breached commitment. + var justiceTx *wire.MsgTx + select { + case justiceTx = <-publTx: + case <-time.After(5 * time.Second): + t.Fatalf("tx was not published") + } + + require.Len(t, justiceTx.TxIn, 3) + + // All outputs should initially spend from the force closed txn. + forceTxID := forceCloseTx.TxHash() + for _, txIn := range justiceTx.TxIn { + if txIn.PreviousOutPoint.Hash != forceTxID { + t.Fatalf("og justice tx not spending commitment") + } + } + + // Now we'll pretend some blocks pass without the justice tx + // confirming. + for i := int32(0); i <= 3; i++ { + notifier.EpochChan <- &chainntnfs.BlockEpoch{ + Height: blockHeight + i, + } + + // On every epoch, check that no new tx is published. + select { + case <-publTx: + t.Fatalf("tx was published") + case <-time.After(20 * time.Millisecond): + } + } + + // Now mine another block without the justice tx confirming. This + // should lead to the breacharbiter publishing the split justice tx + // variants. + notifier.EpochChan <- &chainntnfs.BlockEpoch{ + Height: blockHeight + 4, + } + + var ( + splits []*wire.MsgTx + spending = make(map[wire.OutPoint]struct{}) + maxIndex = uint32(len(forceCloseTx.TxOut)) - 1 + ) + for i := 0; i < 2; i++ { + + var tx *wire.MsgTx + select { + case tx = <-publTx: + splits = append(splits, tx) + + case <-time.After(5 * time.Second): + t.Fatalf("tx not published") + } + + // Check that every input is from the breached tx and that + // there are no duplicates. + for _, in := range tx.TxIn { + op := in.PreviousOutPoint + _, ok := spending[op] + if ok { + t.Fatal("already spent") + } + + if op.Hash != forceTxID || op.Index > maxIndex { + t.Fatalf("not spending breach") + } + + spending[op] = struct{}{} + } + } + + // All the inputs from the original justice transaction should have + // been spent by the 2 splits. + require.Len(t, spending, len(justiceTx.TxIn)) + require.Len(t, splits, 2) + + // Finally notify that they confirm, making the breach arbiter clean + // up. + for _, tx := range splits { + for _, in := range tx.TxIn { + op := &in.PreviousOutPoint + notifier.Spend(op, blockHeight+5, tx) + } } // Assert that the channel is fully resolved. diff --git a/input/script_utils.go b/input/script_utils.go index dc3c4ccf..7ce6fff5 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -313,21 +313,32 @@ func SenderHtlcSpendRevokeWithKey(signer Signer, signDesc *SignDescriptor, func SenderHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor, sweepTx *wire.MsgTx) (wire.TxWitness, error) { - if signDesc.KeyDesc.PubKey == nil { - return nil, fmt.Errorf("cannot generate witness with nil " + - "KeyDesc pubkey") + revokeKey, err := deriveRevokePubKey(signDesc) + if err != nil { + return nil, err } - // Derive the revocation key using the local revocation base point and - // commitment point. - revokeKey := DeriveRevocationPubkey( - signDesc.KeyDesc.PubKey, - signDesc.DoubleTweak.PubKey(), - ) - return SenderHtlcSpendRevokeWithKey(signer, signDesc, revokeKey, sweepTx) } +// IsHtlcSpendRevoke is used to determine if the passed spend is spending a +// HTLC output using the revocation key. +func IsHtlcSpendRevoke(txIn *wire.TxIn, signDesc *SignDescriptor) ( + bool, error) { + + revokeKey, err := deriveRevokePubKey(signDesc) + if err != nil { + return false, err + } + + if len(txIn.Witness) == 3 && + bytes.Equal(txIn.Witness[1], revokeKey.SerializeCompressed()) { + return true, nil + } + + return false, nil +} + // SenderHtlcSpendRedeem constructs a valid witness allowing the receiver of an // HTLC to redeem the pending output in the scenario that the sender broadcasts // their version of the commitment transaction. A valid spend requires @@ -575,16 +586,7 @@ func ReceiverHtlcSpendRevokeWithKey(signer Signer, signDesc *SignDescriptor, return witnessStack, nil } -// ReceiverHtlcSpendRevoke constructs a valid witness allowing the sender of an -// HTLC within a previously revoked commitment transaction to re-claim the -// pending funds in the case that the receiver broadcasts this revoked -// commitment transaction. This method first derives the appropriate revocation -// key, and requires that the provided SignDescriptor has a local revocation -// basepoint and commitment secret in the PubKey and DoubleTweak fields, -// respectively. -func ReceiverHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor, - sweepTx *wire.MsgTx) (wire.TxWitness, error) { - +func deriveRevokePubKey(signDesc *SignDescriptor) (*btcec.PublicKey, error) { if signDesc.KeyDesc.PubKey == nil { return nil, fmt.Errorf("cannot generate witness with nil " + "KeyDesc pubkey") @@ -597,6 +599,24 @@ func ReceiverHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor, signDesc.DoubleTweak.PubKey(), ) + return revokeKey, nil +} + +// ReceiverHtlcSpendRevoke constructs a valid witness allowing the sender of an +// HTLC within a previously revoked commitment transaction to re-claim the +// pending funds in the case that the receiver broadcasts this revoked +// commitment transaction. This method first derives the appropriate revocation +// key, and requires that the provided SignDescriptor has a local revocation +// basepoint and commitment secret in the PubKey and DoubleTweak fields, +// respectively. +func ReceiverHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor, + sweepTx *wire.MsgTx) (wire.TxWitness, error) { + + revokeKey, err := deriveRevokePubKey(signDesc) + if err != nil { + return nil, err + } + return ReceiverHtlcSpendRevokeWithKey(signer, signDesc, revokeKey, sweepTx) } diff --git a/lntest/mock/spendnotifier.go b/lntest/mock/spendnotifier.go index 7d51b458..ced93f25 100644 --- a/lntest/mock/spendnotifier.go +++ b/lntest/mock/spendnotifier.go @@ -67,13 +67,20 @@ func (s *SpendNotifier) Spend(outpoint *wire.OutPoint, height int32, s.mtx.Lock() defer s.mtx.Unlock() + var inputIndex uint32 + for i, in := range txn.TxIn { + if in.PreviousOutPoint == *outpoint { + inputIndex = uint32(i) + } + } + txnHash := txn.TxHash() details := &chainntnfs.SpendDetail{ SpentOutPoint: outpoint, SpendingHeight: height, SpendingTx: txn, SpenderTxHash: &txnHash, - SpenderInputIndex: outpoint.Index, + SpenderInputIndex: inputIndex, } // Cache details in case of late registration.