Merge pull request #4576 from cfromknecht/anchor-justice-txn

watchtower: conditionally reconstruct justice txns for anchor channels
This commit is contained in:
Olaoluwa Osuntokun 2020-09-22 21:10:53 -07:00 committed by GitHub
commit 50976e5418
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 259 additions and 163 deletions

@ -100,6 +100,15 @@ type PubKey [33]byte
// and for a watchtower to later decrypt if action must be taken. The encoding
// format is versioned to allow future extensions.
type JusticeKit struct {
// BlobType encodes a bitfield that inform the tower of various features
// requested by the client when resolving a breach. Examples include
// whether the justice transaction contains a reward for the tower, or
// whether the channel is a legacy or anchor channel.
//
// NOTE: This value is not serialized in the encrypted payload. It is
// stored separately and added to the JusticeKit after decryption.
BlobType Type
// SweepAddress is the witness program of the output where the client's
// fund will be deposited. This value is included in the blobs, as
// opposed to the session info, such that the sweep addresses can't be
@ -187,17 +196,33 @@ func (b *JusticeKit) HasCommitToRemoteOutput() bool {
}
// CommitToRemoteWitnessScript returns the witness script for the commitment
// to-remote p2wkh output, which is the pubkey itself.
// to-remote output given the blob type. The script returned will either be for
// a p2wpkh to-remote output or an p2wsh anchor to-remote output which includes
// a CSV delay.
func (b *JusticeKit) CommitToRemoteWitnessScript() ([]byte, error) {
if !btcec.IsCompressedPubKey(b.CommitToRemotePubKey[:]) {
return nil, ErrNoCommitToRemoteOutput
}
// If this is a blob for an anchor channel, we'll return the p2wsh
// output containing a CSV delay of 1.
if b.BlobType.IsAnchorChannel() {
pk, err := btcec.ParsePubKey(
b.CommitToRemotePubKey[:], btcec.S256(),
)
if err != nil {
return nil, err
}
return input.CommitScriptToRemoteConfirmed(pk)
}
return b.CommitToRemotePubKey[:], nil
}
// CommitToRemoteWitnessStack returns a witness stack spending the commitment
// to-remote output, which is a regular p2wkh.
// to-remote output, which consists of a single signature satisfying either the
// legacy or anchor witness scripts.
// <to-remote-sig>
func (b *JusticeKit) CommitToRemoteWitnessStack() ([][]byte, error) {
toRemoteSig, err := b.CommitToRemoteSig.ToSignature()
@ -218,11 +243,11 @@ func (b *JusticeKit) CommitToRemoteWitnessStack() ([][]byte, error) {
//
// NOTE: It is the caller's responsibility to ensure that this method is only
// called once for a given (nonce, key) pair.
func (b *JusticeKit) Encrypt(key BreachKey, blobType Type) ([]byte, error) {
func (b *JusticeKit) Encrypt(key BreachKey) ([]byte, error) {
// Encode the plaintext using the provided version, to obtain the
// plaintext bytes.
var ptxtBuf bytes.Buffer
err := b.encode(&ptxtBuf, blobType)
err := b.encode(&ptxtBuf, b.BlobType)
if err != nil {
return nil, err
}
@ -236,7 +261,7 @@ func (b *JusticeKit) Encrypt(key BreachKey, blobType Type) ([]byte, error) {
// Allocate the ciphertext, which will contain the nonce, encrypted
// plaintext and MAC.
plaintext := ptxtBuf.Bytes()
ciphertext := make([]byte, Size(blobType))
ciphertext := make([]byte, Size(b.BlobType))
// Generate a random 24-byte nonce in the ciphertext's prefix.
nonce := ciphertext[:NonceSize]
@ -284,7 +309,9 @@ func Decrypt(key BreachKey, ciphertext []byte,
// If decryption succeeded, we will then decode the plaintext bytes
// using the specified blob version.
boj := &JusticeKit{}
boj := &JusticeKit{
BlobType: blobType,
}
err = boj.decode(bytes.NewReader(plaintext), blobType)
if err != nil {
return nil, err

@ -13,6 +13,7 @@ import (
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/stretchr/testify/require"
)
func makePubKey(i uint64) blob.PubKey {
@ -149,6 +150,7 @@ func TestBlobJusticeKitEncryptDecrypt(t *testing.T) {
func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
boj := &blob.JusticeKit{
BlobType: test.encVersion,
SweepAddress: test.sweepAddr,
RevocationPubKey: test.revPubKey,
LocalDelayPubKey: test.delayPubKey,
@ -169,7 +171,7 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
// Encrypt the blob plaintext using the generated key and
// target version for this test.
ctxt, err := boj.Encrypt(key, test.encVersion)
ctxt, err := boj.Encrypt(key)
if err != test.encErr {
t.Fatalf("unable to encrypt blob: %v", err)
} else if test.encErr != nil {
@ -213,15 +215,48 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
}
}
type remoteWitnessTest struct {
name string
blobType blob.Type
expWitnessScript func(pk *btcec.PublicKey) []byte
}
// 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: blob.Type(blob.FlagCommitOutputs),
expWitnessScript: func(pk *btcec.PublicKey) []byte {
return pk.SerializeCompressed()
},
},
{
name: "anchor commitment",
blobType: blob.Type(blob.FlagCommitOutputs |
blob.FlagAnchorChannel),
expWitnessScript: func(pk *btcec.PublicKey) []byte {
script, _ := input.CommitScriptToRemoteConfirmed(pk)
return script
},
},
}
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(btcec.S256())
if err != nil {
t.Fatalf("unable to generate to-remote priv key: %v", err)
}
require.Nil(t, err)
// Copy the to-remote pubkey into the format expected by our justice
// kit.
@ -232,19 +267,15 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
// 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)
}
require.Nil(t, 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)
}
require.Nil(t, err)
// Populate the justice kit fields relevant to the to-remote output.
justiceKit := &blob.JusticeKit{
BlobType: test.blobType,
CommitToRemotePubKey: toRemotePubKey,
CommitToRemoteSig: commitToRemoteSig,
}
@ -252,29 +283,16 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
// 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)
}
require.Nil(t, 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)
}
expToRemoteScript := test.expWitnessScript(toRemotePrivKey.PubKey())
require.Equal(t, expToRemoteScript, 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))
}
require.Nil(t, err)
// Compute the expected first element, by appending a sighash all byte
// to our raw DER-encoded signature.
@ -282,13 +300,11 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
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])
// Assert that the expected witness stack is returned.
expWitnessStack := [][]byte{
rawToRemoteSigWithSigHash,
}
require.Equal(t, expWitnessStack, toRemoteWitnessStack)
// Finally, set the CommitToRemotePubKey to be a blank value.
justiceKit.CommitToRemotePubKey = blob.PubKey{}
@ -297,9 +313,7 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
// 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)
}
require.Error(t, blob.ErrNoCommitToRemoteOutput, err)
}
// TestJusticeKitToLocalWitnessConstruction tests that a JusticeKit returns the
@ -310,14 +324,10 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) {
// 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)
}
require.Nil(t, err)
delayPrivKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
t.Fatalf("unable to generate delay priv key: %v", err)
}
require.Nil(t, err)
// Copy the revocation and delay pubkeys into the format expected by our
// justice kit.
@ -331,16 +341,11 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) {
// 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)
}
require.Nil(t, 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)
}
require.Nil(t, err)
// Populate the justice kit with fields relevant to the to-local output.
justiceKit := &blob.JusticeKit{
@ -355,52 +360,29 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) {
expToLocalScript, err := input.CommitScriptToSelf(
csvDelay, delayPrivKey.PubKey(), revPrivKey.PubKey(),
)
if err != nil {
t.Fatalf("unable to generate expected to-local script: %v", err)
}
require.Nil(t, 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)
}
require.Nil(t, 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)
}
require.Equal(t, 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)
}
require.Nil(t, 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.
// 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])
// Finally, validate against our expected witness stack.
expWitnessStack := [][]byte{
rawRevSigWithSigHash,
{1},
}
require.Equal(t, expWitnessStack, toLocalWitnessStack)
}

