Skip to content

tapchannel: support HTLC revocation sweeps #1994

Open
GeorgeTsagk wants to merge 11 commits into
lightninglabs:mainfrom
GeorgeTsagk:htlc-revocation
Open

tapchannel: support HTLC revocation sweeps #1994
GeorgeTsagk wants to merge 11 commits into
lightninglabs:mainfrom
GeorgeTsagk:htlc-revocation

Conversation

@GeorgeTsagk
Copy link
Copy Markdown
Member

Description

This PR adds support for sweeping revoked HTLC outputs in Taproot Asset channels.

The aux sweeper is extended with resolution logic for revoked offered/accepted HTLCs and second-level HTLC revocations. The signing path is updated to handle breach scenarios, where inputs are spent via key spend rather than script spend, with the HTLC index applied as a tweak to match the commitment's taproot internal key.

Additionally, the NotifyBroadcast interface gains a skipBroadcast flag. During breach resolution, LND crafts multiple competing justice transactions as a pinning mitigation. Rather than notifying tapd at time, LND now notifies after confirmation — the flag signals that the tx is already on-chain and proof finalization can proceed without re-broadcasting.

Depends on https://github.com/lightningnetwork/lnd/pull/10583/commits

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @GeorgeTsagk, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the 'tapchannel' functionality by introducing robust support for sweeping revoked HTLC outputs in Taproot Asset channels. It refines the auxiliary sweeper's resolution capabilities to manage various types of revoked HTLCs and adapts the signing path to correctly handle key spend operations required for breach recovery. Furthermore, it optimizes transaction broadcasting during breach events by allowing the system to skip broadcasting for already confirmed transactions, which is crucial for pinning mitigation.

Highlights

  • HTLC Revocation Sweeps: Implemented support for sweeping revoked HTLC outputs within Taproot Asset channels, enabling recovery in breach scenarios.
  • Auxiliary Sweeper Enhancements: Extended the auxiliary sweeper with specific resolution logic for offered, accepted, and second-level revoked HTLCs.
  • Breach Scenario Signing Path: Updated the signing process to correctly handle key spend for breach scenarios, applying the HTLC index as a tweak to the commitment's taproot internal key.
  • Conditional Transaction Broadcasting: Introduced a 'skipBroadcast' flag to the NotifyBroadcast interface, allowing tapd to avoid re-broadcasting transactions already on-chain during breach resolution.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • go.mod
    • Updated Go language version to 1.25.5.
    • Added temporary replace directives for 'github.com/lightningnetwork/lnd' and 'github.com/lightningnetwork/lnd/sqldb' to a specific commit by GeorgeTsagk.
  • go.sum
    • Updated checksums to reflect the Go version change and the new LND replace directives.
  • server.go
    • Modified the 'NotifyBroadcast' method signature to include a 'skipBroadcast' boolean parameter.
    • Updated the logging for 'NotifyBroadcast' to include the 'skipBroadcast' status.
    • Passed the new 'skipBroadcast' parameter to the underlying auxiliary sweeper's 'NotifyBroadcast' method.
  • tapchannel/aux_closer.go
    • Modified the 'shipChannelTxn' function signature to accept a 'skipBroadcast' boolean parameter.
    • Updated the creation of 'PreAnchoredParcel' to use the new 'skipBroadcast' parameter.
    • Passed 'false' for 'skipBroadcast' when calling 'shipChannelTxn' from 'FinalizeClose'.
  • tapchannel/aux_leaf_signer.go
    • Refactored 'applySignDescToVIn' to distinguish between key spend (breach scenarios) and script spend.
    • For key spend, it now applies 'DoubleTweak' (revocation key) first, then 'SingleTweak' (HTLC index) to the signing key.
    • Adjusted the return value for 'leafToSign' to be empty for key spend scenarios.
    • Updated comments to clarify the tweak application order for breach scenarios.
  • tapchannel/aux_sweeper.go
    • Added a 'skipBroadcast' field to the 'broadcastReq' struct.
    • Imported the 'encoding/binary' package.
    • Modified 'signSweepVpackets' to handle key spend scenarios for HTLC revocations, applying the HTLC index as a 'SingleTweak' and setting the correct 'signMethod' and 'signingKey' for verification.
    • Introduced 'htlcOfferedRevokeSweepDesc', 'htlcAcceptedRevokeSweepDesc', and 'htlcSecondLevelRevokeSweepDesc' functions to generate sweep descriptors for various revoked HTLC types, ensuring correct key tweaking.
    • Updated 'importCommitTx' to pass 'true' for 'skipBroadcast' when shipping commitment transactions, indicating they are already on-chain.
    • Added 'errNoHtlcID' error constant.
    • Extended the 'resolveContract' function to include new resolution types for 'TaprootHtlcOfferedRevoke', 'TaprootHtlcAcceptedRevoke', and 'TaprootHtlcSecondLevelRevoke', filtering assets by HTLC ID and generating appropriate sweep descriptors.
    • Modified 'registerAndBroadcastSweep' and 'NotifyBroadcast' to accept and pass through the 'skipBroadcast' parameter.
