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 // and for a watchtower to later decrypt if action must be taken. The encoding
// format is versioned to allow future extensions. // format is versioned to allow future extensions.
type JusticeKit struct { 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 // SweepAddress is the witness program of the output where the client's
// fund will be deposited. This value is included in the blobs, as // 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 // 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 // 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) { func (b *JusticeKit) CommitToRemoteWitnessScript() ([]byte, error) {
if !btcec.IsCompressedPubKey(b.CommitToRemotePubKey[:]) { if !btcec.IsCompressedPubKey(b.CommitToRemotePubKey[:]) {
return nil, ErrNoCommitToRemoteOutput 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 return b.CommitToRemotePubKey[:], nil
} }
// CommitToRemoteWitnessStack returns a witness stack spending the commitment // 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> // <to-remote-sig>
func (b *JusticeKit) CommitToRemoteWitnessStack() ([][]byte, error) { func (b *JusticeKit) CommitToRemoteWitnessStack() ([][]byte, error) {
toRemoteSig, err := b.CommitToRemoteSig.ToSignature() 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 // NOTE: It is the caller's responsibility to ensure that this method is only
// called once for a given (nonce, key) pair. // 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 // Encode the plaintext using the provided version, to obtain the
// plaintext bytes. // plaintext bytes.
var ptxtBuf bytes.Buffer var ptxtBuf bytes.Buffer
err := b.encode(&ptxtBuf, blobType) err := b.encode(&ptxtBuf, b.BlobType)
if err != nil { if err != nil {
return nil, err 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 // Allocate the ciphertext, which will contain the nonce, encrypted
// plaintext and MAC. // plaintext and MAC.
plaintext := ptxtBuf.Bytes() 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. // Generate a random 24-byte nonce in the ciphertext's prefix.
nonce := ciphertext[:NonceSize] nonce := ciphertext[:NonceSize]
@ -284,7 +309,9 @@ func Decrypt(key BreachKey, ciphertext []byte,
// If decryption succeeded, we will then decode the plaintext bytes // If decryption succeeded, we will then decode the plaintext bytes
// using the specified blob version. // using the specified blob version.
boj := &JusticeKit{} boj := &JusticeKit{
BlobType: blobType,
}
err = boj.decode(bytes.NewReader(plaintext), blobType) err = boj.decode(bytes.NewReader(plaintext), blobType)
if err != nil { if err != nil {
return nil, err return nil, err

@ -13,6 +13,7 @@ import (
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/stretchr/testify/require"
) )
func makePubKey(i uint64) blob.PubKey { func makePubKey(i uint64) blob.PubKey {
@ -149,6 +150,7 @@ func TestBlobJusticeKitEncryptDecrypt(t *testing.T) {
func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) { func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
boj := &blob.JusticeKit{ boj := &blob.JusticeKit{
BlobType: test.encVersion,
SweepAddress: test.sweepAddr, SweepAddress: test.sweepAddr,
RevocationPubKey: test.revPubKey, RevocationPubKey: test.revPubKey,
LocalDelayPubKey: test.delayPubKey, LocalDelayPubKey: test.delayPubKey,
@ -169,7 +171,7 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
// Encrypt the blob plaintext using the generated key and // Encrypt the blob plaintext using the generated key and
// target version for this test. // target version for this test.
ctxt, err := boj.Encrypt(key, test.encVersion) ctxt, err := boj.Encrypt(key)
if err != test.encErr { if err != test.encErr {
t.Fatalf("unable to encrypt blob: %v", err) t.Fatalf("unable to encrypt blob: %v", err)
} else if test.encErr != nil { } 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 // TestJusticeKitRemoteWitnessConstruction tests that a JusticeKit returns the
// proper to-remote witnes script and to-remote witness stack. This should be // proper to-remote witnes script and to-remote witness stack. This should be
// equivalent to p2wkh spend. // equivalent to p2wkh spend.
func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { 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. // Generate the to-remote pubkey.
toRemotePrivKey, err := btcec.NewPrivateKey(btcec.S256()) toRemotePrivKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil { require.Nil(t, err)
t.Fatalf("unable to generate to-remote priv key: %v", err)
}
// Copy the to-remote pubkey into the format expected by our justice // Copy the to-remote pubkey into the format expected by our justice
// kit. // kit.
@ -232,19 +267,15 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
// doesn't matter as we won't be validating the signature's validity. // doesn't matter as we won't be validating the signature's validity.
digest := bytes.Repeat([]byte("a"), 32) digest := bytes.Repeat([]byte("a"), 32)
rawToRemoteSig, err := toRemotePrivKey.Sign(digest) rawToRemoteSig, err := toRemotePrivKey.Sign(digest)
if err != nil { require.Nil(t, err)
t.Fatalf("unable to generate to-remote signature: %v", err)
}
// Convert the DER-encoded signature into a fixed-size sig. // Convert the DER-encoded signature into a fixed-size sig.
commitToRemoteSig, err := lnwire.NewSigFromSignature(rawToRemoteSig) commitToRemoteSig, err := lnwire.NewSigFromSignature(rawToRemoteSig)
if err != nil { require.Nil(t, err)
t.Fatalf("unable to convert raw to-remote signature to "+
"Sig: %v", err)
}
// Populate the justice kit fields relevant to the to-remote output. // Populate the justice kit fields relevant to the to-remote output.
justiceKit := &blob.JusticeKit{ justiceKit := &blob.JusticeKit{
BlobType: test.blobType,
CommitToRemotePubKey: toRemotePubKey, CommitToRemotePubKey: toRemotePubKey,
CommitToRemoteSig: commitToRemoteSig, CommitToRemoteSig: commitToRemoteSig,
} }
@ -252,29 +283,16 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
// Now, compute the to-remote witness script returned by the justice // Now, compute the to-remote witness script returned by the justice
// kit. // kit.
toRemoteScript, err := justiceKit.CommitToRemoteWitnessScript() toRemoteScript, err := justiceKit.CommitToRemoteWitnessScript()
if err != nil { require.Nil(t, err)
t.Fatalf("unable to compute to-remote witness script: %v", err)
}
// Assert this is exactly the to-remote, compressed pubkey. // Assert this is exactly the to-remote, compressed pubkey.
if !bytes.Equal(toRemoteScript, toRemotePubKey[:]) { expToRemoteScript := test.expWitnessScript(toRemotePrivKey.PubKey())
t.Fatalf("to-remote witness script should be equal to "+ require.Equal(t, expToRemoteScript, toRemoteScript)
"to-remote pubkey, want: %x, got %x",
toRemotePubKey[:], toRemoteScript)
}
// Next, compute the to-remote witness stack, which should be a p2wkh // Next, compute the to-remote witness stack, which should be a p2wkh
// witness stack consisting solely of a signature. // witness stack consisting solely of a signature.
toRemoteWitnessStack, err := justiceKit.CommitToRemoteWitnessStack() toRemoteWitnessStack, err := justiceKit.CommitToRemoteWitnessStack()
if err != nil { require.Nil(t, err)
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 // Compute the expected first element, by appending a sighash all byte
// to our raw DER-encoded signature. // to our raw DER-encoded signature.
@ -282,13 +300,11 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
rawToRemoteSig.Serialize(), byte(txscript.SigHashAll), rawToRemoteSig.Serialize(), byte(txscript.SigHashAll),
) )
// Assert that the expected signature matches the first element in the // Assert that the expected witness stack is returned.
// witness stack. expWitnessStack := [][]byte{
if !bytes.Equal(rawToRemoteSigWithSigHash, toRemoteWitnessStack[0]) { rawToRemoteSigWithSigHash,
t.Fatalf("mismatched sig in to-remote witness stack, want: %v, "+
"got: %v", rawToRemoteSigWithSigHash,
toRemoteWitnessStack[0])
} }
require.Equal(t, expWitnessStack, toRemoteWitnessStack)
// Finally, set the CommitToRemotePubKey to be a blank value. // Finally, set the CommitToRemotePubKey to be a blank value.
justiceKit.CommitToRemotePubKey = blob.PubKey{} justiceKit.CommitToRemotePubKey = blob.PubKey{}
@ -297,9 +313,7 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
// ErrNoCommitToRemoteOutput since a valid pubkey could not be parsed // ErrNoCommitToRemoteOutput since a valid pubkey could not be parsed
// from CommitToRemotePubKey. // from CommitToRemotePubKey.
_, err = justiceKit.CommitToRemoteWitnessScript() _, err = justiceKit.CommitToRemoteWitnessScript()
if err != blob.ErrNoCommitToRemoteOutput { require.Error(t, blob.ErrNoCommitToRemoteOutput, err)
t.Fatalf("expected ErrNoCommitToRemoteOutput, got: %v", err)
}
} }
// TestJusticeKitToLocalWitnessConstruction tests that a JusticeKit returns the // TestJusticeKitToLocalWitnessConstruction tests that a JusticeKit returns the
@ -310,14 +324,10 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) {
// Generate the revocation and delay private keys. // Generate the revocation and delay private keys.
revPrivKey, err := btcec.NewPrivateKey(btcec.S256()) revPrivKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil { require.Nil(t, err)
t.Fatalf("unable to generate revocation priv key: %v", err)
}
delayPrivKey, err := btcec.NewPrivateKey(btcec.S256()) delayPrivKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil { require.Nil(t, err)
t.Fatalf("unable to generate delay priv key: %v", err)
}
// Copy the revocation and delay pubkeys into the format expected by our // Copy the revocation and delay pubkeys into the format expected by our
// justice kit. // justice kit.
@ -331,16 +341,11 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) {
// doesn't matter as we won't be validating the signature's validity. // doesn't matter as we won't be validating the signature's validity.
digest := bytes.Repeat([]byte("a"), 32) digest := bytes.Repeat([]byte("a"), 32)
rawRevSig, err := revPrivKey.Sign(digest) rawRevSig, err := revPrivKey.Sign(digest)
if err != nil { require.Nil(t, err)
t.Fatalf("unable to generate revocation signature: %v", err)
}
// Convert the DER-encoded signature into a fixed-size sig. // Convert the DER-encoded signature into a fixed-size sig.
commitToLocalSig, err := lnwire.NewSigFromSignature(rawRevSig) commitToLocalSig, err := lnwire.NewSigFromSignature(rawRevSig)
if err != nil { require.Nil(t, err)
t.Fatalf("unable to convert raw revocation signature to "+
"Sig: %v", err)
}
// Populate the justice kit with fields relevant to the to-local output. // Populate the justice kit with fields relevant to the to-local output.
justiceKit := &blob.JusticeKit{ justiceKit := &blob.JusticeKit{
@ -355,52 +360,29 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) {
expToLocalScript, err := input.CommitScriptToSelf( expToLocalScript, err := input.CommitScriptToSelf(
csvDelay, delayPrivKey.PubKey(), revPrivKey.PubKey(), csvDelay, delayPrivKey.PubKey(), revPrivKey.PubKey(),
) )
if err != nil { require.Nil(t, err)
t.Fatalf("unable to generate expected to-local script: %v", err)
}
// Compute the to-local script that is returned by the justice kit. // Compute the to-local script that is returned by the justice kit.
toLocalScript, err := justiceKit.CommitToLocalWitnessScript() toLocalScript, err := justiceKit.CommitToLocalWitnessScript()
if err != nil { require.Nil(t, err)
t.Fatalf("unable to compute to-local witness script: %v", err)
}
// Assert that the expected to-local script matches the actual script. // Assert that the expected to-local script matches the actual script.
if !bytes.Equal(expToLocalScript, toLocalScript) { require.Equal(t, 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. // Next, compute the to-local witness stack returned by the justice kit.
toLocalWitnessStack, err := justiceKit.CommitToLocalRevokeWitnessStack() toLocalWitnessStack, err := justiceKit.CommitToLocalRevokeWitnessStack()
if err != nil { require.Nil(t, err)
t.Fatalf("unable to compute to-local witness stack: %v", err)
}
// A valid witness that spends the revocation path should have exactly // Compute the expected signature in the bottom element of the stack, by
// two elements on the stack. // appending a sighash all flag to the raw DER signature.
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( rawRevSigWithSigHash := append(
rawRevSig.Serialize(), byte(txscript.SigHashAll), rawRevSig.Serialize(), byte(txscript.SigHashAll),
) )
// Assert that the second element on the stack matches our expected // Finally, validate against our expected witness stack.
// signature under the revocation pubkey. expWitnessStack := [][]byte{
if !bytes.Equal(rawRevSigWithSigHash, toLocalWitnessStack[0]) { rawRevSigWithSigHash,
t.Fatalf("mismatched sig in to-local witness stack, want: %v, "+ {1},
"got: %v", rawRevSigWithSigHash, toLocalWitnessStack[0])
} }
require.Equal(t, expWitnessStack, toLocalWitnessStack)
} }

@ -14,11 +14,16 @@ const (
// include the reward script negotiated during session creation. Without // include the reward script negotiated during session creation. Without
// the flag, there is only one output sweeping clients funds back to // the flag, there is only one output sweeping clients funds back to
// them solely. // them solely.
FlagReward Flag = 1 << iota FlagReward Flag = 1
// FlagCommitOutputs signals that the blob contains the information // FlagCommitOutputs signals that the blob contains the information
// required to sweep commitment outputs. // 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. // Type returns a Type consisting solely of this flag enabled.
@ -33,6 +38,8 @@ func (f Flag) String() string {
return "FlagReward" return "FlagReward"
case FlagCommitOutputs: case FlagCommitOutputs:
return "FlagCommitOutputs" return "FlagCommitOutputs"
case FlagAnchorChannel:
return "FlagAnchorChannel"
default: default:
return "FlagUnknown" return "FlagUnknown"
} }
@ -50,6 +57,11 @@ const (
// controlled by the user, and does not give the tower a reward. // controlled by the user, and does not give the tower a reward.
TypeAltruistCommit = Type(FlagCommitOutputs) 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 // TypeRewardCommit sweeps only commitment outputs to a sweep address
// controlled by the user, and pays a negotiated reward to the tower. // controlled by the user, and pays a negotiated reward to the tower.
TypeRewardCommit = Type(FlagCommitOutputs | FlagReward) TypeRewardCommit = Type(FlagCommitOutputs | FlagReward)
@ -70,10 +82,16 @@ func TypeFromFlags(flags ...Flag) Type {
return typ 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. // knownFlags maps the supported flags to their name.
var knownFlags = map[Flag]struct{}{ var knownFlags = map[Flag]struct{}{
FlagReward: {}, FlagReward: {},
FlagCommitOutputs: {}, FlagCommitOutputs: {},
FlagAnchorChannel: {},
} }
// String returns a human readable description of a Type. // String returns a human readable description of a Type.

@ -18,17 +18,17 @@ var typeStringTests = []typeStringTest{
{ {
name: "commit no-reward", name: "commit no-reward",
typ: blob.TypeAltruistCommit, typ: blob.TypeAltruistCommit,
expStr: "[FlagCommitOutputs|No-FlagReward]", expStr: "[No-FlagAnchorChannel|FlagCommitOutputs|No-FlagReward]",
}, },
{ {
name: "commit reward", name: "commit reward",
typ: blob.TypeRewardCommit, typ: blob.TypeRewardCommit,
expStr: "[FlagCommitOutputs|FlagReward]", expStr: "[No-FlagAnchorChannel|FlagCommitOutputs|FlagReward]",
}, },
{ {
name: "unknown flag", name: "unknown flag",
typ: unknownFlag.Type(), 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 txOut *wire.TxOut
outPoint wire.OutPoint outPoint wire.OutPoint
witness [][]byte witness [][]byte
sequence uint32
} }
// commitToLocalInput extracts the information required to spend the commit // commitToLocalInput extracts the information required to spend the commit
@ -104,20 +105,35 @@ func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) {
return nil, err return nil, err
} }
// Since the to-remote witness script should just be a regular p2wkh var (
// output, we'll parse it to retrieve the public key. toRemoteScriptHash []byte
toRemotePubKey, err := btcec.ParsePubKey(toRemoteScript, btcec.S256()) toRemoteSequence uint32
if err != nil {
return nil, err
}
// 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(
toRemotePubKey,
) )
if err != nil { if p.JusticeKit.BlobType.IsAnchorChannel() {
return nil, err 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())
if err != nil {
return nil, err
}
// 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(
toRemotePubKey,
)
if err != nil {
return nil, err
}
} }
// Locate the to-remote output on the breaching commitment transaction. // Locate the to-remote output on the breaching commitment transaction.
@ -146,6 +162,7 @@ func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) {
txOut: toRemoteTxOut, txOut: toRemoteTxOut,
outPoint: toRemoteOutPoint, outPoint: toRemoteOutPoint,
witness: buildWitness(witnessStack, toRemoteScript), witness: buildWitness(witnessStack, toRemoteScript),
sequence: toRemoteSequence,
}, nil }, nil
} }
@ -164,6 +181,7 @@ func (p *JusticeDescriptor) assembleJusticeTxn(txWeight int64,
totalAmt += btcutil.Amount(input.txOut.Value) totalAmt += btcutil.Amount(input.txOut.Value)
justiceTxn.AddTxIn(&wire.TxIn{ justiceTxn.AddTxIn(&wire.TxIn{
PreviousOutPoint: input.outPoint, 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 // An older ToLocalPenaltyWitnessSize constant used to underestimate the
// size by one byte. The diferrence in weight can cause different output // size by one byte. The diferrence in weight can cause different output
// values on the sweep transaction, so we mimic the original bug to // 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
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1) // 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) sweepInputs = append(sweepInputs, toLocalInput)
@ -279,8 +302,13 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
sweepInputs = append(sweepInputs, toRemoteInput) sweepInputs = append(sweepInputs, toRemoteInput)
if p.JusticeKit.BlobType.IsAnchorChannel() {
weightEstimate.AddWitnessInput(input.ToRemoteConfirmedWitnessSize)
} else {
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
}
} }
// TODO(conner): sweep htlc outputs // TODO(conner): sweep htlc outputs

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

@ -137,7 +137,9 @@ func TestLookoutBreachMatching(t *testing.T) {
} }
// Construct a justice kit for each possible breach transaction. // Construct a justice kit for each possible breach transaction.
blobType := blob.FlagCommitOutputs.Type()
blob1 := &blob.JusticeKit{ blob1 := &blob.JusticeKit{
BlobType: blobType,
SweepAddress: makeAddrSlice(22), SweepAddress: makeAddrSlice(22),
RevocationPubKey: makePubKey(1), RevocationPubKey: makePubKey(1),
LocalDelayPubKey: makePubKey(1), LocalDelayPubKey: makePubKey(1),
@ -145,6 +147,7 @@ func TestLookoutBreachMatching(t *testing.T) {
CommitToLocalSig: makeArray64(1), CommitToLocalSig: makeArray64(1),
} }
blob2 := &blob.JusticeKit{ blob2 := &blob.JusticeKit{
BlobType: blobType,
SweepAddress: makeAddrSlice(22), SweepAddress: makeAddrSlice(22),
RevocationPubKey: makePubKey(2), RevocationPubKey: makePubKey(2),
LocalDelayPubKey: makePubKey(2), LocalDelayPubKey: makePubKey(2),
@ -156,13 +159,13 @@ func TestLookoutBreachMatching(t *testing.T) {
key2 := blob.NewBreachKeyFromHash(&hash2) key2 := blob.NewBreachKeyFromHash(&hash2)
// Encrypt the first justice kit under breach key one. // 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 { if err != nil {
t.Fatalf("unable to encrypt sweep detail 1: %v", err) t.Fatalf("unable to encrypt sweep detail 1: %v", err)
} }
// Encrypt the second justice kit under breach key two. // 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 { if err != nil {
t.Fatalf("unable to encrypt sweep detail 2: %v", err) 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. // to-local script, and the remote CSV delay.
keyRing := t.breachInfo.KeyRing keyRing := t.breachInfo.KeyRing
justiceKit := &blob.JusticeKit{ justiceKit := &blob.JusticeKit{
BlobType: t.blobType,
SweepAddress: t.sweepPkScript, SweepAddress: t.sweepPkScript,
RevocationPubKey: toBlobPubKey(keyRing.RevocationKey), RevocationPubKey: toBlobPubKey(keyRing.RevocationKey),
LocalDelayPubKey: toBlobPubKey(keyRing.ToLocalKey), LocalDelayPubKey: toBlobPubKey(keyRing.ToLocalKey),
@ -299,7 +300,7 @@ func (t *backupTask) craftSessionPayload(
// Then, we'll encrypt the computed justice kit using the full breach // Then, we'll encrypt the computed justice kit using the full breach
// transaction id, which will allow the tower to recover the contents // transaction id, which will allow the tower to recover the contents
// after the transaction is seen in the chain or mempool. // 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 { if err != nil {
return hint, nil, err return hint, nil, err
} }