lnwallet: integrate new taproot channels into internal funding flow

In this commit, we build on all the prior commits and integrate the new
taproot channels into the existing internal funding flow. Along the way,
we do some refactoring to unify things like signing and verifying
incoming commitment transaction signatures.

For our local nonce, we use the existing functional option type to
derive the nonce based on the initial shachain pre-image we'll use as
our revocation.
This commit is contained in:
Olaoluwa Osuntokun 2023-01-19 17:15:12 -08:00
parent 9e8b00241f
commit 67ecefaac3
No known key found for this signature in database
GPG Key ID: 3BBD59E99B280306
6 changed files with 315 additions and 108 deletions

View File

@ -2586,7 +2586,6 @@ func createBreachRetribution(revokedLog *channeldb.RevocationLog,
// Read the amounts from the breach transaction.
theirAmt = spendTx.TxOut[theirOutpoint.Index].Value
} else {
// Otherwise, we check to see if the revocation log
// contains remote parties' output amount. Due to a

View File

@ -2,12 +2,15 @@ package lnwallet
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"fmt"
"io"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/input"
@ -545,3 +548,27 @@ func NewMusigPairSession(cfg *MusigSessionCfg) *MusigPairSession {
signer: cfg.Signer,
}
}
var (
// taprootRevRootKey is the key used to derive the revocation root for
// the taproot nonces. This is done via HMAC of the existing revocation
// root.
taprootRevRootKey = []byte("taproot-rev-root")
)
// deriveMusig2Shachain derives a shachain producer for the taproot channel
// from normal shachain revocation root.
func deriveMusig2Shachain(revRoot chainhash.Hash) (shachain.Producer, error) {
// For taproot channel types, we'll also generate a distinct shachain
// root using the same seed information. We'll use this to generate
// verification nonces for the channel. We'll bind with this a simple
// hmac.
taprootRevHmac := hmac.New(sha256.New, taprootRevRootKey)
taprootRevRoot := taprootRevHmac.Sum(nil)
// Once we have the root, we can then generate our shachain producer
// and from that generate the per-commitment point.
return shachain.NewRevocationProducerFromBytes(
taprootRevRoot,
)
}

View File

@ -46,7 +46,7 @@ const (
// CommitmentTypeSimpleTaproot is the base commitment type for the
// channels that use a musig2 funding output and the tapscript tree
// where relevant for the commitment transaciton pk scripts.
// where relevant for the commitment transaction pk scripts.
CommitmentTypeSimpleTaproot
)

View File

@ -8,17 +8,19 @@ import (
)
// nextRevocationProducer creates a new revocation producer, deriving the
// revocation root by applying ECDH to a new key from our revocation root family
// and the multisig key we use for the channel.
// revocation root by applying ECDH to a new key from our revocation root
// family and the multisig key we use for the channel. For taproot channels a
// related shachain revocation root is also returned.
func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation,
keyRing keychain.KeyRing) (shachain.Producer, error) {
keyRing keychain.KeyRing,
) (shachain.Producer, shachain.Producer, error) {
// Derive the next key in the revocation root family.
nextRevocationKeyDesc, err := keyRing.DeriveNextKey(
keychain.KeyFamilyRevocationRoot,
)
if err != nil {
return nil, err
return nil, nil, err
}
// If the DeriveNextKey call returns the first key with Index 0, we need
@ -29,7 +31,7 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation,
keychain.KeyFamilyRevocationRoot,
)
if err != nil {
return nil, err
return nil, nil, err
}
}
@ -42,10 +44,16 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation,
nextRevocationKeyDesc, res.ourContribution.MultiSigKey.PubKey,
)
if err != nil {
return nil, err
return nil, nil, err
}
// Once we have the root, we can then generate our shachain producer
// and from that generate the per-commitment point.
return shachain.NewRevocationProducer(revRoot), nil
shaChainRoot := shachain.NewRevocationProducer(revRoot)
taprootShaChainRoot, err := deriveMusig2Shachain(revRoot)
if err != nil {
return nil, nil, err
}
return shaChainRoot, taprootShaChainRoot, nil
}

View File