Activity
  • No specific activity (comments, reviews, progress updates) was provided in the context for this pull request.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for sweeping revoked HTLC outputs in Taproot Asset channels, a crucial feature for channel security. The changes are extensive, primarily affecting the tapchannel package. Key modifications include extending the auxiliary sweeper with resolution logic for various HTLC revocation scenarios and updating the signing path to handle breach scenarios via key-path spends. A skipBroadcast flag has also been added to the NotifyBroadcast interface to handle transactions that are already on-chain, such as justice transactions. The code is generally well-structured and includes detailed comments explaining the complex logic, especially around cryptographic tweaks. I've identified a couple of areas for improvement: one for enhancing code maintainability by reducing duplication, and another to make the HTLC lookup logic more robust. Overall, this is a solid contribution that adds significant functionality.

Comment thread tapchannel/aux_sweeper.go
Comment on lines +2205 to +2209
// If not found in outgoing, try incoming (accepted HTLCs).
if len(assetOutputs) == 0 {
htlcOutputs = commitState.IncomingHtlcAssets.Val
assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If the HTLC is not found in OutgoingHtlcAssets, the code proceeds to check IncomingHtlcAssets. However, if it's not found there either, assetOutputs will be empty, and the function will proceed without error, likely resulting in an empty resolution blob later on. It would be more robust to return an error if the HTLC cannot be found in either list, to make debugging easier in case of an unexpected state.

		if len(assetOutputs) == 0 {
			htlcOutputs = commitState.IncomingHtlcAssets.Val
			assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID)
			if len(assetOutputs) == 0 {
				return lfn.Errf[returnType](
					"htlc with id %d not found", htlcID,
				)
			}
		}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the very least should log here. Invariants further up the stack should prevent this though.

@coveralls
Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 22064145876

Details

  • 0 of 344 (0.0%) changed or added relevant lines in 4 files are covered.
  • 8349 unchanged lines in 129 files lost coverage.
  • Overall coverage decreased (-6.9%) to 49.529%

Changes Missing Coverage Covered Lines Changed/Added Lines %
tapchannel/aux_closer.go 0 3 0.0%
server.go 0 6 0.0%
tapchannel/aux_leaf_signer.go 0 75 0.0%
tapchannel/aux_sweeper.go 0 260 0.0%
Files with Coverage Reduction New Missed Lines %
tapchannel/aux_closer.go 1 1.21%
authmailbox/client.go 2 69.84%
commitment/proof.go 2 87.29%
fn/retry.go 2 92.5%
fn/errors.go 3 90.32%
itest/multisig.go 3 97.46%
universe/archive.go 3 81.05%
universe/interface.go 3 74.21%
commitment/encoding.go 4 68.75%
mssmt/encoding.go 4 76.67%
Totals Coverage Status
Change from base Build 22061897493: -6.9%
Covered Lines: 58972
Relevant Lines: 119066

