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

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

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