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 34fd726a8..07f3676f4 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 { @@ -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) } diff --git a/watchtower/blob/type.go b/watchtower/blob/type.go index 923614dbb..f24b41f31 100644 --- a/watchtower/blob/type.go +++ b/watchtower/blob/type.go @@ -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. 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]", }, } diff --git a/watchtower/lookout/justice_descriptor.go b/watchtower/lookout/justice_descriptor.go index cea3ae032..016bc4aae 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, }) } @@ -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. - 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) @@ -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 diff --git a/watchtower/lookout/justice_descriptor_test.go b/watchtower/lookout/justice_descriptor_test.go index 68a7768c9..dcc79d3fd 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 @@ -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 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 @@ -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. - 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) + } - weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) + 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) } 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 }