lnd/watchtower/blob/justice_kit_test.go

544 lines
15 KiB
Go
Raw Normal View History

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/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)
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,
)
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])
}