package blob import ( "bytes" "crypto/rand" "encoding/binary" "io" "testing" "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/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" ) const csvDelay = uint32(144) func makePubKey() *btcec.PublicKey { priv, _ := btcec.NewPrivateKey() return priv.PubKey() } func makeSig(i int) lnwire.Sig { var sigBytes [64]byte binary.BigEndian.PutUint64(sigBytes[:8], uint64(i)) sig, _ := lnwire.NewSigFromWireECDSA(sigBytes[:]) return sig } func makeAddr(size int) []byte { addr := make([]byte, size) if _, err := io.ReadFull(rand.Reader, addr); err != nil { panic("unable to create addr") } 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 decVersion Type sweepAddr []byte revPubKey *btcec.PublicKey delayPubKey *btcec.PublicKey commitToLocalSig lnwire.Sig hasCommitToRemote bool commitToRemotePubKey *btcec.PublicKey commitToRemoteSig lnwire.Sig encErr error decErr error } var descriptorTests = []descriptorTest{ { name: "to-local only", encVersion: TypeAltruistCommit, decVersion: TypeAltruistCommit, sweepAddr: makeAddr(22), revPubKey: makePubKey(), delayPubKey: makePubKey(), commitToLocalSig: makeSig(1), }, { name: "to-local and p2wkh", encVersion: TypeRewardCommit, decVersion: TypeRewardCommit, sweepAddr: makeAddr(22), revPubKey: makePubKey(), delayPubKey: makePubKey(), commitToLocalSig: makeSig(1), hasCommitToRemote: true, commitToRemotePubKey: makePubKey(), commitToRemoteSig: makeSig(2), }, { name: "unknown encrypt version", encVersion: 0, decVersion: TypeAltruistCommit, sweepAddr: makeAddr(34), revPubKey: makePubKey(), delayPubKey: makePubKey(), commitToLocalSig: makeSig(1), encErr: ErrUnknownBlobType, }, { name: "unknown decrypt version", encVersion: TypeAltruistCommit, decVersion: 0, sweepAddr: makeAddr(34), revPubKey: makePubKey(), delayPubKey: makePubKey(), commitToLocalSig: makeSig(1), decErr: ErrUnknownBlobType, }, { name: "sweep addr length zero", encVersion: TypeAltruistCommit, decVersion: TypeAltruistCommit, sweepAddr: makeAddr(0), revPubKey: makePubKey(), delayPubKey: makePubKey(), commitToLocalSig: makeSig(1), }, { name: "sweep addr max size", encVersion: TypeAltruistCommit, decVersion: TypeAltruistCommit, sweepAddr: makeAddr(MaxSweepAddrSize), revPubKey: makePubKey(), delayPubKey: makePubKey(), commitToLocalSig: makeSig(1), }, { name: "sweep addr too long", encVersion: TypeAltruistCommit, decVersion: TypeAltruistCommit, sweepAddr: makeAddr(MaxSweepAddrSize + 1), revPubKey: makePubKey(), delayPubKey: makePubKey(), 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 // plaintext blob produces the original. The tests include negative assertions // when passed invalid combinations, and that all successfully encrypted blobs // are of constant size. func TestBlobJusticeKitEncryptDecrypt(t *testing.T) { for _, test := range descriptorTests { t.Run(test.name, func(t *testing.T) { testBlobJusticeKitEncryptDecrypt(t, test) }) } } func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) { commitmentType, err := test.encVersion.CommitmentType(nil) if err != nil { require.ErrorIs(t, err, test.encErr) return } breachInfo := &lnwallet.BreachRetribution{ RemoteDelay: csvDelay, KeyRing: &lnwallet.CommitmentKeyRing{ ToLocalKey: test.delayPubKey, ToRemoteKey: test.commitToRemotePubKey, RevocationKey: test.revPubKey, }, } kit, err := commitmentType.NewJusticeKit( test.sweepAddr, breachInfo, test.hasCommitToRemote, ) if err != nil { return } kit.AddToLocalSig(test.commitToLocalSig) kit.AddToRemoteSig(test.commitToRemoteSig) // Generate a random encryption key for the blob. The key is // sized at 32 byte, as in practice we will be using the remote // party's commitment txid as the key. var key BreachKey _, err = rand.Read(key[:]) require.NoError(t, err, "unable to generate blob encryption key") // Encrypt the blob plaintext using the generated key and // target version for this test. ctxt, err := Encrypt(kit, key) require.ErrorIs(t, err, test.encErr) if test.encErr != nil { // If the test expected an encryption failure, we can // continue to the next test. return } // Ensure that all encrypted blobs are padded out to the same // size: 282 bytes for version 0. require.Len(t, ctxt, Size(kit)) // Decrypt the encrypted blob, reconstructing the original // blob plaintext from the decrypted contents. We use the target // decryption version specified by this test case. boj2, err := Decrypt(key, ctxt, test.decVersion) require.ErrorIs(t, err, test.decErr) if test.decErr != nil { // If the test expected an decryption failure, we can // continue to the next test. return } // Check that the decrypted blob properly reports whether it has // a to-remote output or not. if boj2.HasCommitToRemoteOutput() != test.hasCommitToRemote { t.Fatalf("expected blob has_to_remote to be %v, got %v", test.hasCommitToRemote, boj2.HasCommitToRemoteOutput()) } // Check that the original blob plaintext matches the // one reconstructed from the encrypted blob. require.Equal(t, kit, boj2) } 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 // proper to-remote witnes script and to-remote witness stack. This should be // equivalent to p2wkh spend. func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { tests := []remoteWitnessTest{ { name: "legacy commitment", blobType: TypeAltruistCommit, 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", blobType: TypeAltruistAnchorCommit, expWitnessScript: func(pk *btcec.PublicKey) []byte { 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, fn.None[txscript.TapLeaf](), ) 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 { test := test t.Run(test.name, func(t *testing.T) { testJusticeKitRemoteWitnessConstruction(t, test) }) } } func testJusticeKitRemoteWitnessConstruction(t *testing.T, test remoteWitnessTest) { // Generate the to-remote pubkey. toRemotePrivKey, err := btcec.NewPrivateKey() require.NoError(t, err) revKey, err := btcec.NewPrivateKey() require.NoError(t, err) toLocalKey, err := btcec.NewPrivateKey() require.NoError(t, err) // 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 := test.createSig(toRemotePrivKey, digest) // Convert the DER-encoded signature into a fixed-size sig. commitToRemoteSig, err := lnwire.NewSigFromSignature(rawToRemoteSig) require.Nil(t, err) commitType, err := test.blobType.CommitmentType(nil) require.NoError(t, err) breachInfo := &lnwallet.BreachRetribution{ KeyRing: &lnwallet.CommitmentKeyRing{ ToRemoteKey: toRemotePrivKey.PubKey(), RevocationKey: revKey.PubKey(), ToLocalKey: toLocalKey.PubKey(), }, } justiceKit, err := commitType.NewJusticeKit(nil, breachInfo, true) require.NoError(t, err) justiceKit.AddToRemoteSig(commitToRemoteSig) // Now, compute the to-remote witness script returned by the justice // kit. _, witness, _, err := justiceKit.ToRemoteOutputSpendInfo() require.NoError(t, err) // Assert this is exactly the to-remote, compressed pubkey. expToRemoteScript := test.expWitnessScript(toRemotePrivKey.PubKey()) require.Equal(t, expToRemoteScript, witness[1]) // 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) { 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, fn.None[txscript.TapLeaf](), ) 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() require.NoError(t, err) delayPrivKey, err := btcec.NewPrivateKey() require.NoError(t, err) // 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 := 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 := test.blobType.CommitmentType(nil) require.NoError(t, err) breachInfo := &lnwallet.BreachRetribution{ RemoteDelay: csvDelay, KeyRing: &lnwallet.CommitmentKeyRing{ RevocationKey: revPrivKey.PubKey(), ToLocalKey: delayPrivKey.PubKey(), }, } justiceKit, err := commitType.NewJusticeKit(nil, breachInfo, false) require.NoError(t, err) justiceKit.AddToLocalSig(commitToLocalSig) // Compute the expected to-local script, which is a function of the CSV // delay, revocation pubkey and delay pubkey. expToLocalScript := test.expWitnessScript( delayPrivKey.PubKey(), revPrivKey.PubKey(), ) // 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[test.witnessScriptIndex]) // Finally, validate the witness. expWitnessStack := test.expWitnessStack(rawRevSig) require.Equal(t, expWitnessStack, witness[:test.witnessScriptIndex]) }