💛 - Coveralls

@Roasbeef Roasbeef self-requested a review February 24, 2026 19:50
@jtobin jtobin requested a review from gijswijs February 24, 2026 19:52
Comment thread tapchannel/aux_leaf_signer.go Outdated
}
if signDesc.DoubleTweak != nil {
if isBreach {
// Breach scenario: Apply DoubleTweak first, then SingleTweak.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the ordering matter here? The operation should be commutative.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah you're right. The append order of PSBT unknowns is indeed irrelevant since the signer identifies them by key type, not position.

Comment thread tapchannel/aux_sweeper.go

// Revoked second-level HTLC transaction. We sweep this using the
// revocation path.
case input.TaprootHtlcSecondLevelRevoke:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There aren't two diff enum types for accepted vs offered here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, LND's TaprootHtlcSecondLevelRevoke doesn't distinguish offered vs accepted (unlike first-level which has separate TaprootHtlcOfferedRevoke/TaprootHtlcAcceptedRevoke). We handle this by trying outgoing assets first, then falling back to incoming. HTLC IDs are unique within a commitment so at most one lookup succeeds. If neither has assets for that HTLC ID, it's a non-asset HTLC and doesn't need asset-level resolution.

Comment thread tapchannel/aux_sweeper.go
Comment on lines +2205 to +2209
// If not found in outgoing, try incoming (accepted HTLCs).
if len(assetOutputs) == 0 {
htlcOutputs = commitState.IncomingHtlcAssets.Val
assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the very least should log here. Invariants further up the stack should prevent this though.

Comment thread tapchannel/aux_leaf_signer.go Outdated
Comment thread tapchannel/aux_leaf_signer.go Outdated
Comment thread tapchannel/aux_sweeper.go Outdated
Comment thread tapchannel/aux_sweeper.go Outdated
Comment thread tapchannel/aux_sweeper.go Outdated

// Now that we have the script tree, we'll make the control block
// needed to spend it using the revocation path.
ctrlBlock, err := tweakedScriptTree.CtrlBlockForPath(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add tests here.

AFIACT, this would fail in prod, as it's using an enum, but that value isn't handled by this func: https://github.com/lightningnetwork/lnd/blob/0b00c662318c4960a0f8f814e16e43155029ca35/input/script_utils.go#L1754-L1771

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we should add unit level tests here to ensure that this func is able to sign+verify using the same routines used to create the txns+scripts in the first place.

Comment thread tapchannel/aux_sweeper.go Outdated
@github-project-automation github-project-automation Bot moved this from 🆕 New to 👀 In review in Taproot-Assets Project Board Feb 25, 2026
@GeorgeTsagk
Copy link
Copy Markdown
Member Author

GeorgeTsagk commented Mar 4, 2026

Added a gist explaining the 2nd level situation, focused around the sighash of the pre-signed 2nd level HTLC transaction: https://gist.github.com/GeorgeTsagk/9775947b6b9d8cc07cd23e038a246719

Will soon push a version using the sighash_all approach (HTLCs pay their own fee)

@GeorgeTsagk
Copy link
Copy Markdown
Member Author

Breach itest has now been moved here, no need for a Lit PR

@GeorgeTsagk
Copy link
Copy Markdown
Member Author

Had to update some of the test assertions

Now that we bump DefaultOnChainHtlcSat amount some of the test expectations had to reflect the change.

@GeorgeTsagk
Copy link
Copy Markdown
Member Author

Realized that some of the custom channels itests failures on CI aren't flakes: Some actual test assertions need to change since the HTLC anchor amount was bumped.

Will address them asap, shouldn't be a blocker for reviewers.

Copy link
Copy Markdown
Contributor

@gijswijs gijswijs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I did a thorough review of this PR.

The core cryptographic operations are sound but the surrounding infrastructure needs rework.

I've pointed out all issues with inline comments. I also have some nits wrt the commits (commitnit), which you'll find below:

Typos in commit message for commit d728fae:

The tricky part for taproot assets related to the call sequence of "NotifyBroadcast".

Shouldn't that read "is related"?

we need to signal to the NotifyBroadcast call that we do not wish to broadcast, as that is prone to fail.

prone to fail is an understatement. It will fail and makes no sense whatsoever.

Comment thread tapchannel/aux_sweeper.go
ctx := context.Background()
secondLevelTxHash := secondLevelTx.TxHash()

// Check if already imported.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice dedup guard here. The same pattern is needed in registerAndBroadcastSweep — it also goes through shipChannelTxn → LogPendingParcel → InsertAssetTransfer (plain INSERT), so duplicate NotifyBroadcast calls after restart will insert duplicate transfer rows. See the existing TODO at line 3048.

Comment thread tapchannel/aux_sweeper.go Outdated
signDesc, vIn, &a.cfg.ChainParams, tapTweak,
)

// In this case, the witness isn't special, so we'll set
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
I don't like this comment. "special" does a lot of heavy lifting without clarifying what ismeant by it.

// This is a normal scriptspend (not a breach keyspend), so the witness follows the standard structure.
// Set the control block on the leaf script that applySignDescToVIn prepared

Comment thread tapchannel/aux_sweeper.go Outdated
},
SuccessTapLeaf: tree.SuccessTapLeaf,
TimeoutTapLeaf: tree.TimeoutTapLeaf,
AuxLeaf: tree.AuxLeaf,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
Why are we carrying the AuxLeaf value here. It has no actual usage here, it's only there because we are reusing the lnd stuct.

Comment thread tapchannel/aux_sweeper.go Outdated
Comment on lines +1173 to +1186
// IMPORTANT: We must match the creation flow exactly:
// 1. Create script tree with UNTWEAKED keyring
// 2. Then apply HTLC index tweak to the tree's internal key
//
// During creation, GenTaprootHtlcScript is called with the untweaked
// keyring, then TweakHtlcTree applies the index tweak. We must do
// the same here.
//
// For TaprootHtlcAcceptedRevoke (htlc.Incoming=true in remote's log),
// this means incoming to us (they're sending to us).
// On remote's commitment with them sending, GenTaprootHtlcScript uses:
// isIncoming && whoseCommit.IsRemote() → SenderHTLCScriptTaproot
// with parameters: RemoteHtlcKey, LocalHtlcKey (in that order!)
// where RemoteHtlcKey = sender (them), LocalHtlcKey = receiver (us)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ultranit: This comment is way longer than the comment at the same step in htlcOfferedRevokeSweepDesc. Shouldn't both comments be similar?

Comment thread tapchannel/aux_sweeper.go Outdated
// WITHOUT the aux leaf (createSecondLevelHtlcAllocations passes
// None). The aux leaf only affects the BTC-level on-chain output,
// not the asset-level script key derivation.
_ = auxLeaf // Used at BTC level, not needed for asset signing.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not remove the argument altogether? This does validate my earlier point about carrying the auxLeaf value. At the ASSET-level auxLeaf has no meaning.

Comment thread proof/verifier.go Outdated
// This is used for importing confirmed second-level HTLC transactions in
// breach scenarios where exclusion proofs for counterparty wallet outputs
// cannot be constructed.
func WithSkipExclusionProofVerification() ProofVerificationOption {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment as with AssumeVerifiedAnnotatedProofs. This is a public function with no enforcement that it's only used for confirmed channel transactions. Could allow importing proofs where the same asset appears in multiple outputs.

Consider requiring a confirmation check parameter, or (in this case possible, not with AssumeVerifiedAnnotatedProofs) consider making this private.

Comment thread tapchannel/aux_sweeper.go Outdated
// the HTLC script keys needed for a valid asset witness. The
// BTC-level transaction is already confirmed on-chain, which
// serves as proof of validity.
return shipChannelTxn(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing prevents these proofs from being later re-verified, exported, or served to peers where they would fail verification, right?

// This mirrors what RawTxInTaprootSignature does
// internally.
tapTweak := desc.scriptTree.TapTweak()
taprootPriv := txscript.TweakTaprootPrivKey(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of manually deriving the private key, construct a full virtual packet with both tweak unknowns set via applySignDescToVIn, then call SignVirtualPacket (the actual production signer), and verify the resulting signature against the expected public key. That closes the loop — you'd be testing that the PSBT signer interprets the unknowns the same way applySignDescToVIn intends.

Comment thread tapchannel/aux_sweeper.go Outdated

log.Debugf("signing vPacket for input=%v",
limitSpewer.Sdump(vIn.PrevID))
log.Infof("signing vPacket[%d]: isBreach=%v, "+
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider logging singleTweak and signingKey at Trace level instead of Info.

}
}
}
t.Logf("Found %d second-level txns total", len(secondLevelTxns))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

secondLevelTxns populated and logged but never asserted. Add require.NotEmpty.

@GeorgeTsagk
Copy link
Copy Markdown
Member Author

Rebased on main

@jtobin jtobin added this to the v0.8 milestone May 4, 2026
@lightninglabs-deploy
Copy link
Copy Markdown

@Roasbeef: review reminder
@gijswijs: review reminder
@GeorgeTsagk, remember to re-request review from reviewers when ready

@Hunterstech
Copy link
Copy Markdown

Great Job

@GeorgeTsagk
Copy link
Copy Markdown
Member Author

Rebased on latest main

We add the missing cases for resolving contracts that correspond to
revoked offered/accepted HTLCs, and revoked HTLCs that have been taken
to the second level.

For signing, we distinguish between normal and breach scenarios: normal
force close sweeps use scriptspend (control block present), while breach
sweeps use keyspend (no control block). The breach path is auto-detected
by checking if both SingleTweak and DoubleTweak are present in the sign
descriptor.

We also supply the HTLC index as a single tweak, to be applied by the
signer later. This is crucial for matching the taproot internal key that
was placed in the commitment during creation.
A short intro: For sweeping revoked states the breach arbitrator of LND
crafts 3 different sweep transactions:
a) spendAll: spends commitment outputs (local, remote) and all HTLCs
b) spendCommitOuts: spends only the commitment outputs (local, remote)
c) spendHTLCs: spends only the HTLCs

