From ed67ce7678d7acff8db5418f5588e35c75740512 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Tue, 15 Sep 2020 12:43:06 -0400 Subject: [PATCH 1/8] watchtower/blob/type: remove use of iota for flag constants --- watchtower/blob/type.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watchtower/blob/type.go b/watchtower/blob/type.go index 923614dbb..da83472c4 100644 --- a/watchtower/blob/type.go +++ b/watchtower/blob/type.go @@ -14,11 +14,11 @@ 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 ) // Type returns a Type consisting solely of this flag enabled. From 0477c80732c8b6c66fa471ec1bbc8b561603d630 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Tue, 15 Sep 2020 12:43:20 -0400 Subject: [PATCH 2/8] watchtower/blob/type: add new FlagAnchorChannel --- watchtower/blob/type.go | 18 ++++++++++++++++++ watchtower/blob/type_test.go | 6 +++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/watchtower/blob/type.go b/watchtower/blob/type.go index da83472c4..f24b41f31 100644 --- a/watchtower/blob/type.go +++ b/watchtower/blob/type.go @@ -19,6 +19,11 @@ const ( // FlagCommitOutputs signals that the blob contains the information // required to sweep commitment outputs. 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. diff --git a/watchtower/blob/type_test.go b/watchtower/blob/type_test.go index 95b01c017..a88965b2e 100644 --- a/watchtower/blob/type_test.go +++ b/watchtower/blob/type_test.go @@ -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]", }, } From b82695dbccb594a5e87a119975a2ce74d895750c Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Tue, 15 Sep 2020 12:43:26 -0400 Subject: [PATCH 3/8] watchtower/blob/justice_kit_test: convert to require This is also a prepatory step to making TestJusticeKitRemoteWitnessConstruction parameteried by the blob type so we can test both anchor and legacy witness construction. --- watchtower/blob/justice_kit_test.go | 110 +++++++--------------------- 1 file changed, 27 insertions(+), 83 deletions(-) diff --git a/watchtower/blob/justice_kit_test.go b/watchtower/blob/justice_kit_test.go index 34fd726a8..3bec48ee6 100644 --- a/watchtower/blob/justice_kit_test.go +++ b/watchtower/blob/justice_kit_test.go @@ -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 { @@ -219,9 +220,7 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) { 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) - } + require.Nil(t, err) // Copy the to-remote pubkey into the format expected by our justice // kit. @@ -232,16 +231,11 @@ 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{ @@ -252,29 +246,15 @@ 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) - } + require.Equal(t, toRemoteScript, toRemotePubKey[:]) // 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 +262,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 +275,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 +286,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 +303,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 +322,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) } From ffe15e2820f1dee944260606573fd93e7b29c202 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Tue, 15 Sep 2020 12:43:37 -0400 Subject: [PATCH 4/8] watchtower/blob/justice_kit: add BlobType to JusticeKit struct This is preparation for later commits where the values returned by member methods will need to be conditioned on the blob type used during decryption. --- watchtower/blob/justice_kit.go | 39 ++++++++++++++++++++++++----- watchtower/blob/justice_kit_test.go | 3 ++- watchtower/lookout/lookout_test.go | 7 ++++-- watchtower/wtclient/backup_task.go | 3 ++- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/watchtower/blob/justice_kit.go b/watchtower/blob/justice_kit.go index dd0213fb6..20c44cefc 100644 --- a/watchtower/blob/justice_kit.go +++ b/watchtower/blob/justice_kit.go @@ -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. // 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 diff --git a/watchtower/blob/justice_kit_test.go b/watchtower/blob/justice_kit_test.go index 3bec48ee6..69b12b097 100644 --- a/watchtower/blob/justice_kit_test.go +++ b/watchtower/blob/justice_kit_test.go @@ -150,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, @@ -170,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 { diff --git a/watchtower/lookout/lookout_test.go b/watchtower/lookout/lookout_test.go index 157e30422..da2159c19 100644 --- a/watchtower/lookout/lookout_test.go +++ b/watchtower/lookout/lookout_test.go @@ -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) } diff --git a/watchtower/wtclient/backup_task.go b/watchtower/wtclient/backup_task.go index 302a6bc3b..9994f7d0e 100644 --- a/watchtower/wtclient/backup_task.go +++ b/watchtower/wtclient/backup_task.go @@ -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 } From ac2e1d7d96f40892c703c8238e64cb84fc3858aa Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Tue, 15 Sep 2020 12:43:44 -0400 Subject: [PATCH 5/8] watchtower/blob/justice_kit: conditional anchor to-remote script This commit modifies the ToRemoteWitnessScript function to be conditioned on the blob type, and return either the legacy or anchor to-remote script. The same witness satisfies either script, so no changes are necessary to ToRemoteWitnessStack. --- watchtower/blob/justice_kit_test.go | 39 ++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/watchtower/blob/justice_kit_test.go b/watchtower/blob/justice_kit_test.go index 69b12b097..07f3676f4 100644 --- a/watchtower/blob/justice_kit_test.go +++ b/watchtower/blob/justice_kit_test.go @@ -215,10 +215,45 @@ 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()) require.Nil(t, err) @@ -240,6 +275,7 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { // Populate the justice kit fields relevant to the to-remote output. justiceKit := &blob.JusticeKit{ + BlobType: test.blobType, CommitToRemotePubKey: toRemotePubKey, CommitToRemoteSig: commitToRemoteSig, } @@ -250,7 +286,8 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { require.Nil(t, err) // Assert this is exactly the to-remote, compressed pubkey. - require.Equal(t, toRemoteScript, toRemotePubKey[:]) + 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. From cfbde5d2cee539216ca6ff630c967a6c9c3c8c75 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Tue, 15 Sep 2020 12:43:52 -0400 Subject: [PATCH 6/8] watchtower/lookout/justice_descriptor_test: use require --- watchtower/lookout/justice_descriptor_test.go | 50 +++++-------------- 1 file changed, 13 insertions(+), 37 deletions(-) diff --git a/watchtower/lookout/justice_descriptor_test.go b/watchtower/lookout/justice_descriptor_test.go index 68a7768c9..93d9c94c3 100644 --- a/watchtower/lookout/justice_descriptor_test.go +++ b/watchtower/lookout/justice_descriptor_test.go @@ -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 @@ -106,21 +105,15 @@ 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) - } + require.Nil(t, err) // Construct the breaching commitment txn, containing the to-local and // to-remote outputs. We don't need any inputs for this test. @@ -207,9 +200,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 @@ -244,15 +235,12 @@ 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 @@ -260,22 +248,16 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { toRemoteWitness, err := input.CommitSpendNoDelay( signer, toRemoteSignDesc, justiceTxn, false, ) - if err != nil { - t.Fatalf("unable to sign to-remote input: %v", err) - } + require.Nil(t, err) toRemoteSigRaw := toRemoteWitness[0][:len(toRemoteWitness[0])-1] // 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) - } + require.Nil(t, err) // Complete our justice kit by copying the signatures into the payload. copy(justiceKit.CommitToLocalSig[:], toLocalSig[:]) @@ -300,9 +282,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 @@ -326,9 +306,5 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { justiceTxn.TxIn[1].Witness[1] = toRemotePK.SerializeCompressed() // 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) } From d440cc40248639f0fbe14fcfc12d1ca3aa1fc0e8 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Tue, 15 Sep 2020 12:44:00 -0400 Subject: [PATCH 7/8] watchtower/lookout/justice_descriptor: anchor justice txn This commit modifies the JusticeDescriptor to support creation of justice transactions spending from anchor commitments. Rather than the unencumbered p2wkh scripts from before, the tower will now use the to-remote-confirmed that includes the additional CSV delay of 1. This also requires setting the sequence number appropriately on the to-remote input. --- watchtower/lookout/justice_descriptor.go | 51 ++++++++---- watchtower/lookout/justice_descriptor_test.go | 80 ++++++++++++++++--- 2 files changed, 105 insertions(+), 26 deletions(-) diff --git a/watchtower/lookout/justice_descriptor.go b/watchtower/lookout/justice_descriptor.go index cea3ae032..e86748e26 100644 --- a/watchtower/lookout/justice_descriptor.go +++ b/watchtower/lookout/justice_descriptor.go @@ -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,20 +105,35 @@ func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) { return nil, err } - // 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, + var ( + toRemoteScriptHash []byte + toRemoteSequence uint32 ) - if err != nil { - return nil, err + 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()) + 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. @@ -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, }) } @@ -279,8 +297,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 diff --git a/watchtower/lookout/justice_descriptor_test.go b/watchtower/lookout/justice_descriptor_test.go index 93d9c94c3..88ef40ae9 100644 --- a/watchtower/lookout/justice_descriptor_test.go +++ b/watchtower/lookout/justice_descriptor_test.go @@ -50,6 +50,8 @@ var ( ) altruistCommitType = blob.FlagCommitOutputs.Type() + + altruistAnchorCommitType = blob.TypeAltruistAnchorCommit ) // TestJusticeDescriptor asserts that a JusticeDescriptor is able to produce the @@ -67,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 { @@ -77,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) @@ -111,9 +119,54 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { toLocalScriptHash, err := input.WitnessScriptHash(toLocalScript) require.Nil(t, err) - // Compute the to-remote witness script hash. - toRemoteScriptHash, err := input.CommitScriptUnencumbered(toRemotePK) - require.Nil(t, 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 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 + // 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 // to-remote outputs. We don't need any inputs for this test. @@ -142,7 +195,11 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { // create signatures using the original weight estimate. weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1) - weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) + if isAnchorChannel { + weightEstimate.AddWitnessInput(input.ToRemoteConfirmedWitnessSize) + } else { + weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) + } weightEstimate.AddP2WKHOutput() if blobType.Has(blob.FlagReward) { weightEstimate.AddP2WKHOutput() @@ -167,6 +224,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, } @@ -192,6 +250,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { Hash: breachTxID, Index: 1, }, + Sequence: toRemoteSequence, }, }, } @@ -226,7 +285,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, @@ -245,18 +304,15 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { // 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, - ) + toRemoteSigRaw, err := signer.SignOutputRaw(justiceTxn, toRemoteSignDesc) require.Nil(t, err) - toRemoteSigRaw := toRemoteWitness[0][:len(toRemoteWitness[0])-1] // Convert the DER to-local sig into a fixed-size signature. toLocalSig, err := lnwire.NewSigFromSignature(toLocalSigRaw) require.Nil(t, err) // Convert the DER to-remote sig into a fixed-size signature. - toRemoteSig, err := lnwire.NewSigFromRawSignature(toRemoteSigRaw) + toRemoteSig, err := lnwire.NewSigFromSignature(toRemoteSigRaw) require.Nil(t, err) // Complete our justice kit by copying the signatures into the payload. @@ -301,9 +357,9 @@ 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. require.Equal(t, justiceTxn, wtJusticeTxn) From a0e54a9650b52007d68b2070fe15a50c50aef44a Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Tue, 15 Sep 2020 12:48:23 -0400 Subject: [PATCH 8/8] watchtower/lookout: use correct to-local-penalty size for anchors This commit fixes the to-local-witness estimate to use the correct witness size estimate for anchor channels. We retain the off-by-one bug from the original constant otherwise. --- watchtower/lookout/justice_descriptor.go | 9 +++++++-- watchtower/lookout/justice_descriptor_test.go | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/watchtower/lookout/justice_descriptor.go b/watchtower/lookout/justice_descriptor.go index e86748e26..016bc4aae 100644 --- a/watchtower/lookout/justice_descriptor.go +++ b/watchtower/lookout/justice_descriptor.go @@ -284,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. - weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1) + // 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) diff --git a/watchtower/lookout/justice_descriptor_test.go b/watchtower/lookout/justice_descriptor_test.go index 88ef40ae9..dcc79d3fd 100644 --- a/watchtower/lookout/justice_descriptor_test.go +++ b/watchtower/lookout/justice_descriptor_test.go @@ -192,8 +192,13 @@ 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. - weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1) + // 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)