@ -14,11 +14,16 @@ const (
// include the reward script negotiated during session creation. Without
// the flag, there is only one output sweeping clients funds back to
// them solely.
FlagReward Flag = 1 << iota
FlagReward Flag = 1
// FlagCommitOutputs signals that the blob contains the information
// required to sweep commitment outputs.
FlagCommitOutputs
FlagCommitOutputs Flag = 1 << 1
// FlagAnchorChannel signals that this blob is meant to spend an anchor
// channel, and therefore must expect a P2WSH-style to-remote output if
// one exists.
FlagAnchorChannel Flag = 1 << 2
)
// Type returns a Type consisting solely of this flag enabled.
@ -33,6 +38,8 @@ func (f Flag) String() string {
return "FlagReward"
case FlagCommitOutputs:
return "FlagCommitOutputs"
case FlagAnchorChannel:
return "FlagAnchorChannel"
default:
return "FlagUnknown"
}
@ -50,6 +57,11 @@ const (
// controlled by the user, and does not give the tower a reward.
TypeAltruistCommit = Type(FlagCommitOutputs)
// TypeAltruistAnchorCommit sweeps only commitment outputs from an
// anchor commitment to a sweep address controlled by the user, and does
// not give the tower a reward.
TypeAltruistAnchorCommit = Type(FlagCommitOutputs | FlagAnchorChannel)
// TypeRewardCommit sweeps only commitment outputs to a sweep address
// controlled by the user, and pays a negotiated reward to the tower.
TypeRewardCommit = Type(FlagCommitOutputs | FlagReward)
@ -70,10 +82,16 @@ func TypeFromFlags(flags ...Flag) Type {
return typ
}
// IsAnchorChannel returns true if the blob type is for an anchor channel.
func (t Type) IsAnchorChannel() bool {
return t.Has(FlagAnchorChannel)
}
// knownFlags maps the supported flags to their name.
var knownFlags = map[Flag]struct{}{
FlagReward: {},
FlagCommitOutputs: {},
FlagAnchorChannel: {},
}
// String returns a human readable description of a Type.

@ -18,17 +18,17 @@ var typeStringTests = []typeStringTest{
{
name: "commit no-reward",
typ: blob.TypeAltruistCommit,
expStr: "[FlagCommitOutputs|No-FlagReward]",
expStr: "[No-FlagAnchorChannel|FlagCommitOutputs|No-FlagReward]",
},
{
name: "commit reward",
typ: blob.TypeRewardCommit,
expStr: "[FlagCommitOutputs|FlagReward]",
expStr: "[No-FlagAnchorChannel|FlagCommitOutputs|FlagReward]",
},
{
name: "unknown flag",
typ: unknownFlag.Type(),
expStr: "0000000000010000[No-FlagCommitOutputs|No-FlagReward]",
expStr: "0000000000010000[No-FlagAnchorChannel|No-FlagCommitOutputs|No-FlagReward]",
},
}

@ -48,6 +48,7 @@ type breachedInput struct {
txOut *wire.TxOut
outPoint wire.OutPoint
witness [][]byte
sequence uint32
}
// commitToLocalInput extracts the information required to spend the commit
@ -104,6 +105,20 @@ func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) {
return nil, err
}
var (
toRemoteScriptHash []byte
toRemoteSequence uint32
)
if p.JusticeKit.BlobType.IsAnchorChannel() {
toRemoteScriptHash, err = input.WitnessScriptHash(
toRemoteScript,
)
if err != nil {
return nil, err
}
toRemoteSequence = 1
} else {
// Since the to-remote witness script should just be a regular p2wkh
// output, we'll parse it to retrieve the public key.
toRemotePubKey, err := btcec.ParsePubKey(toRemoteScript, btcec.S256())
@ -113,12 +128,13 @@ func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) {
// Compute the witness script hash from the to-remote pubkey, which will
// be used to locate the input on the breach commitment transaction.
toRemoteScriptHash, err := input.CommitScriptUnencumbered(
toRemoteScriptHash, err = input.CommitScriptUnencumbered(
toRemotePubKey,
)
if err != nil {
return nil, err
}
}
// Locate the to-remote output on the breaching commitment transaction.
toRemoteIndex, toRemoteTxOut, err := findTxOutByPkScript(
@ -146,6 +162,7 @@ func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) {
txOut: toRemoteTxOut,
outPoint: toRemoteOutPoint,
witness: buildWitness(witnessStack, toRemoteScript),
sequence: toRemoteSequence,
}, nil
}
@ -164,6 +181,7 @@ func (p *JusticeDescriptor) assembleJusticeTxn(txWeight int64,
totalAmt += btcutil.Amount(input.txOut.Value)
justiceTxn.AddTxIn(&wire.TxIn{
PreviousOutPoint: input.outPoint,
Sequence: input.sequence,
})
}
@ -266,8 +284,13 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) {
// An older ToLocalPenaltyWitnessSize constant used to underestimate the
// size by one byte. The diferrence in weight can cause different output
// values on the sweep transaction, so we mimic the original bug to
// avoid invalidating signatures by older clients.
// avoid invalidating signatures by older clients. For anchor channels
// we correct this and use the correct witness size.
if p.JusticeKit.BlobType.IsAnchorChannel() {
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize)
} else {
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1)
}
sweepInputs = append(sweepInputs, toLocalInput)
@ -279,8 +302,13 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) {
if err != nil {
return nil, err
}
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
sweepInputs = append(sweepInputs, toRemoteInput)
if p.JusticeKit.BlobType.IsAnchorChannel() {
weightEstimate.AddWitnessInput(input.ToRemoteConfirmedWitnessSize)
} else {
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
}
}
// TODO(conner): sweep htlc outputs

