Skip to content

Commit 7a5aa99

Browse files
committed
lnwallet+htlcswitch: add fuzz-friendly commitment key deriver hook
Introduce CommitKeyDeriverFunc and WithCommitKeyDeriver to allow LightningChannel to bypass the secp256k1-based DeriveCommitmentKeys on every commit round. All internal call sites are migrated to lc.deriveCommitmentKeys. The fuzz harness injects fuzzCommitKeyDeriver, a trivial identity deriver that avoids scalar-multiplication overhead.
1 parent ba4ea70 commit 7a5aa99

File tree

3 files changed

+96
-8
lines changed

3 files changed

+96
-8
lines changed

htlcswitch/fuzz_link_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/btcsuite/btcd/btcutil"
1616
"github.com/btcsuite/btcd/txscript"
1717
"github.com/btcsuite/btcd/wire"
18+
"github.com/lightningnetwork/lnd/channeldb"
1819
"github.com/lightningnetwork/lnd/fn/v2"
1920
"github.com/lightningnetwork/lnd/htlcswitch/hop"
2021
"github.com/lightningnetwork/lnd/input"
@@ -99,6 +100,52 @@ func fuzzSigVerifier(sig input.Signature, sigHash []byte,
99100
return bytes.Equal(sBytes, expected[:])
100101
}
101102

103+
// fuzzCommitKeyDeriver is a trivial CommitKeyDeriverFunc for fuzz harnesses.
104+
// It mirrors the local/remote base-point selection of DeriveCommitmentKeys but
105+
// returns the raw base points without any secp256k1 scalar multiplication,
106+
// eliminating the ~30% CPU overhead of TweakPubKey/DeriveRevocationPubkey on
107+
// every commit round. Both Alice and Bob call this with mirrored arguments and
108+
// arrive at the same underlying public keys, so commitment tx scripts remain
109+
// consistent across both sides.
110+
func fuzzCommitKeyDeriver(commitPoint *btcec.PublicKey,
111+
whoseCommit lntypes.ChannelParty, _ channeldb.ChannelType, localChanCfg,
112+
remoteChanCfg *channeldb.ChannelConfig) *lnwallet.CommitmentKeyRing {
113+
114+
localBasePoint := localChanCfg.PaymentBasePoint
115+
if whoseCommit.IsLocal() {
116+
localBasePoint = localChanCfg.DelayBasePoint
117+
}
118+
119+
var toLocalKey, toRemoteKey, revocationKey *btcec.PublicKey
120+
if whoseCommit.IsLocal() {
121+
toLocalKey = localChanCfg.DelayBasePoint.PubKey
122+
toRemoteKey = remoteChanCfg.PaymentBasePoint.PubKey
123+
revocationKey = remoteChanCfg.RevocationBasePoint.PubKey
124+
} else {
125+
toLocalKey = remoteChanCfg.DelayBasePoint.PubKey
126+
toRemoteKey = localChanCfg.PaymentBasePoint.PubKey
127+
revocationKey = localChanCfg.RevocationBasePoint.PubKey
128+
}
129+
130+
return &lnwallet.CommitmentKeyRing{
131+
CommitPoint: commitPoint,
132+
// Tweaks are cheap (just SHA256), keep them accurate.
133+
LocalCommitKeyTweak: input.SingleTweakBytes(
134+
commitPoint, localBasePoint.PubKey,
135+
),
136+
LocalHtlcKeyTweak: input.SingleTweakBytes(
137+
commitPoint, localChanCfg.HtlcBasePoint.PubKey,
138+
),
139+
// Skip TweakPubKey/DeriveRevocationPubkey — return base points
140+
// directly to avoid secp256k1 scalar multiplications.
141+
LocalHtlcKey: localChanCfg.HtlcBasePoint.PubKey,
142+
RemoteHtlcKey: remoteChanCfg.HtlcBasePoint.PubKey,
143+
ToLocalKey: toLocalKey,
144+
ToRemoteKey: toRemoteKey,
145+
RevocationKey: revocationKey,
146+
}
147+
}
148+
102149
type Event uint8
103150