Initially LND broadcasts version a) of the sweep. If that is not
confirmed within a few blocks(4) then it will broadcast b) and c). This
serves as a pinning attack mitigation.

The tricky part for taproot assets related to the call sequence of
"NotifyBroadcast". That call finalizes the proofs for the transfer given
the txid of the transaction that contains it. Now, we have multiple
competing sweeps fighting to get in a block, so we are uncertain about
which one is going to make it on-chain.

By changing LND into calling NotifyBroadcast after confirming any sweep
transaction, we have a way of certainly telling which proofs need to be
crafted. Given that the transaction is already confirmed, we need to
signal to the NotifyBroadcast call that we do not wish to broadcast, as
that is prone to fail.

The new skipBroadcast flag serves as that signal, and is set by the
caller (LND).
Add a unit test that performs a full sign+verify round-trip for all
three revocation sweep descriptor types (offered, accepted, and
second-level HTLC). The test generates real key material, derives the
revocation signing key using the same routines used in production
(DeriveRevocationPrivKey + TweakPrivKey), and verifies the resulting
schnorr signature against the taproot output key from the sweep
descriptor.

This validates that the sweep descriptor functions produce script trees
whose taproot output keys are consistent with the private key
derivation path, ensuring that breach sweeps will produce valid
signatures at runtime.
…mitment caching