@ -1,7 +1,6 @@
package lookout_test
import (
"reflect"
"testing"
"time"
@ -11,7 +10,6 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/txsort"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire"
@ -20,6 +18,7 @@ import (
"github.com/lightningnetwork/lnd/watchtower/wtdb"
"github.com/lightningnetwork/lnd/watchtower/wtmock"
"github.com/lightningnetwork/lnd/watchtower/wtpolicy"
"github.com/stretchr/testify/require"
)
const csvDelay uint32 = 144
@ -51,6 +50,8 @@ var (
)
altruistCommitType = blob.FlagCommitOutputs.Type()
altruistAnchorCommitType = blob.TypeAltruistAnchorCommit
)
// TestJusticeDescriptor asserts that a JusticeDescriptor is able to produce the
@ -68,6 +69,10 @@ func TestJusticeDescriptor(t *testing.T) {
name: "altruist and commit type",
blobType: altruistCommitType,
},
{
name: "altruist anchor commit type",
blobType: altruistAnchorCommitType,
},
}
for _, test := range tests {
@ -78,6 +83,8 @@ func TestJusticeDescriptor(t *testing.T) {
}
func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
isAnchorChannel := blobType.IsAnchorChannel()
const (
localAmount = btcutil.Amount(100000)
remoteAmount = btcutil.Amount(200000)
@ -106,20 +113,59 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
toLocalScript, err := input.CommitScriptToSelf(
csvDelay, toLocalPK, revPK,
)
if err != nil {
t.Fatalf("unable to create to-local script: %v", err)
}
require.Nil(t, err)
// Compute the to-local witness script hash.
toLocalScriptHash, err := input.WitnessScriptHash(toLocalScript)
if err != nil {
t.Fatalf("unable to create to-local witness script hash: %v", err)
}
require.Nil(t, err)
// Compute the to-remote witness script hash.
toRemoteScriptHash, err := input.CommitScriptUnencumbered(toRemotePK)
if err != nil {
t.Fatalf("unable to create to-remote script: %v", err)
// Compute the to-remote redeem script, witness script hash, and
// sequence numbers.
//
// NOTE: This is pretty subtle.
//
// The actual redeem script for a p2wkh output is just the pubkey, but
// the witness sighash calculation injects the classic p2kh script:
// OP_DUP OP_HASH160 <pubkey-hash160> OP_EQUALVERIFY OP_CHECKSIG. When
// signing for p2wkh we don't pass the raw pubkey as the witness script
// to the sign descriptor (since that's also not a valid script).
// Instead we give it the _pkscript_ of the form OP_0 <pubkey-hash160>
// from which pubkey-hash160 is extracted during sighash calculation.
//
// On the other hand, signing for the anchor p2wsh to-remote outputs
// requires the sign descriptor to contain the redeem script ver batim.
// This difference in behavior forces us to use a distinct
// toRemoteSigningScript to handle both cases.
var (
toRemoteSequence uint32
toRemoteRedeemScript []byte
toRemoteScriptHash []byte
toRemoteSigningScript []byte
)
if isAnchorChannel {
toRemoteSequence = 1
toRemoteRedeemScript, err = input.CommitScriptToRemoteConfirmed(
toRemotePK,
)
require.Nil(t, err)
toRemoteScriptHash, err = input.WitnessScriptHash(
toRemoteRedeemScript,
)
require.Nil(t, err)
// As it should be.
toRemoteSigningScript = toRemoteRedeemScript
} else {
toRemoteRedeemScript = toRemotePK.SerializeCompressed()
toRemoteScriptHash, err = input.CommitScriptUnencumbered(
toRemotePK,
)
require.Nil(t, err)
// NOTE: This is the _pkscript_.
toRemoteSigningScript = toRemoteScriptHash
}
// Construct the breaching commitment txn, containing the to-local and
@ -146,10 +192,19 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
// An older ToLocalPenaltyWitnessSize constant used to underestimate the
// size by one byte. The diferrence in weight can cause different output
// values on the sweep transaction, so we mimic the original bug and
// create signatures using the original weight estimate.
// create signatures using the original weight estimate. For anchor
// channels we fix this and use the correct witness size.
if isAnchorChannel {
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize)
} else {
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1)
}
if isAnchorChannel {
weightEstimate.AddWitnessInput(input.ToRemoteConfirmedWitnessSize)
} else {
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
}
weightEstimate.AddP2WKHOutput()
if blobType.Has(blob.FlagReward) {
weightEstimate.AddP2WKHOutput()
@ -174,6 +229,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
// Begin to assemble the justice kit, starting with the sweep address,
// pubkeys, and csv delay.
justiceKit := &blob.JusticeKit{
BlobType: blobType,
SweepAddress: makeAddrSlice(22),
CSVDelay: csvDelay,
}
@ -199,6 +255,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
Hash: breachTxID,
Index: 1,
},
Sequence: toRemoteSequence,
},
},
}
@ -207,9 +264,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
totalAmount, int64(txWeight), justiceKit.SweepAddress,
sessionInfo.RewardAddress,
)
if err != nil {
t.Fatalf("unable to compute justice txouts: %v", err)
}
require.Nil(t, err)
// Attach the txouts and BIP69 sort the resulting transaction.
justiceTxn.TxOut = outputs
@ -235,7 +290,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
KeyLocator: toRemoteKeyLoc,
PubKey: toRemotePK,
},
WitnessScript: toRemoteScriptHash,
WitnessScript: toRemoteSigningScript,
Output: breachTxn.TxOut[1],
SigHashes: hashCache,
InputIndex: 1,
@ -244,38 +299,26 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
// Verify that our test justice transaction is sane.
btx := btcutil.NewTx(justiceTxn)
if err := blockchain.CheckTransactionSanity(btx); err != nil {
t.Fatalf("justice txn is not sane: %v", err)
}
err = blockchain.CheckTransactionSanity(btx)
require.Nil(t, err)
// Compute a DER-encoded signature for the to-local input.
toLocalSigRaw, err := signer.SignOutputRaw(justiceTxn, toLocalSignDesc)
if err != nil {
t.Fatalf("unable to sign to-local input: %v", err)
}
require.Nil(t, err)
// Compute the witness for the to-remote input. The first element is a
// DER-encoded signature under the to-remote pubkey. The sighash flag is
// also present, so we trim it.
toRemoteWitness, err := input.CommitSpendNoDelay(
signer, toRemoteSignDesc, justiceTxn, false,
)
if err != nil {
t.Fatalf("unable to sign to-remote input: %v", err)
}
toRemoteSigRaw := toRemoteWitness[0][:len(toRemoteWitness[0])-1]
toRemoteSigRaw, err := signer.SignOutputRaw(justiceTxn, toRemoteSignDesc)
require.Nil(t, err)
// Convert the DER to-local sig into a fixed-size signature.
toLocalSig, err := lnwire.NewSigFromSignature(toLocalSigRaw)
if err != nil {
t.Fatalf("unable to parse to-local signature: %v", err)
}
require.Nil(t, err)
// Convert the DER to-remote sig into a fixed-size signature.
toRemoteSig, err := lnwire.NewSigFromRawSignature(toRemoteSigRaw)
if err != nil {
t.Fatalf("unable to parse to-remote signature: %v", err)
}
toRemoteSig, err := lnwire.NewSigFromSignature(toRemoteSigRaw)
require.Nil(t, err)
// Complete our justice kit by copying the signatures into the payload.
copy(justiceKit.CommitToLocalSig[:], toLocalSig[:])
@ -300,9 +343,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
// Exact retribution on the offender. If no error is returned, we expect
// the justice transaction to be published via the channel.
err = punisher.Punish(justiceDesc, nil)
if err != nil {
t.Fatalf("unable to punish breach: %v", err)
}
require.Nil(t, err)
// Retrieve the published justice transaction.
var wtJusticeTxn *wire.MsgTx
@ -321,14 +362,10 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
// Construct the test's to-remote witness.
justiceTxn.TxIn[1].Witness = make([][]byte, 2)
justiceTxn.TxIn[1].Witness[0] = append(toRemoteSigRaw,
justiceTxn.TxIn[1].Witness[0] = append(toRemoteSigRaw.Serialize(),
byte(txscript.SigHashAll))
justiceTxn.TxIn[1].Witness[1] = toRemotePK.SerializeCompressed()
justiceTxn.TxIn[1].Witness[1] = toRemoteRedeemScript
// Assert that the watchtower derives the same justice txn.
if !reflect.DeepEqual(justiceTxn, wtJusticeTxn) {
t.Fatalf("expected justice txn: %v\ngot %v",
spew.Sdump(justiceTxn),
spew.Sdump(wtJusticeTxn))
}
require.Equal(t, justiceTxn, wtJusticeTxn)
}

