lnwallet: update commitmentKeyRing to account for distinct HTLC keys

In this commit, we update all the key derivation within the state
machine to account for the recent spec change which introduces a
distinct key for usages within all HTLC scripts. This change means that
the commitment payment and delay base points, are only required to be
online in the case that a party is forced to go to chain.

We introduce an additional local tweak to the keyring for the HTLC
tweak. Additionally, two new keys have been added: a local and a remote
HTLC key. Generation of sender/receiver HTLC scripts now use the local
and remote HTLC keys rather than the “payment” key for each party.
Finally, when creating/verifying signatures for second-level HTLC
transactions, we use these the distinct HTLC keys, rather than re-using
the payment keys.
This commit is contained in:
Olaoluwa Osuntokun 2017-11-14 20:48:49 -08:00
parent a812974808
commit d7cdf822e3
No known key found for this signature in database
GPG Key ID: 964EA263DD637C21
3 changed files with 106 additions and 63 deletions

@ -757,32 +757,46 @@ func (lc *LightningChannel) diskCommitToMemCommit(isLocal, isPendingCommit bool,
// is referred to in the field comments, regardless of which is local and which
// is remote.
type commitmentKeyRing struct {
// commitPoint is the "per commitment point" used to derive the tweak for
// each base point.
// commitPoint is the "per commitment point" used to derive the tweak
// for each base point.
commitPoint *btcec.PublicKey
// localKeyTweak is the tweak used to derive the local public key from the
// local payment base point or the local private key from the base point
// secret. This may be included in a SignDescriptor to generate signatures
// for the local payment key.
localKeyTweak []byte
// localCommitKeyTweak is the tweak used to derive the local public key
// from the local payment base point or the local private key from the
// base point secret. This may be included in a SignDescriptor to
// generate signatures for the local payment key.
localCommitKeyTweak []byte
// delayKey is the commitment transaction owner's key which is included in
// HTLC success and timeout transaction scripts.
// TODO(roasbeef): need delay tweak as well?
// localHtlcKeyTweak is the teak used to derive the local HTLC key from
// the local HTLC base point point. This value is needed in order to
// derive the final key used within the HTLC scripts in the commitment
// transaction.
localHtlcKeyTweak []byte
// localHtlcKey is the key that will be used in the "to self" clause of
// any HTLC scripts within the commitment transaction for this key ring
// set.
localHtlcKey *btcec.PublicKey
// remoteHtlcKey is the key that will be used in clauses within the
// HTLC script that send money to the remote party.
remoteHtlcKey *btcec.PublicKey
// delayKey is the commitment transaction owner's key which is included
// in HTLC success and timeout transaction scripts.
delayKey *btcec.PublicKey
// paymentKey is the other party's payment key in the commitment tx.
paymentKey *btcec.PublicKey
// noDelayKey is the other party's payment key in the commitment tx.
// This is the key used to generate the unencumbered output within the
// commitment transaction.
noDelayKey *btcec.PublicKey
// revocationKey is the key that can be used by the other party to redeem
// outputs from a revoked commitment transaction if it were to be published.
// revocationKey is the key that can be used by the other party to
// redeem outputs from a revoked commitment transaction if it were to
// be published.
revocationKey *btcec.PublicKey
// localKey is this node's payment key in the commitment tx.
localKey *btcec.PublicKey
// remoteKey is the remote node's payment key in the commitment tx.
remoteKey *btcec.PublicKey
}
// deriveCommitmentKey generates a new commitment key set using the base points
@ -790,32 +804,50 @@ type commitmentKeyRing struct {
// commitment transaction is ours or the remote peer's.
func deriveCommitmentKeys(commitPoint *btcec.PublicKey, isOurCommit bool,
localChanCfg, remoteChanCfg *channeldb.ChannelConfig) *commitmentKeyRing {
keyRing := new(commitmentKeyRing)
keyRing.commitPoint = commitPoint
keyRing.localKeyTweak = SingleTweakBytes(commitPoint,
localChanCfg.PaymentBasePoint)
keyRing.localKey = TweakPubKeyWithTweak(localChanCfg.PaymentBasePoint,
keyRing.localKeyTweak)
keyRing.remoteKey = TweakPubKey(remoteChanCfg.PaymentBasePoint, commitPoint)
// First, we'll derive all the keys that don't depend on the context of
// whose commitment transaction this is.
keyRing := &commitmentKeyRing{
commitPoint: commitPoint,
// We'll now compute the delay, payment and revocation key based on the
// current commitment point. All keys are tweaked each state in order
// to ensure the keys from each state are unlinkable. TO create the
// revocation key, we take the opposite party's revocation base point
// and combine that with the current commitment point.
var delayBasePoint, revocationBasePoint *btcec.PublicKey
localCommitKeyTweak: SingleTweakBytes(commitPoint,
localChanCfg.PaymentBasePoint),
localHtlcKeyTweak: SingleTweakBytes(commitPoint,
localChanCfg.HtlcBasePoint),
localHtlcKey: TweakPubKey(localChanCfg.HtlcBasePoint,
commitPoint),
remoteHtlcKey: TweakPubKey(remoteChanCfg.HtlcBasePoint,
commitPoint),
}
// We'll now compute the delay, no delay, and revocation key based on
// the current commitment point. All keys are tweaked each state in
// order to ensure the keys from each state are unlinkable. To create
// the revocation key, we take the opposite party's revocation base
// point and combine that with the current commitment point.
var (
delayBasePoint *btcec.PublicKey
noDelayBasePoint *btcec.PublicKey
revocationBasePoint *btcec.PublicKey
)
if isOurCommit {
keyRing.paymentKey = keyRing.remoteKey
delayBasePoint = localChanCfg.DelayBasePoint
noDelayBasePoint = remoteChanCfg.PaymentBasePoint
revocationBasePoint = remoteChanCfg.RevocationBasePoint
} else {
keyRing.paymentKey = keyRing.localKey
delayBasePoint = remoteChanCfg.DelayBasePoint
noDelayBasePoint = localChanCfg.PaymentBasePoint
revocationBasePoint = localChanCfg.RevocationBasePoint
}
// With the base points assigned, we can now derive the actual keys
// using the base point, and the current commitment tweak.
keyRing.delayKey = TweakPubKey(delayBasePoint, commitPoint)
keyRing.revocationKey = DeriveRevocationPubkey(revocationBasePoint, commitPoint)
keyRing.noDelayKey = TweakPubKey(noDelayBasePoint, commitPoint)
keyRing.revocationKey = DeriveRevocationPubkey(
revocationBasePoint, commitPoint,
)
return keyRing
}
@ -1743,7 +1775,7 @@ func newBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
if err != nil {
return nil, err
}
localPkScript, err := commitScriptUnencumbered(keyRing.localKey)
localPkScript, err := commitScriptUnencumbered(keyRing.noDelayKey)
if err != nil {
return nil, err
}
@ -1781,7 +1813,7 @@ func newBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
// instantiate the local sign descriptor.
if localAmt >= chanState.RemoteChanCfg.DustLimit {
localSignDesc = &SignDescriptor{
SingleTweak: keyRing.localKeyTweak,
SingleTweak: keyRing.localCommitKeyTweak,
PubKey: chanState.LocalChanCfg.PaymentBasePoint,
WitnessScript: localPkScript,
Output: &wire.TxOut{
@ -1821,8 +1853,10 @@ func newBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
// the sender of the HTLC (relative to us). So we'll
// re-generate the sender HTLC script.
if htlc.Incoming {
htlcScript, err = senderHTLCScript(keyRing.localKey,
keyRing.remoteKey, keyRing.revocationKey, htlc.RHash[:])
htlcScript, err = senderHTLCScript(
keyRing.localHtlcKey, keyRing.remoteHtlcKey,
keyRing.revocationKey, htlc.RHash[:],
)
if err != nil {
return nil, err
}
@ -1832,8 +1866,9 @@ func newBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
// receiver of this HTLC.
} else {
htlcScript, err = receiverHTLCScript(
htlc.RefundTimeout, keyRing.localKey, keyRing.remoteKey,
keyRing.revocationKey, htlc.RHash[:],
htlc.RefundTimeout, keyRing.localHtlcKey,
keyRing.remoteHtlcKey, keyRing.revocationKey,
htlc.RHash[:],
)
if err != nil {
return nil, err
@ -2010,7 +2045,7 @@ func (lc *LightningChannel) closeObserver(channelCloseNtfn *chainntnfs.SpendEven
// Before we can generate the proper sign descriptor, we'll
// need to locate the output index of our non-delayed output on
// the commitment transaction.
selfP2WKH, err := commitScriptUnencumbered(keyRing.localKey)
selfP2WKH, err := commitScriptUnencumbered(keyRing.noDelayKey)
if err != nil {
walletLog.Errorf("unable to create self commit "+
"script: %v", err)
@ -2036,7 +2071,7 @@ func (lc *LightningChannel) closeObserver(channelCloseNtfn *chainntnfs.SpendEven
localBalance := lc.channelState.LocalCommitment.LocalBalance.ToSatoshis()
selfSignDesc = &SignDescriptor{
PubKey: localPayBase,
SingleTweak: keyRing.localKeyTweak,
SingleTweak: keyRing.localCommitKeyTweak,
WitnessScript: selfP2WKH,
Output: &wire.TxOut{
Value: int64(localBalance),
@ -2662,8 +2697,8 @@ func genRemoteHtlcSigJobs(keyRing *commitmentKeyRing,
// signature to give to the remote party for this commitment
// transaction. Note we use the raw HTLC amount.
sigJob.signDesc = SignDescriptor{
PubKey: localChanCfg.PaymentBasePoint,
SingleTweak: keyRing.localKeyTweak,
PubKey: localChanCfg.HtlcBasePoint,
SingleTweak: keyRing.localHtlcKeyTweak,
WitnessScript: htlc.theirWitnessScript,
Output: &wire.TxOut{
Value: int64(htlc.Amount.ToSatoshis()),
@ -2712,8 +2747,8 @@ func genRemoteHtlcSigJobs(keyRing *commitmentKeyRing,
// signature to give to the remote party for this commitment
// transaction. Note we use the raw HTLC amount.
sigJob.signDesc = SignDescriptor{
PubKey: localChanCfg.PaymentBasePoint,
SingleTweak: keyRing.localKeyTweak,
PubKey: localChanCfg.HtlcBasePoint,
SingleTweak: keyRing.localHtlcKeyTweak,
WitnessScript: htlc.theirWitnessScript,
Output: &wire.TxOut{
Value: int64(htlc.Amount.ToSatoshis()),
@ -3317,7 +3352,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment,
}
verifyJobs = append(verifyJobs, verifyJob{
pubKey: keyRing.remoteKey,
pubKey: keyRing.remoteHtlcKey,
sig: htlcSigs[i],
sigHash: sigHash,
})
@ -3952,29 +3987,30 @@ func genHtlcScript(isIncoming, ourCommit bool, timeout uint32, rHash [32]byte,
// transaction. So we need to use the receiver's version of HTLC the
// script.
case isIncoming && ourCommit:
witnessScript, err = receiverHTLCScript(timeout, keyRing.remoteKey,
keyRing.localKey, keyRing.revocationKey, rHash[:])
witnessScript, err = receiverHTLCScript(timeout,
keyRing.remoteHtlcKey, keyRing.localHtlcKey,
keyRing.revocationKey, rHash[:])
// We're being paid via an HTLC by the remote party, and the HTLC is
// being added to their commitment transaction, so we use the sender's
// version of the HTLC script.
case isIncoming && !ourCommit:
witnessScript, err = senderHTLCScript(keyRing.remoteKey,
keyRing.localKey, keyRing.revocationKey, rHash[:])
witnessScript, err = senderHTLCScript(keyRing.remoteHtlcKey,
keyRing.localHtlcKey, keyRing.revocationKey, rHash[:])
// We're sending an HTLC which is being added to our commitment
// transaction. Therefore, we need to use the sender's version of the
// HTLC script.
case !isIncoming && ourCommit:
witnessScript, err = senderHTLCScript(keyRing.localKey,
keyRing.remoteKey, keyRing.revocationKey, rHash[:])
witnessScript, err = senderHTLCScript(keyRing.localHtlcKey,
keyRing.remoteHtlcKey, keyRing.revocationKey, rHash[:])
// Finally, we're paying the remote party via an HTLC, which is being
// added to their commitment transaction. Therefore, we use the
// receiver's version of the HTLC script.
case !isIncoming && !ourCommit:
witnessScript, err = receiverHTLCScript(timeout, keyRing.localKey,
keyRing.remoteKey, keyRing.revocationKey, rHash[:])
witnessScript, err = receiverHTLCScript(timeout, keyRing.localHtlcKey,
keyRing.remoteHtlcKey, keyRing.revocationKey, rHash[:])
}
if err != nil {
return nil, nil, err
@ -4148,17 +4184,19 @@ func newHtlcResolution(signer Signer, localChanCfg *channeldb.ChannelConfig,
return nil, err
}
// TODO(roasbeef): branch based on local vs remote commit
// With the transaction created, we can generate a sign descriptor
// that's capable of generating the signature required to spend the
// HTLC output using the timeout transaction.
htlcCreationScript, err := senderHTLCScript(keyRing.localKey,
keyRing.remoteKey, keyRing.revocationKey, htlc.RHash[:])
htlcCreationScript, err := senderHTLCScript(keyRing.localHtlcKey,
keyRing.remoteHtlcKey, keyRing.revocationKey, htlc.RHash[:])
if err != nil {
return nil, err
}
timeoutSignDesc := SignDescriptor{
PubKey: localChanCfg.PaymentBasePoint,
SingleTweak: keyRing.localKeyTweak,
PubKey: localChanCfg.HtlcBasePoint,
SingleTweak: keyRing.localHtlcKeyTweak,
WitnessScript: htlcCreationScript,
Output: &wire.TxOut{
Value: int64(htlc.Amt.ToSatoshis()),
@ -4191,7 +4229,9 @@ func newHtlcResolution(signer Signer, localChanCfg *channeldb.ChannelConfig,
return nil, err
}
// TODO: Signing with the delay key is wrong for remote commitments
// TODO(roasbeef): signing with the delay key is wrong for remote
// commitments
// * would instead be signing with local htlc key
localDelayTweak := SingleTweakBytes(keyRing.commitPoint,
localChanCfg.DelayBasePoint)
return &OutgoingHtlcResolution{
@ -4808,7 +4848,7 @@ func CreateCommitTx(fundingOutput *wire.TxIn,
// Next, we create the script paying to them. This is just a regular
// P2WPKH output, without any added CSV delay.
theirWitnessKeyHash, err := commitScriptUnencumbered(keyRing.paymentKey)
theirWitnessKeyHash, err := commitScriptUnencumbered(keyRing.noDelayKey)
if err != nil {
return nil, err
}

@ -225,6 +225,7 @@ func createTestChannels(revocationWindow int) (*LightningChannel, *LightningChan
}
fundingTxIn := wire.NewTxIn(prevOut, nil, nil)
// TODO(roasbeef): use distinct keys
aliceCfg := channeldb.ChannelConfig{
ChannelConstraints: channeldb.ChannelConstraints{
DustLimit: aliceDustLimit,
@ -238,6 +239,7 @@ func createTestChannels(revocationWindow int) (*LightningChannel, *LightningChan
RevocationBasePoint: aliceKeyPub,
PaymentBasePoint: aliceKeyPub,
DelayBasePoint: aliceKeyPub,
HtlcBasePoint: aliceKeyPub,
}
bobCfg := channeldb.ChannelConfig{
ChannelConstraints: channeldb.ChannelConstraints{
@ -252,6 +254,7 @@ func createTestChannels(revocationWindow int) (*LightningChannel, *LightningChan
RevocationBasePoint: bobKeyPub,
PaymentBasePoint: bobKeyPub,
DelayBasePoint: bobKeyPub,
HtlcBasePoint: bobKeyPub,
}
bobRoot := DeriveRevocationRoot(bobKeyPriv, testHdSeed, aliceKeyPub)

@ -74,7 +74,7 @@ func TestCommitmentSpendValidation(t *testing.T) {
keyRing := &commitmentKeyRing{
delayKey: aliceDelayKey,
revocationKey: revokePubKey,
paymentKey: bobPayKey,
noDelayKey: bobPayKey,
}
commitmentTx, err := CreateCommitTx(fakeFundingTxIn, keyRing, csvTimeout,
channelBalance, channelBalance, DefaultDustLimit())