diff --git a/watchtower/blob/justice_kit_test.go b/watchtower/blob/justice_kit_test.go index cac4af8f..922290b5 100644 --- a/watchtower/blob/justice_kit_test.go +++ b/watchtower/blob/justice_kit_test.go @@ -1,12 +1,16 @@ package blob_test import ( + "bytes" "crypto/rand" "encoding/binary" "io" "reflect" "testing" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/watchtower/blob" ) @@ -212,3 +216,195 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) { "want: %v, got %v", boj, boj2) } } + +// 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) { + // Generate the to-remote pubkey. + toRemotePrivKey, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Fatalf("unable to generate to-remote priv key: %v", err) + } + + // Copy the to-remote pubkey into the format expected by our justice + // kit. + var toRemotePubKey blob.PubKey + copy(toRemotePubKey[:], toRemotePrivKey.PubKey().SerializeCompressed()) + + // 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, err := toRemotePrivKey.Sign(digest) + if err != nil { + t.Fatalf("unable to generate to-remote signature: %v", err) + } + + // Convert the DER-encoded signature into a fixed-size sig. + commitToRemoteSig, err := lnwire.NewSigFromSignature(rawToRemoteSig) + if err != nil { + t.Fatalf("unable to convert raw to-remote signature to "+ + "Sig: %v", err) + } + + // Populate the justice kit fields relevant to the to-remote output. + justiceKit := &blob.JusticeKit{ + CommitToRemotePubKey: toRemotePubKey, + CommitToRemoteSig: commitToRemoteSig, + } + + // Now, compute the to-remote witness script returned by the justice + // kit. + toRemoteScript, err := justiceKit.CommitToRemoteWitnessScript() + if err != nil { + t.Fatalf("unable to compute to-remote witness script: %v", err) + } + + // Assert this is exactly the to-remote, compressed pubkey. + if !bytes.Equal(toRemoteScript, toRemotePubKey[:]) { + t.Fatalf("to-remote witness script should be equal to "+ + "to-remote pubkey, want: %x, got %x", + toRemotePubKey[:], toRemoteScript) + } + + // Next, compute the to-remote witness stack, which should be a p2wkh + // witness stack consisting solely of a signature. + toRemoteWitnessStack, err := justiceKit.CommitToRemoteWitnessStack() + if err != nil { + t.Fatalf("unable to compute to-remote witness stack: %v", err) + } + + // Assert that the witness stack only has one element. + if len(toRemoteWitnessStack) != 1 { + t.Fatalf("to-remote witness stack should be of length 1, is %d", + len(toRemoteWitnessStack)) + } + + // 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 signature matches the first element in the + // witness stack. + if !bytes.Equal(rawToRemoteSigWithSigHash, toRemoteWitnessStack[0]) { + t.Fatalf("mismatched sig in to-remote witness stack, want: %v, "+ + "got: %v", rawToRemoteSigWithSigHash, + toRemoteWitnessStack[0]) + } + + // Finally, set the CommitToRemotePubKey to be a blank value. + justiceKit.CommitToRemotePubKey = blob.PubKey{} + + // When trying to compute the witness script, this should now return + // ErrNoCommitToRemoteOutput since a valid pubkey could not be parsed + // from CommitToRemotePubKey. + _, err = justiceKit.CommitToRemoteWitnessScript() + if err != blob.ErrNoCommitToRemoteOutput { + t.Fatalf("expected ErrNoCommitToRemoteOutput, got: %v", err) + } +} + +// 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) + + // Generate the revocation and delay private keys. + revPrivKey, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Fatalf("unable to generate revocation priv key: %v", err) + } + + delayPrivKey, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Fatalf("unable to generate delay priv key: %v", err) + } + + // Copy the revocation and delay pubkeys into the format expected by our + // justice kit. + var revPubKey blob.PubKey + copy(revPubKey[:], revPrivKey.PubKey().SerializeCompressed()) + + var delayPubKey blob.PubKey + copy(delayPubKey[:], delayPrivKey.PubKey().SerializeCompressed()) + + // 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, err := revPrivKey.Sign(digest) + if err != nil { + t.Fatalf("unable to generate revocation signature: %v", err) + } + + // Convert the DER-encoded signature into a fixed-size sig. + commitToLocalSig, err := lnwire.NewSigFromSignature(rawRevSig) + if err != nil { + t.Fatalf("unable to convert raw revocation signature to "+ + "Sig: %v", err) + } + + // Populate the justice kit with fields relevant to the to-local output. + justiceKit := &blob.JusticeKit{ + CSVDelay: csvDelay, + RevocationPubKey: revPubKey, + LocalDelayPubKey: delayPubKey, + CommitToLocalSig: commitToLocalSig, + } + + // Compute the expected to-local script, which is a function of the CSV + // delay, revocation pubkey and delay pubkey. + expToLocalScript, err := lnwallet.CommitScriptToSelf( + csvDelay, delayPrivKey.PubKey(), revPrivKey.PubKey(), + ) + if err != nil { + t.Fatalf("unable to generate expected to-local script: %v", err) + } + + // Compute the to-local script that is returned by the justice kit. + toLocalScript, err := justiceKit.CommitToLocalWitnessScript() + if err != nil { + t.Fatalf("unable to compute to-local witness script: %v", err) + } + + // Assert that the expected to-local script matches the actual script. + if !bytes.Equal(expToLocalScript, toLocalScript) { + t.Fatalf("mismatched to-local witness script, want: %v, got %v", + expToLocalScript, toLocalScript) + } + + // Next, compute the to-local witness stack returned by the justice kit. + toLocalWitnessStack, err := justiceKit.CommitToLocalRevokeWitnessStack() + if err != nil { + t.Fatalf("unable to compute to-local witness stack: %v", err) + } + + // A valid witness that spends the revocation path should have exactly + // two elements on the stack. + if len(toLocalWitnessStack) != 2 { + t.Fatalf("to-local witness stack should be of length 2, is %d", + len(toLocalWitnessStack)) + } + + // First, we'll verify that the top element is 0x01, which triggers the + // revocation path within the to-local witness script. + if !bytes.Equal(toLocalWitnessStack[1], []byte{0x01}) { + t.Fatalf("top item on witness stack should be 0x01, found: %v", + toLocalWitnessStack[1]) + } + + // Next, 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), + ) + + // Assert that the second element on the stack matches our expected + // signature under the revocation pubkey. + if !bytes.Equal(rawRevSigWithSigHash, toLocalWitnessStack[0]) { + t.Fatalf("mismatched sig in to-local witness stack, want: %v, "+ + "got: %v", rawRevSigWithSigHash, toLocalWitnessStack[0]) + } +}