@ -137,7 +137,9 @@ func TestLookoutBreachMatching(t *testing.T) {
}
// Construct a justice kit for each possible breach transaction.
blobType := blob.FlagCommitOutputs.Type()
blob1 := &blob.JusticeKit{
BlobType: blobType,
SweepAddress: makeAddrSlice(22),
RevocationPubKey: makePubKey(1),
LocalDelayPubKey: makePubKey(1),
@ -145,6 +147,7 @@ func TestLookoutBreachMatching(t *testing.T) {
CommitToLocalSig: makeArray64(1),
}
blob2 := &blob.JusticeKit{
BlobType: blobType,
SweepAddress: makeAddrSlice(22),
RevocationPubKey: makePubKey(2),
LocalDelayPubKey: makePubKey(2),
@ -156,13 +159,13 @@ func TestLookoutBreachMatching(t *testing.T) {
key2 := blob.NewBreachKeyFromHash(&hash2)
// Encrypt the first justice kit under breach key one.
encBlob1, err := blob1.Encrypt(key1, blob.FlagCommitOutputs.Type())
encBlob1, err := blob1.Encrypt(key1)
if err != nil {
t.Fatalf("unable to encrypt sweep detail 1: %v", err)
}
// Encrypt the second justice kit under breach key two.
encBlob2, err := blob2.Encrypt(key2, blob.FlagCommitOutputs.Type())
encBlob2, err := blob2.Encrypt(key2)
if err != nil {
t.Fatalf("unable to encrypt sweep detail 2: %v", err)
}

@ -194,6 +194,7 @@ func (t *backupTask) craftSessionPayload(
// to-local script, and the remote CSV delay.
keyRing := t.breachInfo.KeyRing
justiceKit := &blob.JusticeKit{
BlobType: t.blobType,
SweepAddress: t.sweepPkScript,
RevocationPubKey: toBlobPubKey(keyRing.RevocationKey),
LocalDelayPubKey: toBlobPubKey(keyRing.ToLocalKey),
@ -299,7 +300,7 @@ func (t *backupTask) craftSessionPayload(
// Then, we'll encrypt the computed justice kit using the full breach
// transaction id, which will allow the tower to recover the contents
// after the transaction is seen in the chain or mempool.
encBlob, err := justiceKit.Encrypt(key, t.blobType)
encBlob, err := justiceKit.Encrypt(key)
if err != nil {
return hint, nil, err
}