diff --git a/watchtower/wtclient/backup_task.go b/watchtower/wtclient/backup_task.go index 6801d12b..adc000a8 100644 --- a/watchtower/wtclient/backup_task.go +++ b/watchtower/wtclient/backup_task.go @@ -39,6 +39,7 @@ import ( type backupTask struct { id wtdb.BackupID breachInfo *lnwallet.BreachRetribution + chanType channeldb.ChannelType // state-dependent variables @@ -90,18 +91,33 @@ func newBackupTask(chanID *lnwire.ChannelID, if breachInfo.LocalOutputSignDesc != nil { var witnessType input.WitnessType switch { + case chanType.HasAnchors(): + witnessType = input.CommitmentToRemoteConfirmed case chanType.IsTweakless(): witnessType = input.CommitSpendNoDelayTweakless default: witnessType = input.CommitmentNoDelay } - toRemoteInput = input.NewBaseInput( - &breachInfo.LocalOutpoint, - witnessType, - breachInfo.LocalOutputSignDesc, - 0, - ) + // Anchor channels have a CSV-encumbered to-remote output. We'll + // construct a CSV input in that case and assign the proper CSV + // delay of 1, otherwise we fallback to the a regular P2WKH + // to-remote output for tweaked or tweakless channels. + if chanType.HasAnchors() { + toRemoteInput = input.NewCsvInput( + &breachInfo.LocalOutpoint, + witnessType, + breachInfo.LocalOutputSignDesc, + 0, 1, + ) + } else { + toRemoteInput = input.NewBaseInput( + &breachInfo.LocalOutpoint, + witnessType, + breachInfo.LocalOutputSignDesc, + 0, + ) + } totalAmt += breachInfo.LocalOutputSignDesc.Output.Value } @@ -112,6 +128,7 @@ func newBackupTask(chanID *lnwire.ChannelID, CommitHeight: breachInfo.RevokedStateNum, }, breachInfo: breachInfo, + chanType: chanType, toLocalInput: toLocalInput, toRemoteInput: toRemoteInput, totalAmt: btcutil.Amount(totalAmt), @@ -151,13 +168,28 @@ func (t *backupTask) bindSession(session *wtdb.ClientSessionBody) error { // underestimate the size by one byte. The diferrence in weight // can cause different output values on the sweep transaction, // so we mimic the original bug and create signatures using the - // original weight estimate. - weightEstimate.AddWitnessInput( - input.ToLocalPenaltyWitnessSize - 1, - ) + // original weight estimate. For anchor channels we'll go ahead + // an use the correct penalty witness when signing our justice + // transactions. + if t.chanType.HasAnchors() { + weightEstimate.AddWitnessInput( + input.ToLocalPenaltyWitnessSize, + ) + } else { + weightEstimate.AddWitnessInput( + input.ToLocalPenaltyWitnessSize - 1, + ) + } } if t.toRemoteInput != nil { - weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) + // Legacy channels (both tweaked and non-tweaked) spend from + // P2WKH output. Anchor channels spend a to-remote confirmed + // P2WSH output. + if t.chanType.HasAnchors() { + weightEstimate.AddWitnessInput(input.ToRemoteConfirmedWitnessSize) + } else { + weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) + } } // All justice transactions have a p2wkh output paying to the victim. @@ -169,6 +201,12 @@ func (t *backupTask) bindSession(session *wtdb.ClientSessionBody) error { weightEstimate.AddP2WKHOutput() } + if t.chanType.HasAnchors() != session.Policy.IsAnchorChannel() { + log.Criticalf("Invalid task (has_anchors=%t) for session "+ + "(has_anchors=%t)", t.chanType.HasAnchors(), + session.Policy.IsAnchorChannel()) + } + // Now, compute the output values depending on whether FlagReward is set // in the current session's policy. outputs, err := session.Policy.ComputeJusticeTxOuts( @@ -225,9 +263,10 @@ func (t *backupTask) craftSessionPayload( // information. This will either be contain both the to-local and // to-remote outputs, or only be the to-local output. inputs := t.inputs() - for prevOutPoint := range inputs { + for prevOutPoint, input := range inputs { justiceTxn.AddTxIn(&wire.TxIn{ PreviousOutPoint: prevOutPoint, + Sequence: input.BlocksToMaturity(), }) } @@ -294,6 +333,8 @@ func (t *backupTask) craftSessionPayload( case input.CommitSpendNoDelayTweakless: fallthrough case input.CommitmentNoDelay: + fallthrough + case input.CommitmentToRemoteConfirmed: copy(justiceKit.CommitToRemoteSig[:], signature[:]) default: return hint, nil, fmt.Errorf("invalid witness type: %v", diff --git a/watchtower/wtclient/backup_task_internal_test.go b/watchtower/wtclient/backup_task_internal_test.go index 4841ca02..db85472d 100644 --- a/watchtower/wtclient/backup_task_internal_test.go +++ b/watchtower/wtclient/backup_task_internal_test.go @@ -95,6 +95,12 @@ func genTaskTest( bindErr error, chanType channeldb.ChannelType) backupTaskTest { + // Set the anchor flag in the blob type if the session needs to support + // anchor channels. + if chanType.HasAnchors() { + blobType |= blob.Type(blob.FlagAnchorChannel) + } + // Parse the key pairs for all keys used in the test. revSK, revPK := btcec.PrivKeyFromBytes( btcec.S256(), revPrivBytes, @@ -195,18 +201,29 @@ func genTaskTest( var witnessType input.WitnessType switch { + case chanType.HasAnchors(): + witnessType = input.CommitmentToRemoteConfirmed case chanType.IsTweakless(): witnessType = input.CommitSpendNoDelayTweakless default: witnessType = input.CommitmentNoDelay } - toRemoteInput = input.NewBaseInput( - &breachInfo.LocalOutpoint, - witnessType, - breachInfo.LocalOutputSignDesc, - 0, - ) + if chanType.HasAnchors() { + toRemoteInput = input.NewCsvInput( + &breachInfo.LocalOutpoint, + witnessType, + breachInfo.LocalOutputSignDesc, + 0, 1, + ) + } else { + toRemoteInput = input.NewBaseInput( + &breachInfo.LocalOutpoint, + witnessType, + breachInfo.LocalOutputSignDesc, + 0, + ) + } } return backupTaskTest{ @@ -260,61 +277,93 @@ func TestBackupTask(t *testing.T) { chanTypes := []channeldb.ChannelType{ channeldb.SingleFunderBit, channeldb.SingleFunderTweaklessBit, + channeldb.AnchorOutputsBit, } var backupTaskTests []backupTaskTest for _, chanType := range chanTypes { + // Depending on whether the test is for anchor channels or + // legacy (tweaked and non-tweaked) channels, adjust the + // expected sweep amount to accommodate. These are different for + // several reasons: + // - anchor to-remote outputs require a P2WSH sweep rather + // than a P2WKH sweep. + // - the to-local weight estimate fixes an off-by-one. + // In tests related to the dust threshold, the size difference + // between the channel types makes it so that the threshold fee + // rate is slightly lower (since the transactions are heavier). + var ( + expSweepCommitNoRewardBoth int64 = 299241 + expSweepCommitNoRewardLocal int64 = 199514 + expSweepCommitNoRewardRemote int64 = 99561 + expSweepCommitRewardBoth int64 = 296117 + expSweepCommitRewardLocal int64 = 197390 + expSweepCommitRewardRemote int64 = 98437 + sweepFeeRateNoRewardRemoteDust chainfee.SatPerKWeight = 227500 + sweepFeeRateRewardRemoteDust chainfee.SatPerKWeight = 175000 + ) + if chanType.HasAnchors() { + expSweepCommitNoRewardBoth = 299236 + expSweepCommitNoRewardLocal = 199513 + expSweepCommitNoRewardRemote = 99557 + expSweepCommitRewardBoth = 296112 + expSweepCommitRewardLocal = 197389 + expSweepCommitRewardRemote = 98433 + sweepFeeRateNoRewardRemoteDust = 225000 + sweepFeeRateRewardRemoteDust = 173750 + } + backupTaskTests = append(backupTaskTests, []backupTaskTest{ genTaskTest( "commit no-reward, both outputs", - 100, // stateNum - 200000, // toLocalAmt - 100000, // toRemoteAmt - blobTypeCommitNoReward, // blobType - 1000, // sweepFeeRate - nil, // rewardScript - 299241, // expSweepAmt - 0, // expRewardAmt - nil, // bindErr + 100, // stateNum + 200000, // toLocalAmt + 100000, // toRemoteAmt + blobTypeCommitNoReward, // blobType + 1000, // sweepFeeRate + nil, // rewardScript + expSweepCommitNoRewardBoth, // expSweepAmt + 0, // expRewardAmt + nil, // bindErr chanType, ), genTaskTest( "commit no-reward, to-local output only", - 1000, // stateNum - 200000, // toLocalAmt - 0, // toRemoteAmt - blobTypeCommitNoReward, // blobType - 1000, // sweepFeeRate - nil, // rewardScript - 199514, // expSweepAmt - 0, // expRewardAmt - nil, // bindErr + 1000, // stateNum + 200000, // toLocalAmt + 0, // toRemoteAmt + blobTypeCommitNoReward, // blobType + 1000, // sweepFeeRate + nil, // rewardScript + expSweepCommitNoRewardLocal, // expSweepAmt + 0, // expRewardAmt + nil, // bindErr chanType, ), genTaskTest( "commit no-reward, to-remote output only", - 1, // stateNum - 0, // toLocalAmt - 100000, // toRemoteAmt - blobTypeCommitNoReward, // blobType - 1000, // sweepFeeRate - nil, // rewardScript - 99561, // expSweepAmt - 0, // expRewardAmt - nil, // bindErr + 1, // stateNum + 0, // toLocalAmt + 100000, // toRemoteAmt + blobTypeCommitNoReward, // blobType + 1000, // sweepFeeRate + nil, // rewardScript + expSweepCommitNoRewardRemote, // expSweepAmt + 0, // expRewardAmt + nil, // bindErr chanType, ), genTaskTest( "commit no-reward, to-remote output only, creates dust", - 1, // stateNum - 0, // toLocalAmt - 100000, // toRemoteAmt - blobTypeCommitNoReward, // blobType - 227500, // sweepFeeRate - nil, // rewardScript - 0, // expSweepAmt - 0, // expRewardAmt - wtpolicy.ErrCreatesDust, // bindErr + 1, // stateNum + 0, // toLocalAmt + 100000, // toRemoteAmt + blobTypeCommitNoReward, // blobType + sweepFeeRateNoRewardRemoteDust, // sweepFeeRate + nil, // rewardScript + 0, // expSweepAmt + 0, // expRewardAmt + wtpolicy.ErrCreatesDust, // bindErr chanType, ), genTaskTest( @@ -345,54 +394,54 @@ func TestBackupTask(t *testing.T) { ), genTaskTest( "commit reward, both outputs", - 100, // stateNum - 200000, // toLocalAmt - 100000, // toRemoteAmt - blobTypeCommitReward, // blobType - 1000, // sweepFeeRate - addrScript, // rewardScript - 296117, // expSweepAmt - 3000, // expRewardAmt - nil, // bindErr + 100, // stateNum + 200000, // toLocalAmt + 100000, // toRemoteAmt + blobTypeCommitReward, // blobType + 1000, // sweepFeeRate + addrScript, // rewardScript + expSweepCommitRewardBoth, // expSweepAmt + 3000, // expRewardAmt + nil, // bindErr chanType, ), genTaskTest( "commit reward, to-local output only", - 1000, // stateNum - 200000, // toLocalAmt - 0, // toRemoteAmt - blobTypeCommitReward, // blobType - 1000, // sweepFeeRate - addrScript, // rewardScript - 197390, // expSweepAmt - 2000, // expRewardAmt - nil, // bindErr + 1000, // stateNum + 200000, // toLocalAmt + 0, // toRemoteAmt + blobTypeCommitReward, // blobType + 1000, // sweepFeeRate + addrScript, // rewardScript + expSweepCommitRewardLocal, // expSweepAmt + 2000, // expRewardAmt + nil, // bindErr chanType, ), genTaskTest( "commit reward, to-remote output only", - 1, // stateNum - 0, // toLocalAmt - 100000, // toRemoteAmt - blobTypeCommitReward, // blobType - 1000, // sweepFeeRate - addrScript, // rewardScript - 98437, // expSweepAmt - 1000, // expRewardAmt - nil, // bindErr + 1, // stateNum + 0, // toLocalAmt + 100000, // toRemoteAmt + blobTypeCommitReward, // blobType + 1000, // sweepFeeRate + addrScript, // rewardScript + expSweepCommitRewardRemote, // expSweepAmt + 1000, // expRewardAmt + nil, // bindErr chanType, ), genTaskTest( "commit reward, to-remote output only, creates dust", - 1, // stateNum - 0, // toLocalAmt - 100000, // toRemoteAmt - blobTypeCommitReward, // blobType - 175000, // sweepFeeRate - addrScript, // rewardScript - 0, // expSweepAmt - 0, // expRewardAmt - wtpolicy.ErrCreatesDust, // bindErr + 1, // stateNum + 0, // toLocalAmt + 100000, // toRemoteAmt + blobTypeCommitReward, // blobType + sweepFeeRateRewardRemoteDust, // sweepFeeRate + addrScript, // rewardScript + 0, // expSweepAmt + 0, // expRewardAmt + wtpolicy.ErrCreatesDust, // bindErr chanType, ), genTaskTest(