Introduce the DeterministicHTLCs feature bit (bits 4/5) which gates
deterministic second-level HTLC transactions. When negotiated,
second-level HTLCs use SigHashDefault instead of
SigHashSingle|AnyOneCanPay, and the revoking party includes
dual-path AuxSigs in RevokeAndAck for breach proof reconstruction.

Also add SigHashType caching in the commitment blob so the sighash
choice is recorded alongside the commitment state and can be
recovered at breach time without re-querying the feature store.
…HTLCs flag

Implement Server.HtlcSigHashType (lnwallet.AuxSigner interface) which
checks live feature negotiation state first, then falls back to decoding
the commitment blob's cached SigHashDefault flag. Includes a nil guard
for the startup race when tapd config is not yet initialized.

Thread the DeterministicHTLCs feature flag through the commitment and
leaf creation paths:

- aux_funding_controller: store flag from negotiated features
  in pendingAssetFunding and pass through to initial commitment blobs.
- aux_leaf_creator: query feature store to determine sighash type
  for each new commitment, and pass to allocation helpers.
- commitment.go: accept sigHashDefault parameter in
  GenerateCommitmentAllocations and CreateSecondLevelHtlcPackets.
Teach the revoked HTLC recovery flow to sweep second-level outputs back through
the aux channel machinery.

