diff --git a/watchtower/blob/justice_kit_test.go b/watchtower/blob/justice_kit_test.go new file mode 100644 index 00000000..4d9c7537 --- /dev/null +++ b/watchtower/blob/justice_kit_test.go @@ -0,0 +1,170 @@ +package blob_test + +import ( + "crypto/rand" + "encoding/binary" + "io" + "reflect" + "testing" + + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/watchtower/blob" +) + +func makePubKey(i uint64) blob.PubKey { + var pk blob.PubKey + pk[0] = 0x02 + if i%2 == 1 { + pk[0] |= 0x01 + } + binary.BigEndian.PutUint64(pk[1:9], i) + return pk +} + +func makeSig(i int) lnwire.Sig { + var sig lnwire.Sig + binary.BigEndian.PutUint64(sig[:8], uint64(i)) + return sig +} + +var descriptorTests = []struct { + name string + encVersion uint16 + decVersion uint16 + revPubKey blob.PubKey + delayPubKey blob.PubKey + csvDelay uint32 + commitToLocalSig lnwire.Sig + hasCommitToRemote bool + commitToRemotePubKey blob.PubKey + commitToRemoteSig lnwire.Sig + encErr error + decErr error +}{ + { + name: "to-local only", + encVersion: 0, + decVersion: 0, + revPubKey: makePubKey(0), + delayPubKey: makePubKey(1), + csvDelay: 144, + commitToLocalSig: makeSig(1), + }, + { + name: "to-local and p2wkh", + encVersion: 0, + decVersion: 0, + revPubKey: makePubKey(0), + delayPubKey: makePubKey(1), + csvDelay: 144, + commitToLocalSig: makeSig(1), + hasCommitToRemote: true, + commitToRemotePubKey: makePubKey(2), + commitToRemoteSig: makeSig(2), + }, + { + name: "unknown encrypt version", + encVersion: 1, + decVersion: 0, + revPubKey: makePubKey(0), + delayPubKey: makePubKey(1), + csvDelay: 144, + commitToLocalSig: makeSig(1), + encErr: blob.ErrUnknownBlobVersion, + }, + { + name: "unknown decrypt version", + encVersion: 0, + decVersion: 1, + revPubKey: makePubKey(0), + delayPubKey: makePubKey(1), + csvDelay: 144, + commitToLocalSig: makeSig(1), + decErr: blob.ErrUnknownBlobVersion, + }, +} + +// 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 i, test := range descriptorTests { + boj := &blob.JusticeKit{ + RevocationPubKey: test.revPubKey, + LocalDelayPubKey: test.delayPubKey, + CSVDelay: test.csvDelay, + CommitToLocalSig: test.commitToLocalSig, + CommitToRemotePubKey: test.commitToRemotePubKey, + CommitToRemoteSig: 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. + key := make([]byte, blob.KeySize) + _, err := io.ReadFull(rand.Reader, key) + if err != nil { + t.Fatalf("test #%d %s -- unable to generate blob "+ + "encryption key: %v", i, test.name, err) + } + + nonce := make([]byte, blob.NonceSize) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + t.Fatalf("test #%d %s -- unable to generate nonce "+ + "nonce: %v", i, test.name, err) + } + + // Encrypt the blob plaintext using the generated key and + // target version for this test. + ctxt, err := boj.Encrypt(nonce, key, test.encVersion) + if err != test.encErr { + t.Fatalf("test #%d %s -- unable to encrypt blob: %v", + i, test.name, err) + } else if test.encErr != nil { + // If the test expected an encryption failure, we can + // continue to the next test. + continue + } + + // Ensure that all encrypted blobs are padded out to the same + // size: 282 bytes for version 0. + if len(ctxt) != blob.Size(test.encVersion) { + t.Fatalf("test #%d %s -- expected blob to have "+ + "size %d, got %d instead", i, test.name, + blob.Size(test.encVersion), len(ctxt)) + + } + + // 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 := blob.Decrypt(nonce, key, ctxt, test.decVersion) + if err != test.decErr { + t.Fatalf("test #%d %s -- unable to decrypt blob: %v", + i, test.name, err) + } else if test.decErr != nil { + // If the test expected an decryption failure, we can + // continue to the next test. + continue + } + + // Check that the decrypted blob properly reports whether it has + // a to-remote output or not. + if boj2.HasCommitToRemoteOutput() != test.hasCommitToRemote { + t.Fatalf("test #%d %s -- expected blob has_to_remote "+ + "to be %v, got %v", i, test.name, + test.hasCommitToRemote, + boj2.HasCommitToRemoteOutput()) + } + + // Check that the original blob plaintext matches the + // one reconstructed from the encrypted blob. + if !reflect.DeepEqual(boj, boj2) { + t.Fatalf("test #%d %s -- decrypted plaintext does not "+ + "match original, want: %v, got %v", + i, test.name, boj, boj2) + } + } +}