Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions htlcswitch/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -4553,6 +4553,26 @@ func (l *channelLink) processRemoteError(msg *lnwire.Error) {
// Error received from remote, MUST fail channel, but should only print
// the contents of the error message if all characters are printable
// ASCII.

// Before tearing the link down, attempt to re-derive the commitment
// transaction and sighash we last computed for the remote party. If
// the peer sent a rejected-commitment error, logging our view alongside
// theirs makes it possible to compare the two transactions
// byte-for-byte and immediately identify any discrepancy.
commitTx, err := l.channel.BuildLastSignedRemoteCommitTx()
switch {
case err != nil:
l.log.Errorf("ChannelPoint(%v): unable to re-derive last "+
"signed remote commit tx: %v",
l.channel.ChannelPoint(), err)

case commitTx != nil:
l.log.Errorf("ChannelPoint(%v): signer-side commit_tx=%x "+
"(compare with commit_tx in the peer error if present "+
"to identify any state divergence)",
l.channel.ChannelPoint(), commitTx)
}

l.failf(
// TODO(halseth): we currently don't fail the channel
// permanently, as there are some sync issues with other
Expand Down
34 changes: 34 additions & 0 deletions lnwallet/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -4303,6 +4303,40 @@ func (lc *LightningChannel) SignNextCommitment(
}, nil
}

// BuildLastSignedRemoteCommitTx returns the serialized commitment transaction
// most recently signed for the remote party. The remote commit chain holds
// all commitments we have signed but the remote has not yet revoked. Its tip
// is the newest entry — added by the last SignNextCommitment call — and is
// only removed once the remote sends a RevokeAndAck for the preceding
// commitment. On the error path the remote sends an Error instead of a
// RevokeAndAck, so the tip is guaranteed to be the commitment the remote
// rejected.
//
// This method is intended exclusively for error-path diagnostics. When the
// remote rejects our CommitSig, logging our commit TX alongside the one
// embedded in the peer's error message makes it possible to determine
// immediately whether the two sides derived different transactions (state
// divergence) or agreed on the same transaction but the signature still
// failed (signing bug).
func (lc *LightningChannel) BuildLastSignedRemoteCommitTx() ([]byte, error) {
lc.RLock()
defer lc.RUnlock()

// tip() is the back of the list — the most recently signed
// commitment.
lastCommit := lc.commitChains.Remote.tip()
if lastCommit == nil {
return nil, nil
}

var buf bytes.Buffer
if err := lastCommit.txn.Serialize(&buf); err != nil {
return nil, fmt.Errorf("serializing remote commit tx: %w", err)
}

return buf.Bytes(), nil
}

// resignMusigCommit is used to resign a commitment transaction for taproot
// channels when we need to retransmit a signature after a channel reestablish
// message. Taproot channels use musig2, which means we must use fresh nonces
Comment thread
ziggie1984 marked this conversation as resolved.
Expand Down
Loading