Skip to content

lnwire+lnwallet: add LocalNonces field for splice nonce coordination w/ taproot channels #9982

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: taproot-final-scripts
Choose a base branch
from
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
18 changes: 16 additions & 2 deletions channeldb/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -1815,7 +1815,10 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) {
// If this is a taproot channel, then we'll need to generate our next
// verification nonce to send to the remote party. They'll use this to
// sign the next update to our commitment transaction.
var nextTaprootNonce lnwire.OptMusig2NonceTLV
var (
nextTaprootNonce lnwire.OptMusig2NonceTLV
nextLocalNonces lnwire.OptLocalNonces
)
if c.ChanType.IsTaproot() {
taprootRevProducer, err := DeriveMusig2Shachain(
c.RevocationProducer,
Expand All @@ -1833,7 +1836,17 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) {
"nonce: %w", err)
}

// Populate the legacy LocalNonce field for backwards compatibility.
nextTaprootNonce = lnwire.SomeMusig2Nonce(nextNonce.PubNonce)

// Also populate the new LocalNonces field. For channel
// re-establishment, we use an empty hash as the key to indicate
// the primary commitment nonce.
noncesMap := make(map[chainhash.Hash]lnwire.Musig2Nonce)
noncesMap[chainhash.Hash{}] = nextNonce.PubNonce
nextLocalNonces = lnwire.SomeLocalNonces(
lnwire.LocalNoncesData{NoncesMap: noncesMap},
)
}

return &lnwire.ChannelReestablish{
Expand All @@ -1846,7 +1859,8 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) {
LocalUnrevokedCommitPoint: input.ComputeCommitmentPoint(
currentCommitSecret[:],
),
LocalNonce: nextTaprootNonce,
LocalNonce: nextTaprootNonce,
LocalNonces: nextLocalNonces,
}, nil
}