The sweep/import path can report an already-broadcast pre-anchored transfer
more than once, and it can also confirm a successor spend before the exact
predecessor asset row has been materialized locally. Without additional DB-side
handling those cases strand recovery transfers in pending state even though the
BTC spend is already confirmed.

Plumb second-level revoked HTLC sweeps through the aux sweeper and make the
asset-store side of the porter tolerant of repeated notifications and missing
exact predecessor rows by treating pending transfer insertion as idempotent by
anchor txid and falling back to a same-asset template row when finalizing a
confirmed recovery spend.
Update NotifyBroadcast callers and implementation to use the new
AuxNotifyOpts struct from lnd, replacing the bare skipBroadcast bool.
SkipBroadcast and SkipProofVerify are now evaluated independently.
Replace placeholder witnesses in importSecondLevelHtlcTx with real
asset-level signatures. When AuxSigDesc is available (breach case),
sign with our local HTLC key via SignVirtualPacket and insert the
remote party's pre-stored signature to produce a full 2-of-2 witness.

Try both primary and alternate AuxSig candidates when constructing
the witness, since the virtual transaction's spending path may not
match the BTC on-chain path. Use the imported proof suffix (with
correct inclusion proof, block header, and merkle proof) for the
justice sweep descriptor instead of hand-built stubs.

When AuxSigDesc is not available, fall back to placeholder witnesses
with proof verification skipped.
The default 30-second RPC timeout in lndclient is too short when the
integrated daemon restarts after many blocks have been mined while it
was offline. The chain sync during GetInfo can easily exceed 30 seconds
when catching up on 100+ blocks. Increase to 2 minutes to match
production configurations and prevent spurious restart failures during
integration tests.
@GeorgeTsagk GeorgeTsagk force-pushed the htlc-revocation branch 2 times, most recently from 6ca3f29 to ac03a97 Compare May 14, 2026 12:25
Add a custom-channel breach integration test that exercises revoked HTLCs which
advance to second level before the honest party comes back online and sweeps
them.

Keep the default scenario small for normal suite runtime, but structure the
itest so it can be scaled up to stress the same recovery path. Higher-count
variants need additional setup discipline: the pre-breach rebalance must scale
with the requested number of in-flight HTLCs, the post-settlement keysend must
wait until HTLC cleanup has actually completed, and justice mining must accept
the variable set of justice transactions published once more HTLCs reach second
level.

Make the breach test configurable through TAPD_BREACH_HODL_INVOICES_PER_SIDE
while keeping the default at two HTLCs per side.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: 👀 In review

Development

Successfully merging this pull request may close these issues.

7 participants