@ -12,14 +12,15 @@ import (
// revocation root by applying ECDH to a new key from our revocation root family
// and the multisig key we use for the channel.
func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation,
keyRing keychain.KeyRing) (shachain.Producer, error) {
keyRing keychain.KeyRing,
) (shachain.Producer, shachain.Producer, error) {
// Derive the next key in the revocation root family.
nextRevocationKeyDesc, err := keyRing.DeriveNextKey(
keychain.KeyFamilyRevocationRoot,
)
if err != nil {
return nil, err
return nil, nil, err
}
// Within our itests, we want to make sure we can still restore channel
@ -34,17 +35,23 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation,
if res.pendingChanID == itestLegacyFormatChanID {
revocationRoot, err := l.DerivePrivKey(nextRevocationKeyDesc)
if err != nil {
return nil, err
return nil, nil, err
}
// Once we have the root, we can then generate our shachain
// producer and from that generate the per-commitment point.
revRoot, err := chainhash.NewHash(revocationRoot.Serialize())
if err != nil {
return nil, err
return nil, nil, err
}
shaChainRoot := shachain.NewRevocationProducer(*revRoot)
taprootShaChainRoot, err := deriveMusig2Shachain(*revRoot)
if err != nil {
return nil, nil, err
}
return shachain.NewRevocationProducer(*revRoot), nil
return shaChainRoot, taprootShaChainRoot, nil
}
// If the DeriveNextKey call returns the first key with Index 0, we need
@ -55,7 +62,7 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation,
keychain.KeyFamilyRevocationRoot,
)
if err != nil {
return nil, err
return nil, nil, err
}
}
@ -68,10 +75,16 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation,
nextRevocationKeyDesc, res.ourContribution.MultiSigKey.PubKey,
)
if err != nil {
return nil, err
return nil, nil, err
}
// Once we have the root, we can then generate our shachain producer
// and from that generate the per-commitment point.
return shachain.NewRevocationProducer(revRoot), nil
shaChainRoot := shachain.NewRevocationProducer(revRoot)
taprootShaChainRoot, err := deriveMusig2Shachain(revRoot)
if err != nil {
return nil, nil, err
}
return shaChainRoot, taprootShaChainRoot, nil
}

View File

