411 lines
13 KiB
Go
411 lines
13 KiB
Go
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"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type descriptorTest struct {
|
|
name string
|
|
encVersion blob.Type
|
|
decVersion blob.Type
|
|
sweepAddr []byte
|
|
revPubKey blob.PubKey
|
|
delayPubKey blob.PubKey
|
|
csvDelay uint32
|
|
commitToLocalSig lnwire.Sig
|
|
hasCommitToRemote bool
|
|
commitToRemotePubKey blob.PubKey
|
|
commitToRemoteSig lnwire.Sig
|
|
encErr error
|
|
decErr error
|
|
}
|
|
|
|
var rewardAndCommitType = blob.TypeFromFlags(
|
|
blob.FlagReward, blob.FlagCommitOutputs,
|
|
)
|
|
|
|
var descriptorTests = []descriptorTest{
|
|
{
|
|
name: "to-local only",
|
|
encVersion: blob.TypeDefault,
|
|
decVersion: blob.TypeDefault,
|
|
sweepAddr: makeAddr(22),
|
|
revPubKey: makePubKey(0),
|
|
delayPubKey: makePubKey(1),
|
|
csvDelay: 144,
|
|
commitToLocalSig: makeSig(1),
|
|
},
|
|
{
|
|
name: "to-local and p2wkh",
|
|
encVersion: rewardAndCommitType,
|
|
decVersion: rewardAndCommitType,
|
|
sweepAddr: makeAddr(22),
|
|
revPubKey: makePubKey(0),
|
|
delayPubKey: makePubKey(1),
|
|
csvDelay: 144,
|
|
commitToLocalSig: makeSig(1),
|
|
hasCommitToRemote: true,
|
|
commitToRemotePubKey: makePubKey(2),
|
|
commitToRemoteSig: makeSig(2),
|
|
},
|
|
{
|
|
name: "unknown encrypt version",
|
|
encVersion: 0,
|
|
decVersion: blob.TypeDefault,
|
|
sweepAddr: makeAddr(34),
|
|
revPubKey: makePubKey(0),
|
|
delayPubKey: makePubKey(1),
|
|
csvDelay: 144,
|
|
commitToLocalSig: makeSig(1),
|
|
encErr: blob.ErrUnknownBlobType,
|
|
},
|
|
{
|
|
name: "unknown decrypt version",
|
|
encVersion: blob.TypeDefault,
|
|
decVersion: 0,
|
|
sweepAddr: makeAddr(34),
|
|
revPubKey: makePubKey(0),
|
|
delayPubKey: makePubKey(1),
|
|
csvDelay: 144,
|
|
commitToLocalSig: makeSig(1),
|
|
decErr: blob.ErrUnknownBlobType,
|
|
},
|
|
{
|
|
name: "sweep addr length zero",
|
|
encVersion: blob.TypeDefault,
|
|
decVersion: blob.TypeDefault,
|
|
sweepAddr: makeAddr(0),
|
|
revPubKey: makePubKey(0),
|
|
delayPubKey: makePubKey(1),
|
|
csvDelay: 144,
|
|
commitToLocalSig: makeSig(1),
|
|
},
|
|
{
|
|
name: "sweep addr max size",
|
|
encVersion: blob.TypeDefault,
|
|
decVersion: blob.TypeDefault,
|
|
sweepAddr: makeAddr(blob.MaxSweepAddrSize),
|
|
revPubKey: makePubKey(0),
|
|
delayPubKey: makePubKey(1),
|
|
csvDelay: 144,
|
|
commitToLocalSig: makeSig(1),
|
|
},
|
|
{
|
|
name: "sweep addr too long",
|
|
encVersion: blob.TypeDefault,
|
|
decVersion: blob.TypeDefault,
|
|
sweepAddr: makeAddr(blob.MaxSweepAddrSize + 1),
|
|
revPubKey: makePubKey(0),
|
|
delayPubKey: makePubKey(1),
|
|
csvDelay: 144,
|
|
commitToLocalSig: makeSig(1),
|
|
encErr: blob.ErrSweepAddressToLong,
|
|
},
|
|
}
|
|
|
|
// 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) {
|
|
boj := &blob.JusticeKit{
|
|
SweepAddress: test.sweepAddr,
|
|
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("unable to generate blob encryption key: %v", err)
|
|
}
|
|
|
|
// Encrypt the blob plaintext using the generated key and
|
|
// target version for this test.
|
|
ctxt, err := boj.Encrypt(key, test.encVersion)
|
|
if err != test.encErr {
|
|
t.Fatalf("unable to encrypt blob: %v", err)
|
|
} else 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.
|
|
if len(ctxt) != blob.Size(test.encVersion) {
|
|
t.Fatalf("expected blob to have size %d, got %d instead",
|
|
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(key, ctxt, test.decVersion)
|
|
if err != test.decErr {
|
|
t.Fatalf("unable to decrypt blob: %v", err)
|
|
} else 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.
|
|
if !reflect.DeepEqual(boj, boj2) {
|
|
t.Fatalf("decrypted plaintext does not match original, "+
|
|
"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])
|
|
}
|
|
}
|