104151
const (
@@ -207,6 +254,7 @@ func newFuzzFSM(t *testing.T, channelSize, aliceShareGen uint64) *fuzzFSM {
207254
withTestSignerFactory(mkFuzzSigner),
208255
withTestChanOpts(
209256
lnwallet.WithSigVerifier(fuzzSigVerifier),
257+
lnwallet.WithCommitKeyDeriver(fuzzCommitKeyDeriver),
210258
),
211259
)
212260
require.NoError(t, err)

lnwallet/channel.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -647,15 +647,15 @@ func (lc *LightningChannel) diskCommitToMemCommit(
647647
// haven't yet received a responding commitment from the remote party.
648648
var commitKeys lntypes.Dual[*CommitmentKeyRing]
649649
if localCommitPoint != nil {
650-
commitKeys.SetForParty(lntypes.Local, DeriveCommitmentKeys(
650+
commitKeys.SetForParty(lntypes.Local, lc.deriveCommitmentKeys(
651651
localCommitPoint, lntypes.Local,
652652
lc.channelState.ChanType,
653653
&lc.channelState.LocalChanCfg,
654654
&lc.channelState.RemoteChanCfg,
655655
))
656656
}
657657
if remoteCommitPoint != nil {
658-
commitKeys.SetForParty(lntypes.Remote, DeriveCommitmentKeys(
658+
commitKeys.SetForParty(lntypes.Remote, lc.deriveCommitmentKeys(
659659
remoteCommitPoint, lntypes.Remote,
660660
lc.channelState.ChanType,
661661
&lc.channelState.LocalChanCfg,
@@ -864,6 +864,10 @@ type channelOpts struct {
864864
// standard sig.Verify method is used.
865865
sigVerifier SigVerifier
866866

867+
// commitKeyDeriver is an optional override for DeriveCommitmentKeys.
868+
// When nil, the real secp256k1-based function is used.
869+
commitKeyDeriver CommitKeyDeriverFunc
870+
867871
skipNonceInit bool
868872
}
869873

@@ -942,6 +946,25 @@ func (lc *LightningChannel) verifySig(sig input.Signature, sigHash []byte,
942946
return sig.Verify(sigHash, pubKey)
943947
}
944948

949+
// deriveCommitmentKeys calls the injected CommitKeyDeriverFunc if one is set,
950+
// otherwise falls back to the real secp256k1-based DeriveCommitmentKeys.
951+
func (lc *LightningChannel) deriveCommitmentKeys(commitPoint *btcec.PublicKey,
952+
whoseCommit lntypes.ChannelParty, chanType channeldb.ChannelType,
953+
localChanCfg, remoteChanCfg *channeldb.ChannelConfig) *CommitmentKeyRing { //nolint:ll
954+
955+
if lc.opts.commitKeyDeriver != nil {
956+
return lc.opts.commitKeyDeriver(
957+
commitPoint, whoseCommit, chanType,
958+
localChanCfg, remoteChanCfg,
959+
)
960+
}
961+
962+
return DeriveCommitmentKeys(
963+
commitPoint, whoseCommit, chanType,
964+
localChanCfg, remoteChanCfg,
965+
)
966+
}
967+
945968
// NewLightningChannel creates a new, active payment channel given an
946969
// implementation of the chain notifier, channel database, and the current
947970
// settled channel state. Throughout state transitions, then channel will
@@ -1565,7 +1588,7 @@ func (lc *LightningChannel) restoreCommitState(
15651588

15661589
// We'll also re-create the set of commitment keys needed to
15671590
// fully re-derive the state.
1568-
pendingRemoteKeyChain = DeriveCommitmentKeys(
1591+
pendingRemoteKeyChain = lc.deriveCommitmentKeys(
15691592
pendingCommitPoint, lntypes.Remote,
15701593
lc.channelState.ChanType,
15711594
&lc.channelState.LocalChanCfg,
@@ -4164,7 +4187,7 @@ func (lc *LightningChannel) SignNextCommitment(
41644187
// Grab the next commitment point for the remote party. This will be
41654188
// used within fetchCommitmentView to derive all the keys necessary to
41664189
// construct the commitment state.
4167-
keyRing := DeriveCommitmentKeys(
4190+
keyRing := lc.deriveCommitmentKeys(
41684191
commitPoint, lntypes.Remote, lc.channelState.ChanType,
41694192
&lc.channelState.LocalChanCfg, &lc.channelState.RemoteChanCfg,
41704193
)
@@ -5365,7 +5388,7 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error {
53655388
return err
53665389
}
53675390
commitPoint := input.ComputeCommitmentPoint(commitSecret[:])
5368-
keyRing := DeriveCommitmentKeys(
5391+
keyRing := lc.deriveCommitmentKeys(
53695392
commitPoint, lntypes.Local, lc.channelState.ChanType,
53705393
&lc.channelState.LocalChanCfg, &lc.channelState.RemoteChanCfg,
53715394
)
@@ -8901,7 +8924,7 @@ func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions,
89018924
return nil, err
89028925
}
89038926
localCommitPoint := input.ComputeCommitmentPoint(revocation[:])
8904-
localKeyRing := DeriveCommitmentKeys(
8927+
localKeyRing := lc.deriveCommitmentKeys(
89058928
localCommitPoint, lntypes.Local, lc.channelState.ChanType,
89068929
&lc.channelState.LocalChanCfg, &lc.channelState.RemoteChanCfg,
89078930
)
@@ -8915,7 +8938,7 @@ func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions,
89158938
resolutions.Local = localRes
89168939

89178940
// Add anchor for remote commitment tx, if any.
8918-
remoteKeyRing := DeriveCommitmentKeys(
8941+
remoteKeyRing := lc.deriveCommitmentKeys(
89198942
lc.channelState.RemoteCurrentRevocation, lntypes.Remote,
89208943
lc.channelState.ChanType, &lc.channelState.LocalChanCfg,
89218944
&lc.channelState.RemoteChanCfg,
@@ -8936,7 +8959,7 @@ func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions,
89368959
}
89378960

89388961
if remotePendingCommit != nil {
8939-
pendingRemoteKeyRing := DeriveCommitmentKeys(
8962+
pendingRemoteKeyRing := lc.deriveCommitmentKeys(
89408963
lc.channelState.RemoteNextRevocation, lntypes.Remote,
89418964
lc.channelState.ChanType, &lc.channelState.LocalChanCfg,
89428965
&lc.channelState.RemoteChanCfg,

lnwallet/mock.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,3 +541,20 @@ func WithSigVerifier(v SigVerifier) ChannelOpt {
541541
o.sigVerifier = v
542542
}
543543
}
544+
545+
// CommitKeyDeriverFunc is an optional function that overrides
546+
// DeriveCommitmentKeys inside LightningChannel. When nil, the real
547+
// secp256k1-based derivation is used. Inject a trivial version in fuzz/test
548+
// harnesses to avoid scalar-multiplication overhead on every commit round.
549+
type CommitKeyDeriverFunc func(commitPoint *btcec.PublicKey,
550+
whoseCommit lntypes.ChannelParty, chanType channeldb.ChannelType,
551+
localChanCfg, remoteChanCfg *channeldb.ChannelConfig) *CommitmentKeyRing
552+
553+
// WithCommitKeyDeriver injects a custom commitment key derivation function,
554+
// overriding the default secp256k1-based DeriveCommitmentKeys on every commit
555+
// round. Intended for fuzz/test harnesses that need to avoid scalar-mult cost.
556+
func WithCommitKeyDeriver(fn CommitKeyDeriverFunc) ChannelOpt {
557+
return func(o *channelOpts) {
558+
o.commitKeyDeriver = fn
559+
}
560+
}

0 commit comments

Comments
 (0)