Expand Down
40 changes: 40 additions & 0 deletions lnwallet/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -4273,7 +4273,47 @@ func (lc *LightningChannel) ProcessChanSyncMsg(ctx context.Context,
// has sent the next verification nonce. If they haven't, then we'll
// bail out, otherwise we'll init our local session then continue as
// normal.
// If this is a taproot channel, then we need to handle the remote
// party's MuSig2 nonce. We prioritize the new LocalNonces field over
// the legacy LocalNonce field for backwards compatibility.
switch {
case lc.channelState.ChanType.IsTaproot() && msg.LocalNonces.IsSome():
// Handle the new LocalNonces field which contains a map of nonces.
noncesData, err := msg.LocalNonces.UnwrapOrErr(
fmt.Errorf("failed to unwrap LocalNonces"),
)
if err != nil {
return nil, nil, nil, err
}

// For channel re-establishment, we expect the nonce for the main
// commitment. For now, we take the first nonce from the map.
// TODO: Define proper key semantics for different nonce purposes.
var commitNonce *lnwire.Musig2Nonce
for _, nonce := range noncesData.NoncesMap {
n := nonce
commitNonce = &n
break
}

if commitNonce == nil {
return nil, nil, nil, fmt.Errorf("remote verification nonce " +
"not sent")
}

if !lc.opts.skipNonceInit {
err = lc.InitRemoteMusigNonces(&musig2.Nonces{
PubNonce: *commitNonce,
})
if err != nil {
return nil, nil, nil, fmt.Errorf("unable to init "+
"remote nonce: %w", err)
}
}

// TODO: Store the full nonces map in channel state for other
// purposes like pending splices.

case lc.channelState.ChanType.IsTaproot() && msg.LocalNonce.IsNone():
return nil, nil, nil, fmt.Errorf("remote verification nonce " +
"not sent")
Expand Down
91 changes: 88 additions & 3 deletions lnwallet/channel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3073,6 +3073,25 @@ func TestAddHTLCNegativeBalance(t *testing.T) {
// assertNoChanSyncNeeded is a helper function that asserts that upon restart,
// two channels conclude that they're fully synchronized and don't need to
// retransmit any new messages.
// extractCommitmentNonce extracts the commitment nonce from a ChannelReestablish
// message, prioritizing LocalNonces over the legacy LocalNonce field.
func extractCommitmentNonce(t *testing.T, msg *lnwire.ChannelReestablish) lnwire.Musig2Nonce {
// Prefer LocalNonces if present
if msg.LocalNonces.IsSome() {
noncesData := msg.LocalNonces.UnwrapOrFail(t)

// Return the first nonce (for main commitment)
for _, nonce := range noncesData.NoncesMap {
return nonce
}

// If map is empty, fall back to LocalNonce
}

// Fall back to legacy LocalNonce field
return msg.LocalNonce.UnwrapOrFailV(t)
}

func assertNoChanSyncNeeded(t *testing.T, aliceChannel *LightningChannel,
bobChannel *LightningChannel) {

Expand All @@ -3090,13 +3109,14 @@ func assertNoChanSyncNeeded(t *testing.T, aliceChannel *LightningChannel,
}

// For taproot channels, simulate the link/peer binding the generated
// nonces.
// nonces. Use helper to extract nonces from either LocalNonces or
// LocalNonce.
if aliceChannel.channelState.ChanType.IsTaproot() {
aliceChannel.pendingVerificationNonce = &musig2.Nonces{
PubNonce: aliceChanSyncMsg.LocalNonce.UnwrapOrFailV(t),
PubNonce: extractCommitmentNonce(t, aliceChanSyncMsg),
}
bobChannel.pendingVerificationNonce = &musig2.Nonces{
PubNonce: bobChanSyncMsg.LocalNonce.UnwrapOrFailV(t),
PubNonce: extractCommitmentNonce(t, bobChanSyncMsg),
}
}

Expand Down Expand Up @@ -3538,6 +3558,71 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
}
}

// TestChanSyncTaprootLocalNonces tests that for taproot channels, both the
// legacy LocalNonce field and the new LocalNonces field are populated in
// ChannelReestablish messages, and that the receiving side can handle either.
func TestChanSyncTaprootLocalNonces(t *testing.T) {
t.Parallel()

// Create a taproot test channel.
chanType := channeldb.SimpleTaprootFeatureBit
aliceChannel, bobChannel, err := CreateTestChannels(t, chanType)
require.NoError(t, err, "unable to create test channels")

// Both sides should be fully synced from the start.
assertNoChanSyncNeeded(t, aliceChannel, bobChannel)

// Generate ChannelReestablish messages.
aliceChanSyncMsg, err := aliceChannel.channelState.ChanSyncMsg()
require.NoError(t, err, "unable to produce chan sync msg")
bobChanSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
require.NoError(t, err, "unable to produce chan sync msg")

// For taproot channels, both LocalNonce and LocalNonces should be populated.
require.True(t, aliceChanSyncMsg.LocalNonce.IsSome(), "LocalNonce should be set")
require.True(t, aliceChanSyncMsg.LocalNonces.IsSome(), "LocalNonces should be set")
require.True(t, bobChanSyncMsg.LocalNonce.IsSome(), "LocalNonce should be set")
require.True(t, bobChanSyncMsg.LocalNonces.IsSome(), "LocalNonces should be set")

// The nonces from both fields should be identical.
aliceLegacyNonce := aliceChanSyncMsg.LocalNonce.UnwrapOrFailV(t)
aliceNoncesData := aliceChanSyncMsg.LocalNonces.UnwrapOrFail(t)
require.Len(t, aliceNoncesData.NoncesMap, 1, "should have exactly one nonce")
var aliceMapNonce lnwire.Musig2Nonce
for _, nonce := range aliceNoncesData.NoncesMap {
aliceMapNonce = nonce
break
}
require.Equal(t, aliceLegacyNonce, aliceMapNonce, "nonces should match")

// Test that our helper function works correctly.
extractedNonce := extractCommitmentNonce(t, aliceChanSyncMsg)
require.Equal(t, aliceLegacyNonce, extractedNonce, "helper should extract correct nonce")

// Test sync behavior when only LocalNonces is present by clearing LocalNonce.
aliceModifiedMsg := *aliceChanSyncMsg
aliceModifiedMsg.LocalNonce = lnwire.OptMusig2NonceTLV{}

// Bob should still be able to process the message with only LocalNonces.
bobChannel.pendingVerificationNonce = &musig2.Nonces{
PubNonce: extractCommitmentNonce(t, bobChanSyncMsg),
}
bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg(
ctxb, &aliceModifiedMsg,
)
require.NoError(t, err, "unable to process modified ChannelReestablish msg")
require.Empty(t, bobMsgsToSend, "bob shouldn't need to send messages")

// Test that missing both fields results in an error.
aliceEmptyMsg := *aliceChanSyncMsg
aliceEmptyMsg.LocalNonce = lnwire.OptMusig2NonceTLV{}
aliceEmptyMsg.LocalNonces = lnwire.OptLocalNonces{}

_, _, _, err = bobChannel.ProcessChanSyncMsg(ctxb, &aliceEmptyMsg)
require.Error(t, err, "should error when no nonce is provided")
require.Contains(t, err.Error(), "remote verification nonce not sent")
}

// TestChanSyncOweCommitment tests that if Bob restarts (and then Alice) before
// he receives Alice's CommitSig message, then Alice concludes that she needs
// to re-send the CommitDiff. After the diff has been sent, both nodes should
Expand Down
22 changes: 16 additions & 6 deletions lnwire/channel_reestablish.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ type ChannelReestablish struct {
// a dynamic commitment negotiation
DynHeight fn.Option[DynHeight]

// LocalNonces is an optional field that stores a map of local musig2
// nonces, keyed by TXID. This is used for splice nonce coordination.
LocalNonces OptLocalNonces

// ExtraData is the set of data that was appended to this message to
// fill out the full maximum transport message size. These fields can
// be used to specify optional data such as custom TLV fields.
Expand Down Expand Up @@ -140,19 +144,21 @@ func (a *ChannelReestablish) Encode(w *bytes.Buffer, pver uint32) error {
return err
}

recordProducers := make([]tlv.RecordProducer, 0, 1)
recordProducers := make([]tlv.RecordProducer, 0, 3)
a.LocalNonce.WhenSome(func(localNonce Musig2NonceTLV) {
recordProducers = append(recordProducers, &localNonce)
})
a.DynHeight.WhenSome(func(h DynHeight) {
recordProducers = append(recordProducers, &h)
})
a.LocalNonces.WhenSome(func(ln LocalNoncesData) {
recordProducers = append(recordProducers, &ln)
})

err := EncodeMessageExtraData(&a.ExtraData, recordProducers...)
if err != nil {
return err
}

return WriteBytes(w, a.ExtraData)
}

Expand Down Expand Up @@ -207,11 +213,13 @@ func (a *ChannelReestablish) Decode(r io.Reader, pver uint32) error {
}

var (
dynHeight DynHeight
localNonce = a.LocalNonce.Zero()
dynHeight DynHeight
localNonce = a.LocalNonce.Zero()
localNoncesData LocalNoncesData
)

typeMap, err := tlvRecords.ExtractRecords(
&localNonce, &dynHeight,
&localNonce, &dynHeight, &localNoncesData,
)
if err != nil {
return err
Expand All @@ -223,11 +231,13 @@ func (a *ChannelReestablish) Decode(r io.Reader, pver uint32) error {
if val, ok := typeMap[CRDynHeight]; ok && val == nil {
a.DynHeight = fn.Some(dynHeight)
}
if val, ok := typeMap[(LocalNoncesRecordTypeDef)(nil).TypeVal()]; ok && val == nil {
a.LocalNonces = SomeLocalNonces(localNoncesData)
}

if len(tlvRecords) != 0 {
a.ExtraData = tlvRecords
}

return nil
}

Expand Down
Loading
Loading