diff --git a/watchtower/blob/commitments.go b/watchtower/blob/commitments.go index 0f476e5a3..f86240447 100644 --- a/watchtower/blob/commitments.go +++ b/watchtower/blob/commitments.go @@ -27,6 +27,10 @@ const ( // anchor channel. The key differences are that the to_remote is // encumbered by a 1 block CSV and so is thus a P2WSH output. AnchorCommitment + + // TaprootCommitment represents the commitment transaction of a simple + // taproot channel. + TaprootCommitment ) // ToLocalInput constructs the input that will be used to spend the to_local @@ -61,9 +65,10 @@ func (c CommitmentType) ToRemoteInput(info *lnwallet.BreachRetribution) ( info.LocalOutputSignDesc, 0, ), nil - case AnchorCommitment: - // Anchor channels have a CSV-encumbered to-remote output. We'll - // construct a CSV input and assign the proper CSV delay of 1. + case AnchorCommitment, TaprootCommitment: + // Anchor and Taproot channels have a CSV-encumbered to-remote + // output. We'll construct a CSV input and assign the proper CSV + // delay of 1. return input.NewCsvInput( &info.LocalOutpoint, witnessType, info.LocalOutputSignDesc, 0, 1, @@ -80,6 +85,9 @@ func (c CommitmentType) ToLocalWitnessType() (input.WitnessType, error) { case LegacyTweaklessCommitment, LegacyCommitment, AnchorCommitment: return input.CommitmentRevoke, nil + case TaprootCommitment: + return input.TaprootCommitmentRevoke, nil + default: return nil, fmt.Errorf("unknown commitment type: %v", c) } @@ -97,6 +105,9 @@ func (c CommitmentType) ToRemoteWitnessType() (input.WitnessType, error) { case AnchorCommitment: return input.CommitmentToRemoteConfirmed, nil + case TaprootCommitment: + return input.TaprootRemoteCommitSpend, nil + default: return nil, fmt.Errorf("unknown commitment type: %v", c) } @@ -115,6 +126,10 @@ func (c CommitmentType) ToRemoteWitnessSize() (int, error) { case AnchorCommitment: return input.ToRemoteConfirmedWitnessSize, nil + // Taproot channels spend a confirmed P2SH output. + case TaprootCommitment: + return input.TaprootToRemoteWitnessSize, nil + default: return 0, fmt.Errorf("unknown commitment type: %v", c) } @@ -134,6 +149,9 @@ func (c CommitmentType) ToLocalWitnessSize() (int, error) { case AnchorCommitment: return input.ToLocalPenaltyWitnessSize, nil + case TaprootCommitment: + return input.TaprootToLocalRevokeWitnessSize, nil + default: return 0, fmt.Errorf("unknown commitment type: %v", c) } @@ -143,21 +161,22 @@ func (c CommitmentType) ToLocalWitnessSize() (int, error) { func (c CommitmentType) ParseRawSig(witness wire.TxWitness) (lnwire.Sig, error) { + // Check that the witness has at least one item since this is required + // for all commitment types to follow. + if len(witness) < 1 { + return lnwire.Sig{}, fmt.Errorf("the witness should have at " + + "least one element") + } + + // Check that the first witness element is non-nil. This is to ensure + // that the witness length checks below do not panic. + if witness[0] == nil { + return lnwire.Sig{}, fmt.Errorf("the first witness element " + + "should not be nil") + } + switch c { case LegacyCommitment, LegacyTweaklessCommitment, AnchorCommitment: - // Check that the witness has at least one item. - if len(witness) < 1 { - return lnwire.Sig{}, fmt.Errorf("the witness should " + - "have at least one element") - } - - // Check that the first witness element is non-nil. This is to - // ensure that the witness length check below does not panic. - if witness[0] == nil { - return lnwire.Sig{}, fmt.Errorf("the first witness " + - "element should not be nil") - } - // Parse the DER-encoded signature from the first position of // the resulting witness. We trim an extra byte to remove the // sighash flag. @@ -167,6 +186,16 @@ func (c CommitmentType) ParseRawSig(witness wire.TxWitness) (lnwire.Sig, // signature. return lnwire.NewSigFromECDSARawSignature(rawSignature) + case TaprootCommitment: + rawSignature := witness[0] + if len(rawSignature) > 64 { + rawSignature = witness[0][:len(witness[0])-1] + } + + // Re-encode the schnorr signature into a fixed-size 64 byte + // signature. + return lnwire.NewSigFromSchnorrRawSignature(rawSignature) + default: return lnwire.Sig{}, fmt.Errorf("unknown commitment type: %v", c) @@ -190,6 +219,11 @@ func (c CommitmentType) NewJusticeKit(sweepScript []byte, sweepScript, breachInfo, withToRemote, ), nil + case TaprootCommitment: + return newTaprootJusticeKit( + sweepScript, breachInfo, withToRemote, + ) + default: return nil, fmt.Errorf("unknown commitment type: %v", c) } @@ -207,6 +241,9 @@ func (c CommitmentType) EmptyJusticeKit() (JusticeKit, error) { legacyJusticeKit: legacyJusticeKit{}, }, nil + case TaprootCommitment: + return &taprootJusticeKit{}, nil + default: return nil, fmt.Errorf("unknown commitment type: %v", c) } diff --git a/watchtower/blob/justice_kit_test.go b/watchtower/blob/justice_kit_test.go index 4851bbf7e..fd12993a0 100644 --- a/watchtower/blob/justice_kit_test.go +++ b/watchtower/blob/justice_kit_test.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/input" @@ -17,6 +18,8 @@ import ( "github.com/stretchr/testify/require" ) +const csvDelay = uint32(144) + func makePubKey() *btcec.PublicKey { priv, _ := btcec.NewPrivateKey() return priv.PubKey() @@ -39,6 +42,15 @@ func makeAddr(size int) []byte { return addr } +func makeSchnorrSig(i int) lnwire.Sig { + var sigBytes [64]byte + binary.BigEndian.PutUint64(sigBytes[:8], uint64(i)) + + sig, _ := lnwire.NewSigFromSchnorrRawSignature(sigBytes[:]) + + return sig +} + type descriptorTest struct { name string encVersion Type @@ -46,7 +58,6 @@ type descriptorTest struct { sweepAddr []byte revPubKey *btcec.PublicKey delayPubKey *btcec.PublicKey - csvDelay uint32 commitToLocalSig lnwire.Sig hasCommitToRemote bool commitToRemotePubKey *btcec.PublicKey @@ -63,7 +74,6 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(22), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), }, { @@ -73,7 +83,6 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(22), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), hasCommitToRemote: true, commitToRemotePubKey: makePubKey(), @@ -86,7 +95,6 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(34), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), encErr: ErrUnknownBlobType, }, @@ -97,7 +105,6 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(34), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), decErr: ErrUnknownBlobType, }, @@ -108,7 +115,6 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(0), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), }, { @@ -118,7 +124,6 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(MaxSweepAddrSize), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), }, { @@ -128,10 +133,30 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(MaxSweepAddrSize + 1), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), encErr: ErrSweepAddressToLong, }, + { + name: "taproot to-local only", + encVersion: TypeAltruistTaprootCommit, + decVersion: TypeAltruistTaprootCommit, + sweepAddr: makeAddr(34), + revPubKey: makePubKey(), + delayPubKey: makePubKey(), + commitToLocalSig: makeSchnorrSig(1), + }, + { + name: "taproot to-local and to-remote", + encVersion: TypeAltruistTaprootCommit, + decVersion: TypeAltruistTaprootCommit, + sweepAddr: makeAddr(34), + revPubKey: makePubKey(), + delayPubKey: makePubKey(), + commitToLocalSig: makeSchnorrSig(1), + hasCommitToRemote: true, + commitToRemotePubKey: makePubKey(), + commitToRemoteSig: makeSchnorrSig(2), + }, } // TestBlobJusticeKitEncryptDecrypt asserts that encrypting and decrypting a @@ -154,7 +179,7 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) { } breachInfo := &lnwallet.BreachRetribution{ - RemoteDelay: test.csvDelay, + RemoteDelay: csvDelay, KeyRing: &lnwallet.CommitmentKeyRing{ ToLocalKey: test.delayPubKey, ToRemoteKey: test.commitToRemotePubKey, @@ -221,6 +246,8 @@ type remoteWitnessTest struct { name string blobType Type expWitnessScript func(pk *btcec.PublicKey) []byte + expWitnessStack func(sig input.Signature) wire.TxWitness + createSig func(*btcec.PrivateKey, []byte) input.Signature } // TestJusticeKitRemoteWitnessConstruction tests that a JusticeKit returns the @@ -234,6 +261,21 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { expWitnessScript: func(pk *btcec.PublicKey) []byte { return pk.SerializeCompressed() }, + expWitnessStack: func( + sig input.Signature) wire.TxWitness { + + sigBytes := append( + sig.Serialize(), + byte(txscript.SigHashAll), + ) + + return [][]byte{sigBytes} + }, + createSig: func(priv *btcec.PrivateKey, + digest []byte) input.Signature { + + return ecdsa.Sign(priv, digest) + }, }, { name: "anchor commitment", @@ -242,6 +284,42 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { script, _ := input.CommitScriptToRemoteConfirmed(pk) return script }, + expWitnessStack: func( + sig input.Signature) wire.TxWitness { + + sigBytes := append( + sig.Serialize(), + byte(txscript.SigHashAll), + ) + + return [][]byte{sigBytes} + }, + createSig: func(priv *btcec.PrivateKey, + digest []byte) input.Signature { + + return ecdsa.Sign(priv, digest) + }, + }, + { + name: "taproot commitment", + blobType: TypeAltruistTaprootCommit, + expWitnessScript: func(pk *btcec.PublicKey) []byte { + tree, _ := input.NewRemoteCommitScriptTree(pk) + + return tree.SettleLeaf.Script + }, + expWitnessStack: func( + sig input.Signature) wire.TxWitness { + + return [][]byte{sig.Serialize()} + }, + createSig: func(priv *btcec.PrivateKey, + digest []byte) input.Signature { + + sig, _ := schnorr.Sign(priv, digest) + + return sig + }, }, } for _, test := range tests { @@ -252,8 +330,8 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { } } -func testJusticeKitRemoteWitnessConstruction( - t *testing.T, test remoteWitnessTest) { +func testJusticeKitRemoteWitnessConstruction(t *testing.T, + test remoteWitnessTest) { // Generate the to-remote pubkey. toRemotePrivKey, err := btcec.NewPrivateKey() @@ -268,7 +346,7 @@ func testJusticeKitRemoteWitnessConstruction( // Sign a message using the to-remote private key. The exact message // doesn't matter as we won't be validating the signature's validity. digest := bytes.Repeat([]byte("a"), 32) - rawToRemoteSig := ecdsa.Sign(toRemotePrivKey, digest) + rawToRemoteSig := test.createSig(toRemotePrivKey, digest) // Convert the DER-encoded signature into a fixed-size sig. commitToRemoteSig, err := lnwire.NewSigFromSignature(rawToRemoteSig) @@ -298,24 +376,122 @@ func testJusticeKitRemoteWitnessConstruction( expToRemoteScript := test.expWitnessScript(toRemotePrivKey.PubKey()) require.Equal(t, expToRemoteScript, witness[1]) - // Compute the expected first element, by appending a sighash all byte - // to our raw DER-encoded signature. - rawToRemoteSigWithSigHash := append( - rawToRemoteSig.Serialize(), byte(txscript.SigHashAll), - ) - - // Assert that the expected witness stack is returned. - expWitnessStack := wire.TxWitness{ - rawToRemoteSigWithSigHash, - } + // Compute the expected signature. + expWitnessStack := test.expWitnessStack(rawToRemoteSig) require.Equal(t, expWitnessStack, witness[:1]) } +type localWitnessTest struct { + name string + blobType Type + expWitnessScript func(delay, rev *btcec.PublicKey) []byte + expWitnessStack func(sig input.Signature) wire.TxWitness + witnessScriptIndex int + createSig func(*btcec.PrivateKey, []byte) input.Signature +} + // TestJusticeKitToLocalWitnessConstruction tests that a JusticeKit returns the // proper to-local witness script and to-local witness stack for spending the // revocation path. func TestJusticeKitToLocalWitnessConstruction(t *testing.T) { - csvDelay := uint32(144) + tests := []localWitnessTest{ + { + name: "legacy commitment", + blobType: TypeAltruistCommit, + expWitnessScript: func(delay, + rev *btcec.PublicKey) []byte { + + script, _ := input.CommitScriptToSelf( + csvDelay, delay, rev, + ) + + return script + }, + expWitnessStack: func( + sig input.Signature) wire.TxWitness { + + sigBytes := append( + sig.Serialize(), + byte(txscript.SigHashAll), + ) + + return [][]byte{sigBytes, {1}} + }, + witnessScriptIndex: 2, + createSig: func(priv *btcec.PrivateKey, + digest []byte) input.Signature { + + return ecdsa.Sign(priv, digest) + }, + }, + { + name: "anchor commitment", + blobType: TypeAltruistAnchorCommit, + expWitnessScript: func(delay, + rev *btcec.PublicKey) []byte { + + script, _ := input.CommitScriptToSelf( + csvDelay, delay, rev, + ) + + return script + }, + witnessScriptIndex: 2, + expWitnessStack: func( + sig input.Signature) wire.TxWitness { + + sigBytes := append( + sig.Serialize(), + byte(txscript.SigHashAll), + ) + + return [][]byte{sigBytes, {1}} + }, + createSig: func(priv *btcec.PrivateKey, + digest []byte) input.Signature { + + return ecdsa.Sign(priv, digest) + }, + }, + { + name: "taproot commitment", + blobType: TypeAltruistTaprootCommit, + expWitnessScript: func(delay, + rev *btcec.PublicKey) []byte { + + script, _ := input.NewLocalCommitScriptTree( + csvDelay, delay, rev, + ) + + return script.RevocationLeaf.Script + }, + witnessScriptIndex: 1, + expWitnessStack: func( + sig input.Signature) wire.TxWitness { + + return [][]byte{sig.Serialize()} + }, + createSig: func(priv *btcec.PrivateKey, + digest []byte) input.Signature { + + sig, _ := schnorr.Sign(priv, digest) + + return sig + }, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + testJusticeKitToLocalWitnessConstruction(t, test) + }) + } +} + +func testJusticeKitToLocalWitnessConstruction(t *testing.T, + test localWitnessTest) { // Generate the revocation and delay private keys. revPrivKey, err := btcec.NewPrivateKey() @@ -327,13 +503,13 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) { // Sign a message using the revocation private key. The exact message // doesn't matter as we won't be validating the signature's validity. digest := bytes.Repeat([]byte("a"), 32) - rawRevSig := ecdsa.Sign(revPrivKey, digest) + rawRevSig := test.createSig(revPrivKey, digest) // Convert the DER-encoded signature into a fixed-size sig. commitToLocalSig, err := lnwire.NewSigFromSignature(rawRevSig) require.NoError(t, err) - commitType, err := TypeAltruistCommit.CommitmentType(nil) + commitType, err := test.blobType.CommitmentType(nil) require.NoError(t, err) breachInfo := &lnwallet.BreachRetribution{ @@ -350,28 +526,18 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) { // Compute the expected to-local script, which is a function of the CSV // delay, revocation pubkey and delay pubkey. - expToLocalScript, err := input.CommitScriptToSelf( - csvDelay, delayPrivKey.PubKey(), revPrivKey.PubKey(), + expToLocalScript := test.expWitnessScript( + delayPrivKey.PubKey(), revPrivKey.PubKey(), ) - require.NoError(t, err) // Compute the to-local script that is returned by the justice kit. _, witness, err := justiceKit.ToLocalOutputSpendInfo() require.NoError(t, err) // Assert that the expected to-local script matches the actual script. - require.Equal(t, expToLocalScript, witness[2]) + require.Equal(t, expToLocalScript, witness[test.witnessScriptIndex]) - // Compute the expected signature in the bottom element of the stack, by - // appending a sighash all flag to the raw DER signature. - rawRevSigWithSigHash := append( - rawRevSig.Serialize(), byte(txscript.SigHashAll), - ) - - // Finally, validate against our expected witness stack. - expWitnessStack := wire.TxWitness{ - rawRevSigWithSigHash, - {1}, - } - require.Equal(t, expWitnessStack, witness[:2]) + // Finally, validate the witness. + expWitnessStack := test.expWitnessStack(rawRevSig) + require.Equal(t, expWitnessStack, witness[:test.witnessScriptIndex]) } diff --git a/watchtower/blob/type.go b/watchtower/blob/type.go index cef9dfb10..83dfcf184 100644 --- a/watchtower/blob/type.go +++ b/watchtower/blob/type.go @@ -102,6 +102,9 @@ func (t Type) CommitmentType(chanType *channeldb.ChannelType) (CommitmentType, error) { switch { + case t.Has(FlagTaprootChannel): + return TaprootCommitment, nil + case t.Has(FlagAnchorChannel): return AnchorCommitment, nil