@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/btcutil/txsort"
@ -1330,8 +1331,11 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation,
}
// With the above keys created, we'll also need to initialize our
// revocation tree state, and from that generate the per-commitment point.
producer, err := l.nextRevocationProducer(reservation, keyRing)
// revocation tree state, and from that generate the per-commitment
// point.
producer, taprootNonceProducer, err := l.nextRevocationProducer(
reservation, keyRing,
)
if err != nil {
return err
}
@ -1347,6 +1351,34 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation,
reservation.partialState.RevocationProducer = producer
reservation.ourContribution.ChannelConstraints = l.Cfg.DefaultConstraints
// If taproot channels are active, then we'll generate our verification
// nonce here. We'll use this nonce to verify the signature for our
// local commitment transaction. If we need to force close, then this
// is also what'll be used to sign that transaction.
if reservation.partialState.ChanType.IsTaproot() {
firstNoncePreimage, err := taprootNonceProducer.AtIndex(0)
if err != nil {
return err
}
// As we'd like the local nonce we send over to be generated
// deterministically, we'll provide a custom reader that
// actually just uses our sha-chain pre-image as the primary
// randomness source.
shaChainRand := musig2.WithCustomRand(
bytes.NewBuffer(firstNoncePreimage[:]),
)
pubKeyOpt := musig2.WithPublicKey(
reservation.ourContribution.MultiSigKey.PubKey,
)
reservation.ourContribution.LocalNonce, err = musig2.GenNonces(
pubKeyOpt, shaChainRand,
)
if err != nil {
return err
}
}
return nil
}
@ -1603,8 +1635,94 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
})
}
// handleChanPointReady continues the funding process once the channel point
// is known and the funding transaction can be completed.
// genMusigSession generates a new musig2 pair session that we can use to sign
// the commitment transaction for the remote party, and verify their incoming
// partial signature.
func genMusigSession(ourContribution, theirContribution *ChannelContribution,
signer input.MuSig2Signer,
fundingOutput *wire.TxOut) *MusigPairSession {
return NewMusigPairSession(&MusigSessionCfg{
LocalKey: ourContribution.MultiSigKey,
RemoteKey: theirContribution.MultiSigKey,
LocalNonce: *ourContribution.LocalNonce,
RemoteNonce: *theirContribution.LocalNonce,
Signer: signer,
InputTxOut: fundingOutput,
})
}
// signCommitTx generates a valid input.Signature to send to the remote party
// for their version of the commitment transaction. For regular channels, this
// will be a normal ECDSA signature. For taproot channels, this will instead be
// a musig2 partial signature that also includes the nonce used to generate it.
func (l *LightningWallet) signCommitTx(pendingReservation *ChannelReservation,
commitTx *wire.MsgTx, fundingOutput *wire.TxOut,
fundingWitnessScript []byte) (input.Signature, error) {
ourContribution := pendingReservation.ourContribution
theirContribution := pendingReservation.theirContribution
var (
sigTheirCommit input.Signature
err error
)
switch {
// For regular channels, we can just send over a normal ECDSA signature
// w/o any extra steps.
case !pendingReservation.partialState.ChanType.IsTaproot():
ourKey := ourContribution.MultiSigKey
signDesc := input.SignDescriptor{
WitnessScript: fundingWitnessScript,
KeyDesc: ourKey,
Output: fundingOutput,
HashType: txscript.SigHashAll,
SigHashes: input.NewTxSigHashesV0Only(
commitTx,
),
InputIndex: 0,
}
sigTheirCommit, err = l.Cfg.Signer.SignOutputRaw(
commitTx, &signDesc,
)
if err != nil {
return nil, err
}
// If this is a taproot channel, then we'll need to create an initial
// musig2 session here as we'll be sending over a _partial_ signature.
default:
// We're now ready to sign the first commitment. However, we'll
// only create the session if that hasn't been done already.
if pendingReservation.musigSessions == nil {
musigSessions := genMusigSession(
ourContribution, theirContribution,
l.Cfg.Signer, fundingOutput,
)
pendingReservation.musigSessions = musigSessions
}
// Now that we have the funding outpoint, we'll generate a
// musig2 signature for their version of the commitment
// transaction. We use the remote session as this is for the
// remote commitment transaction.
musigSessions := pendingReservation.musigSessions
partialSig, err := musigSessions.RemoteSession.SignCommit(
commitTx,
)
if err != nil {
return nil, fmt.Errorf("unable to sign "+
"commitment: %w", err)
}
sigTheirCommit = partialSig
}
return sigTheirCommit, nil
}
// handleChanPointReady continues the funding process once the channel point is
// known and the funding transaction can be completed.
func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
l.limboMtx.Lock()
pendingReservation, ok := l.fundingLimbo[req.pendingFundingID]
@ -1614,6 +1732,7 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
"funding state")
return
}
ourContribution := pendingReservation.ourContribution
theirContribution := pendingReservation.theirContribution
chanPoint := pendingReservation.partialState.FundingOutpoint
@ -1754,26 +1873,22 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
fundingIntent := pendingReservation.fundingIntent
fundingWitnessScript, fundingOutput, err := fundingIntent.FundingOutput()
if err != nil {
req.err <- fmt.Errorf("unable to obtain funding output")
req.err <- fmt.Errorf("unable to obtain funding "+
"output: %w", err)
return
}
// Generate a signature for their version of the initial commitment
// transaction.
ourKey := ourContribution.MultiSigKey
signDesc := input.SignDescriptor{
WitnessScript: fundingWitnessScript,
KeyDesc: ourKey,
Output: fundingOutput,
HashType: txscript.SigHashAll,
SigHashes: input.NewTxSigHashesV0Only(theirCommitTx),
InputIndex: 0,
}
sigTheirCommit, err := l.Cfg.Signer.SignOutputRaw(theirCommitTx, &signDesc)
sigTheirCommit, err := l.signCommitTx(
pendingReservation, theirCommitTx, fundingOutput,
fundingWitnessScript,
)
if err != nil {
req.err <- err
return
}
pendingReservation.ourCommitmentSig = sigTheirCommit
req.err <- nil
@ -1898,6 +2013,89 @@ func (l *LightningWallet) verifyFundingInputs(fundingTx *wire.MsgTx,
return nil
}
// verifyCommitSig verifies an incoming signature for our version of the
// commitment transaction. For normal channels, this will verify that the ECDSA
// signature is valid. For taproot channels, we'll verify that their partial
// signature is valid, so it can properly be combined with our eventual
// signature when we need to broadcast.
func (l *LightningWallet) verifyCommitSig(res *ChannelReservation,
commitSig input.Signature, commitTx *wire.MsgTx) error {
localKey := res.ourContribution.MultiSigKey.PubKey
remoteKey := res.theirContribution.MultiSigKey.PubKey
channelValue := int64(res.partialState.Capacity)
switch {
// If this isn't a taproot channel, then we'll construct a segwit v0
// p2wsh sighash.
case !res.partialState.ChanType.IsTaproot():
hashCache := input.NewTxSigHashesV0Only(commitTx)
witnessScript, _, err := input.GenFundingPkScript(
localKey.SerializeCompressed(),
remoteKey.SerializeCompressed(), channelValue,
)
if err != nil {
return err
}
sigHash, err := txscript.CalcWitnessSigHash(
witnessScript, hashCache, txscript.SigHashAll,
commitTx, 0, channelValue,
)
if err != nil {
return err
}
// Verify that we've received a valid signature from the remote
// party for our version of the commitment transaction.
if !commitSig.Verify(sigHash, remoteKey) {
return fmt.Errorf("counterparty's commitment " +
"signature is invalid")
}
return nil
// Otherwise for taproot channels, we'll compute the segwit v1 sighash,
// which is slightly different.
default:
// First, check to see if we've generated the musig session
// already. If we're the responder in the funding flow, we may
// not have generated it already.
if res.musigSessions == nil {
_, fundingOutput, err := input.GenTaprootFundingScript(
localKey, remoteKey, channelValue,
)
if err != nil {
return err
}
res.musigSessions = genMusigSession(
res.ourContribution, res.theirContribution,
l.Cfg.Signer, fundingOutput,
)
}
// For the musig2 based channels, we'll use the generated local
// musig2 session to verify the signature.
localSession := res.musigSessions.LocalSession
// At this point, the commitment signature passed in should
// actually be a wrapped musig2 signature, so we'll do a type
// asset to the get the signature we actually need.
partialSig, ok := commitSig.(*MusigPartialSig)
if !ok {
return fmt.Errorf("expected *musig2.PartialSignature, "+
"got: %T", commitSig)
}
_, err := localSession.VerifyCommitSig(
commitTx, partialSig.ToWireSig(),
)
return err
}
}
// handleFundingCounterPartySigs is the final step in the channel reservation
// workflow. During this step, we validate *all* the received signatures for
// inputs to the funding transaction. If any of these are invalid, we bail,
@ -1940,44 +2138,15 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
// commitment transaction.
res.theirCommitmentSig = msg.theirCommitmentSig
commitTx := res.partialState.LocalCommitment.CommitTx
ourKey := res.ourContribution.MultiSigKey
theirKey := res.theirContribution.MultiSigKey
// Re-generate both the witnessScript and p2sh output. We sign the
// witnessScript script, but include the p2sh output as the subscript
// for verification.
witnessScript, _, err := input.GenFundingPkScript(
ourKey.PubKey.SerializeCompressed(),
theirKey.PubKey.SerializeCompressed(),
int64(res.partialState.Capacity),
)
err := l.verifyCommitSig(res, msg.theirCommitmentSig, commitTx)
if err != nil {
msg.err <- err
msg.err <- fmt.Errorf("counterparty's commitment signature is "+
"invalid: %w", err)
msg.completeChan <- nil
return
}
// Next, create the spending scriptSig, and then verify that the script
// is complete, allowing us to spend from the funding transaction.
channelValue := int64(res.partialState.Capacity)
hashCache := input.NewTxSigHashesV0Only(commitTx)
sigHash, err := txscript.CalcWitnessSigHash(
witnessScript, hashCache, txscript.SigHashAll, commitTx,
0, channelValue,
)
if err != nil {
msg.err <- err
msg.completeChan <- nil
return
}
// Verify that we've received a valid signature from the remote party
// for our version of the commitment transaction.
if !msg.theirCommitmentSig.Verify(sigHash, theirKey.PubKey) {
msg.err <- fmt.Errorf("counterparty's commitment signature is invalid")
msg.completeChan <- nil
return
}
theirCommitSigBytes := msg.theirCommitmentSig.Serialize()
res.partialState.LocalCommitment.CommitSig = theirCommitSigBytes
@ -2056,6 +2225,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
defer pendingReservation.Unlock()
chanState := pendingReservation.partialState
chanType := pendingReservation.partialState.ChanType
chanState.FundingOutpoint = *req.fundingOutpoint
fundingTxIn := wire.NewTxIn(req.fundingOutpoint, nil, nil)
@ -2074,7 +2244,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
pendingReservation.theirContribution.ChannelConfig,
pendingReservation.ourContribution.FirstCommitmentPoint,
pendingReservation.theirContribution.FirstCommitmentPoint,
*fundingTxIn, pendingReservation.partialState.ChanType,
*fundingTxIn, chanType,
pendingReservation.partialState.IsInitiator, leaseExpiry,
)
if err != nil {
@ -2110,13 +2280,10 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
walletLog.Debugf("Remote commit tx for ChannelPoint(%v): %v",
req.fundingOutpoint, spew.Sdump(theirCommitTx))
channelValue := int64(pendingReservation.partialState.Capacity)
hashCache := input.NewTxSigHashesV0Only(ourCommitTx)
theirKey := pendingReservation.theirContribution.MultiSigKey
ourKey := pendingReservation.ourContribution.MultiSigKey
witnessScript, _, err := input.GenFundingPkScript(
ourKey.PubKey.SerializeCompressed(),
theirKey.PubKey.SerializeCompressed(), channelValue,
// With both commitment transactions created, we'll now verify their
// signature on our commitment.
err = l.verifyCommitSig(
pendingReservation, req.theirCommitmentSig, ourCommitTx,
)
if err != nil {
req.err <- err
@ -2124,53 +2291,46 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
return
}
sigHash, err := txscript.CalcWitnessSigHash(
witnessScript, hashCache, txscript.SigHashAll, ourCommitTx, 0,
channelValue,
)
if err != nil {
req.err <- err
req.completeChan <- nil
return
}
// Verify that we've received a valid signature from the remote party
// for our version of the commitment transaction.
if !req.theirCommitmentSig.Verify(sigHash, theirKey.PubKey) {
req.err <- fmt.Errorf("counterparty's commitment signature " +
"is invalid")
req.completeChan <- nil
return
}
theirCommitSigBytes := req.theirCommitmentSig.Serialize()
chanState.LocalCommitment.CommitSig = theirCommitSigBytes
channelValue := int64(pendingReservation.partialState.Capacity)
theirKey := pendingReservation.theirContribution.MultiSigKey
ourKey := pendingReservation.ourContribution.MultiSigKey
var (
fundingWitnessScript []byte
fundingTxOut *wire.TxOut
)
if chanType.IsTaproot() {
fundingWitnessScript, fundingTxOut, err = input.GenTaprootFundingScript( //nolint:lll
ourKey.PubKey, theirKey.PubKey, channelValue,
)
} else {
fundingWitnessScript, fundingTxOut, err = input.GenFundingPkScript( //nolint:lll
ourKey.PubKey.SerializeCompressed(),
theirKey.PubKey.SerializeCompressed(), channelValue,
)
}
if err != nil {
req.err <- err
req.completeChan <- nil
return
}
// With their signature for our version of the commitment transactions
// verified, we can now generate a signature for their version,
// allowing the funding transaction to be safely broadcast.
p2wsh, err := input.WitnessScriptHash(witnessScript)
if err != nil {
req.err <- err
req.completeChan <- nil
return
}
signDesc := input.SignDescriptor{
WitnessScript: witnessScript,
KeyDesc: ourKey,
Output: &wire.TxOut{
PkScript: p2wsh,
Value: channelValue,
},
HashType: txscript.SigHashAll,
SigHashes: input.NewTxSigHashesV0Only(theirCommitTx),
InputIndex: 0,
}
sigTheirCommit, err := l.Cfg.Signer.SignOutputRaw(theirCommitTx, &signDesc)
sigTheirCommit, err := l.signCommitTx(
pendingReservation, theirCommitTx, fundingTxOut,
fundingWitnessScript,
)
if err != nil {
req.err <- err
req.completeChan <- nil
return
}
pendingReservation.ourCommitmentSig = sigTheirCommit
_, bestHeight, err := l.Cfg.ChainIO.GetBestBlock()