diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 89be2c08f..56f7861a5 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -192,8 +192,7 @@ func (a *AuxSweeper) Stop() error { // set of asset inputs into the backing wallet. func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, tapscriptDesc lfn.Result[tapscriptSweepDesc], - resReq lnwallet.ResolutionReq, -) lfn.Result[[]*tappsbt.VPacket] { + resReq lnwallet.ResolutionReq) lfn.Result[[]*tappsbt.VPacket] { type returnType = []*tappsbt.VPacket @@ -727,23 +726,26 @@ func commitRevokeSweepDesc(keyRing *lnwallet.CommitmentKeyRing, // remoteHtlcTimeoutSweepDesc creates a sweep desc for an HTLC output that is // close to timing out on the remote party's commitment transaction. -func remoteHtlcTimeoutSweepDesc(keyRing *lnwallet.CommitmentKeyRing, +func remoteHtlcTimeoutSweepDesc(originalKeyRing *lnwallet.CommitmentKeyRing, payHash []byte, csvDelay uint32, htlcExpiry uint32, -) lfn.Result[tapscriptSweepDescs] { + index input.HtlcIndex) lfn.Result[tapscriptSweepDescs] { + + // We're sweeping an HTLC output, which has a tweaked script key. To be + // able to create the correct control block, we need to tweak the key + // ring with the index of the HTLC. + tweakedKeyRing := TweakedRevocationKeyRing(originalKeyRing, index) // We're sweeping a timed out HTLC, which means that we'll need to // create the receiver's HTLC script tree (from the remote party's PoV). htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( - htlcExpiry, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, - keyRing.RevocationKey, payHash, lntypes.Remote, - input.NoneTapLeaf(), + htlcExpiry, tweakedKeyRing.LocalHtlcKey, + tweakedKeyRing.RemoteHtlcKey, tweakedKeyRing.RevocationKey, + payHash, lntypes.Remote, input.NoneTapLeaf(), ) if err != nil { return lfn.Err[tapscriptSweepDescs](err) } - // TODO(roasbeef): use GenTaprootHtlcScript instead? - // Now that we have the script tree, we'll make the control block needed // to spend it, but taking the revoked path. ctrlBlock, err := htlcScriptTree.CtrlBlockForPath( @@ -770,15 +772,21 @@ func remoteHtlcTimeoutSweepDesc(keyRing *lnwallet.CommitmentKeyRing, // remoteHtlcSuccessSweepDesc creates a sweep desc for an HTLC output present on // the remote party's commitment transaction that we can sweep with the // preimage. -func remoteHtlcSuccessSweepDesc(keyRing *lnwallet.CommitmentKeyRing, - payHash []byte, csvDelay uint32) lfn.Result[tapscriptSweepDescs] { +func remoteHtlcSuccessSweepDesc(originalKeyRing *lnwallet.CommitmentKeyRing, + payHash []byte, csvDelay uint32, + index input.HtlcIndex) lfn.Result[tapscriptSweepDescs] { + + // We're sweeping an HTLC output, which has a tweaked script key. To be + // able to create the correct control block, we need to tweak the key + // ring with the index of the HTLC. + tweakedKeyRing := TweakedRevocationKeyRing(originalKeyRing, index) // We're planning on sweeping an HTLC that we know the preimage to, // which the remote party sent, so we'll construct the sender version of // the HTLC script tree (from their PoV, they're the sender). htlcScriptTree, err := input.SenderHTLCScriptTaproot( - keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, payHash, lntypes.Remote, + tweakedKeyRing.RemoteHtlcKey, tweakedKeyRing.LocalHtlcKey, + tweakedKeyRing.RevocationKey, payHash, lntypes.Remote, input.NoneTapLeaf(), ) if err != nil { @@ -811,9 +819,9 @@ func remoteHtlcSuccessSweepDesc(keyRing *lnwallet.CommitmentKeyRing, // present on our local commitment transaction. These are second level HTLCs, so // we'll need to perform two stages of sweeps. func localHtlcTimeoutSweepDesc(req lnwallet.ResolutionReq, -) lfn.Result[tapscriptSweepDescs] { + index input.HtlcIndex) lfn.Result[tapscriptSweepDescs] { - isIncoming := false + const isIncoming = false payHash, err := req.PayHash.UnwrapOrErr( fmt.Errorf("no pay hash"), @@ -828,11 +836,16 @@ func localHtlcTimeoutSweepDesc(req lnwallet.ResolutionReq, return lfn.Err[tapscriptSweepDescs](err) } + // We're sweeping an HTLC output, which has a tweaked script key. To be + // able to create the correct control block, we need to tweak the key + // ring with the index of the HTLC. + tweakedKeyRing := TweakedRevocationKeyRing(req.KeyRing, index) + // We'll need to complete the control block to spend the second-level // HTLC, so first we'll make the script tree for the HTLC. htlcScriptTree, err := lnwallet.GenTaprootHtlcScript( - isIncoming, lntypes.Local, htlcExpiry, - payHash, req.KeyRing, lfn.None[txscript.TapLeaf](), + isIncoming, lntypes.Local, htlcExpiry, payHash, tweakedKeyRing, + lfn.None[txscript.TapLeaf](), ) if err != nil { return lfn.Errf[tapscriptSweepDescs]("error creating "+ @@ -900,13 +913,13 @@ func localHtlcTimeoutSweepDesc(req lnwallet.ResolutionReq, }) } -// localHtlcSucessSweepDesc creates a sweep desc for an HTLC output that is +// localHtlcSuccessSweepDesc creates a sweep desc for an HTLC output that is // present on our local commitment transaction that we can sweep with a // preimage. These sweeps take two stages, so we'll add that extra information. -func localHtlcSucessSweepDesc(req lnwallet.ResolutionReq, -) lfn.Result[tapscriptSweepDescs] { +func localHtlcSuccessSweepDesc(req lnwallet.ResolutionReq, + index input.HtlcIndex) lfn.Result[tapscriptSweepDescs] { - isIncoming := true + const isIncoming = true payHash, err := req.PayHash.UnwrapOrErr( fmt.Errorf("no pay hash"), @@ -921,11 +934,16 @@ func localHtlcSucessSweepDesc(req lnwallet.ResolutionReq, return lfn.Err[tapscriptSweepDescs](err) } + // We're sweeping an HTLC output, which has a tweaked script key. To be + // able to create the correct control block, we need to tweak the key + // ring with the index of the HTLC. + tweakedKeyRing := TweakedRevocationKeyRing(req.KeyRing, index) + // We'll need to complete the control block to spend the second-level // HTLC, so first we'll make the script tree for the HTLC. htlcScriptTree, err := lnwallet.GenTaprootHtlcScript( - isIncoming, lntypes.Local, htlcExpiry, - payHash, req.KeyRing, lfn.None[txscript.TapLeaf](), + isIncoming, lntypes.Local, htlcExpiry, payHash, tweakedKeyRing, + lfn.None[txscript.TapLeaf](), ) if err != nil { return lfn.Errf[tapscriptSweepDescs]("error creating "+ @@ -1707,10 +1725,9 @@ func (a *AuxSweeper) resolveContract( // assets for the remote party, which are actually the HTLCs we // sent outgoing. We only care about this particular HTLC, so // we'll filter out the rest. + htlcID := req.HtlcID.UnwrapOr(math.MaxUint64) htlcOutputs := commitState.OutgoingHtlcAssets.Val - assetOutputs = htlcOutputs.FilterByHtlcIndex( - req.HtlcID.UnwrapOr(math.MaxUint64), - ) + assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID) payHash, err := req.PayHash.UnwrapOrErr(errNoPayHash) if err != nil { @@ -1721,7 +1738,7 @@ func (a *AuxSweeper) resolveContract( // sweep desc for the timeout txn. sweepDesc = remoteHtlcTimeoutSweepDesc( req.KeyRing, payHash[:], req.CsvDelay, - req.CltvDelay.UnwrapOr(0), + req.CltvDelay.UnwrapOr(0), htlcID, ) // The remote party broadcasted a commitment transaction which held an @@ -1730,10 +1747,9 @@ func (a *AuxSweeper) resolveContract( // In this case, it's an outgoing HTLC from the PoV of the // remote party, which is incoming for us. We'll only sweep this // HTLC, so we'll filter out the rest. + htlcID := req.HtlcID.UnwrapOr(math.MaxUint64) htlcOutputs := commitState.IncomingHtlcAssets.Val - assetOutputs = htlcOutputs.FilterByHtlcIndex( - req.HtlcID.UnwrapOr(math.MaxUint64), - ) + assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID) payHash, err := req.PayHash.UnwrapOrErr(errNoPayHash) if err != nil { @@ -1743,7 +1759,7 @@ func (a *AuxSweeper) resolveContract( // Now that we know which output we'll be sweeping, we'll make a // sweep desc for the timeout txn. sweepDesc = remoteHtlcSuccessSweepDesc( - req.KeyRing, payHash[:], req.CsvDelay, + req.KeyRing, payHash[:], req.CsvDelay, htlcID, ) // In this case, we broadcast a commitment transaction which held an @@ -1753,14 +1769,13 @@ func (a *AuxSweeper) resolveContract( case input.TaprootHtlcLocalOfferedTimeout: // Like the other HTLC cases, there's only a single output we // care about here. + htlcID := req.HtlcID.UnwrapOr(math.MaxUint64) htlcOutputs := commitState.OutgoingHtlcAssets.Val - assetOutputs = htlcOutputs.FilterByHtlcIndex( - req.HtlcID.UnwrapOr(math.MaxUint64), - ) + assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID) // With the output and pay desc located, we'll now create the // sweep desc. - sweepDesc = localHtlcTimeoutSweepDesc(req) + sweepDesc = localHtlcTimeoutSweepDesc(req, htlcID) needsSecondLevel = true @@ -1769,21 +1784,26 @@ func (a *AuxSweeper) resolveContract( // needed to sweep both this output, as well as the second level // output it creates. case input.TaprootHtlcAcceptedLocalSuccess: + htlcID := req.HtlcID.UnwrapOr(math.MaxUint64) htlcOutputs := commitState.IncomingHtlcAssets.Val - assetOutputs = htlcOutputs.FilterByHtlcIndex( - req.HtlcID.UnwrapOr(math.MaxUint64), - ) + assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID) // With the output and pay desc located, we'll now create the // sweep desc. - sweepDesc = localHtlcSucessSweepDesc(req) + sweepDesc = localHtlcSuccessSweepDesc(req, htlcID) needsSecondLevel = true default: + // TODO(guggero): Need to do HTLC revocation cases here. + // IMPORTANT: Remember that we applied the HTLC index as a tweak + // to the revocation key on the asset level! That means the + // tweak to the first-level HTLC script key's internal key + // (which is the revocation key) MUST be applied when creating + // a breach sweep transaction! + return lfn.Errf[returnType]("unknown resolution type: %v", req.Type) - // TODO(roasbeef): need to do HTLC revocation casesj:w } tapSweepDesc, err := sweepDesc.Unpack() @@ -2574,3 +2594,25 @@ func (a *AuxSweeper) NotifyBroadcast(req *sweep.BumpRequest, return resp } + +// TweakedRevocationKeyRing returns a new commitment key ring with the +// revocation key tweaked by the given HTLC index. The revocation key is tweaked +// in order to achieve uniqueness for each HTLC output on the asset level. This +// same tweak will need to be applied to the revocation private key in case of +// a breach. +func TweakedRevocationKeyRing(keyRing *lnwallet.CommitmentKeyRing, + index input.HtlcIndex) *lnwallet.CommitmentKeyRing { + + return &lnwallet.CommitmentKeyRing{ + CommitPoint: keyRing.CommitPoint, + LocalCommitKeyTweak: keyRing.LocalCommitKeyTweak, + LocalHtlcKeyTweak: keyRing.LocalHtlcKeyTweak, + LocalHtlcKey: keyRing.LocalHtlcKey, + RemoteHtlcKey: keyRing.RemoteHtlcKey, + ToLocalKey: keyRing.ToLocalKey, + ToRemoteKey: keyRing.ToRemoteKey, + RevocationKey: TweakPubKeyWithIndex( + keyRing.RevocationKey, index, + ), + } +}