diff --git a/watchtower/blob/commitments.go b/watchtower/blob/commitments.go index 4ae935045..0f476e5a3 100644 --- a/watchtower/blob/commitments.go +++ b/watchtower/blob/commitments.go @@ -172,3 +172,42 @@ func (c CommitmentType) ParseRawSig(witness wire.TxWitness) (lnwire.Sig, c) } } + +// NewJusticeKit can be used to construct a new JusticeKit depending on the +// CommitmentType. +func (c CommitmentType) NewJusticeKit(sweepScript []byte, + breachInfo *lnwallet.BreachRetribution, withToRemote bool) (JusticeKit, + error) { + + switch c { + case LegacyCommitment, LegacyTweaklessCommitment: + return newLegacyJusticeKit( + sweepScript, breachInfo, withToRemote, + ), nil + + case AnchorCommitment: + return newAnchorJusticeKit( + sweepScript, breachInfo, withToRemote, + ), nil + + default: + return nil, fmt.Errorf("unknown commitment type: %v", c) + } +} + +// EmptyJusticeKit returns the appropriate empty justice kit for the given +// CommitmentType. +func (c CommitmentType) EmptyJusticeKit() (JusticeKit, error) { + switch c { + case LegacyTweaklessCommitment, LegacyCommitment: + return &legacyJusticeKit{}, nil + + case AnchorCommitment: + return &anchorJusticeKit{ + legacyJusticeKit: legacyJusticeKit{}, + }, nil + + default: + return nil, fmt.Errorf("unknown commitment type: %v", c) + } +} diff --git a/watchtower/blob/justice_kit.go b/watchtower/blob/justice_kit.go index c95db77eb..7741ff026 100644 --- a/watchtower/blob/justice_kit.go +++ b/watchtower/blob/justice_kit.go @@ -1,153 +1,288 @@ package blob import ( + "io" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" ) -// JusticeKit is lé Blob of Justice. The JusticeKit contains information -// required to construct a justice transaction, that sweeps a remote party's -// revoked commitment transaction. It supports encryption and decryption using -// chacha20poly1305, allowing the client to encrypt the contents of the blob, -// 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 +// JusticeKit is an interface that describes lé Blob of Justice. An +// implementation of the JusticeKit contains information required to construct +// a justice transaction, that sweeps a remote party's revoked commitment +// transaction. It supports encryption and decryption using chacha20poly1305, +// allowing the client to encrypt the contents of the blob, and for a +// watchtower to later decrypt if action must be taken. +type JusticeKit interface { + // ToLocalOutputSpendInfo returns the info required to send the to-local + // output. It returns the output pub key script and the witness required + // to spend the output. + ToLocalOutputSpendInfo() (*txscript.PkScript, wire.TxWitness, error) - // 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 - // correlated across sessions and/or towers. - // - // NOTE: This is chosen to be the length of a maximally sized witness - // program. - SweepAddress []byte + // ToRemoteOutputSpendInfo returns the info required to send the + // to-remote output. It returns the output pub key script, the witness + // required to spend the output and the sequence to apply. + ToRemoteOutputSpendInfo() (*txscript.PkScript, wire.TxWitness, uint32, + error) - // RevocationPubKey is the compressed pubkey that guards the revocation - // clause of the remote party's to-local output. - RevocationPubKey PubKey + // HasCommitToRemoteOutput returns true if the kit does include the + // information required to sweep the to-remote output. + HasCommitToRemoteOutput() bool - // LocalDelayPubKey is the compressed pubkey in the to-local script of - // the remote party, which guards the path where the remote party - // claims their commitment output. - LocalDelayPubKey PubKey + // AddToLocalSig adds the to-local signature to the kit. + AddToLocalSig(sig lnwire.Sig) - // CSVDelay is the relative timelock in the remote party's to-local - // output, which the remote party must wait out before sweeping their - // commitment output. - CSVDelay uint32 + // AddToRemoteSig adds the to-remote signature to the kit. + AddToRemoteSig(sig lnwire.Sig) - // CommitToLocalSig is a signature under RevocationPubKey using - // SIGHASH_ALL. - CommitToLocalSig lnwire.Sig + // SweepAddress returns the sweep address to be used on the justice tx + // output. + SweepAddress() []byte - // CommitToRemotePubKey is the public key in the to-remote output of the - // revoked commitment transaction. - // - // NOTE: This value is only used if it contains a valid compressed - // public key. - CommitToRemotePubKey PubKey + // PlainTextSize is the size of the encoded-but-unencrypted blob in + // bytes. + PlainTextSize() int - // CommitToRemoteSig is a signature under CommitToRemotePubKey using - // SIGHASH_ALL. - // - // NOTE: This value is only used if CommitToRemotePubKey contains a - // valid compressed public key. - CommitToRemoteSig lnwire.Sig + encode(w io.Writer) error + decode(r io.Reader) error } -// CommitToLocalWitnessScript returns the serialized witness script for the -// commitment to-local output. -func (b *JusticeKit) CommitToLocalWitnessScript() ([]byte, error) { - revocationPubKey, err := btcec.ParsePubKey( - b.RevocationPubKey[:], - ) - if err != nil { - return nil, err - } - - localDelayedPubKey, err := btcec.ParsePubKey( - b.LocalDelayPubKey[:], - ) - if err != nil { - return nil, err - } - - return input.CommitScriptToSelf( - b.CSVDelay, localDelayedPubKey, revocationPubKey, - ) +// legacyJusticeKit is an implementation of the JusticeKit interface which can +// be used for backing up commitments of legacy (pre-anchor) channels. +type legacyJusticeKit struct { + justiceKitPacketV0 } -// CommitToLocalRevokeWitnessStack constructs a witness stack spending the -// revocation clause of the commitment to-local output. +// A compile-time check to ensure that legacyJusticeKit implements the +// JusticeKit interface. +var _ JusticeKit = (*legacyJusticeKit)(nil) + +// newLegacyJusticeKit constructs a new legacyJusticeKit. +func newLegacyJusticeKit(sweepScript []byte, + breachInfo *lnwallet.BreachRetribution, + withToRemote bool) *legacyJusticeKit { + + keyRing := breachInfo.KeyRing + + packet := justiceKitPacketV0{ + sweepAddress: sweepScript, + revocationPubKey: toBlobPubKey(keyRing.RevocationKey), + localDelayPubKey: toBlobPubKey(keyRing.ToLocalKey), + csvDelay: breachInfo.RemoteDelay, + commitToRemotePubKey: pubKey{}, + } + + if withToRemote { + packet.commitToRemotePubKey = toBlobPubKey( + keyRing.ToRemoteKey, + ) + } + + return &legacyJusticeKit{packet} +} + +// ToLocalOutputSpendInfo returns the info required to send the to-local output. +// It returns the output pub key script and the witness required to spend the +// output. // -// 1 -func (b *JusticeKit) CommitToLocalRevokeWitnessStack() ([][]byte, error) { - toLocalSig, err := b.CommitToLocalSig.ToSignature() +// NOTE: This is part of the JusticeKit interface. +func (l *legacyJusticeKit) ToLocalOutputSpendInfo() (*txscript.PkScript, + wire.TxWitness, error) { + + revocationPubKey, err := btcec.ParsePubKey(l.revocationPubKey[:]) if err != nil { - return nil, err + return nil, nil, err } - witnessStack := make([][]byte, 2) - witnessStack[0] = append(toLocalSig.Serialize(), - byte(txscript.SigHashAll)) - witnessStack[1] = []byte{1} + localDelayedPubKey, err := btcec.ParsePubKey(l.localDelayPubKey[:]) + if err != nil { + return nil, nil, err + } - return witnessStack, nil + script, err := input.CommitScriptToSelf( + l.csvDelay, localDelayedPubKey, revocationPubKey, + ) + if err != nil { + return nil, nil, err + } + + scriptPubKey, err := input.WitnessScriptHash(script) + if err != nil { + return nil, nil, err + } + + toLocalSig, err := l.commitToLocalSig.ToSignature() + if err != nil { + return nil, nil, err + } + + witness := make(wire.TxWitness, 3) + witness[0] = append(toLocalSig.Serialize(), byte(txscript.SigHashAll)) + witness[1] = []byte{1} + witness[2] = script + + pkScript, err := txscript.ParsePkScript(scriptPubKey) + if err != nil { + return nil, nil, err + } + + return &pkScript, witness, nil +} + +// ToRemoteOutputSpendInfo returns the info required to spend the to-remote +// output. It returns the output pub key script, the witness required to spend +// the output and the sequence to apply. +// +// NOTE: This is part of the JusticeKit interface. +func (l *legacyJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript, + wire.TxWitness, uint32, error) { + + if !btcec.IsCompressedPubKey(l.commitToRemotePubKey[:]) { + return nil, nil, 0, ErrNoCommitToRemoteOutput + } + + toRemoteScript := l.commitToRemotePubKey[:] + + // 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) + if err != nil { + return nil, nil, 0, err + } + + // Compute the witness script hash from the to-remote pubkey, which will + // be used to locate the output on the breach commitment transaction. + toRemoteScriptHash, err := input.CommitScriptUnencumbered( + toRemotePubKey, + ) + if err != nil { + return nil, nil, 0, err + } + + toRemoteSig, err := l.commitToRemoteSig.ToSignature() + if err != nil { + return nil, nil, 0, err + } + + witness := make(wire.TxWitness, 2) + witness[0] = append(toRemoteSig.Serialize(), byte(txscript.SigHashAll)) + witness[1] = toRemoteScript + + pkScript, err := txscript.ParsePkScript(toRemoteScriptHash) + if err != nil { + return nil, nil, 0, err + } + + return &pkScript, witness, 0, nil } // HasCommitToRemoteOutput returns true if the blob contains a to-remote p2wkh // pubkey. -func (b *JusticeKit) HasCommitToRemoteOutput() bool { - return btcec.IsCompressedPubKey(b.CommitToRemotePubKey[:]) -} - -// CommitToRemoteWitnessScript returns the witness script for the commitment -// 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[:]) - 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 consists of a single signature satisfying either the -// legacy or anchor witness scripts. // -// -func (b *JusticeKit) CommitToRemoteWitnessStack() ([][]byte, error) { - toRemoteSig, err := b.CommitToRemoteSig.ToSignature() - if err != nil { - return nil, err +// NOTE: This is part of the JusticeKit interface. +func (l *legacyJusticeKit) HasCommitToRemoteOutput() bool { + return btcec.IsCompressedPubKey(l.commitToRemotePubKey[:]) +} + +// SweepAddress returns the sweep address to be used on the justice tx +// output. +// +// NOTE: This is part of the JusticeKit interface. +func (l *legacyJusticeKit) SweepAddress() []byte { + return l.sweepAddress +} + +// AddToLocalSig adds the to-local signature to the kit. +// +// NOTE: This is part of the JusticeKit interface. +func (l *legacyJusticeKit) AddToLocalSig(sig lnwire.Sig) { + l.commitToLocalSig = sig +} + +// AddToRemoteSig adds the to-remote signature to the kit. +// +// NOTE: This is part of the JusticeKit interface. +func (l *legacyJusticeKit) AddToRemoteSig(sig lnwire.Sig) { + l.commitToRemoteSig = sig +} + +// PlainTextSize is the size of the encoded-but-unencrypted blob in +// bytes. +// +// NOTE: This is part of the JusticeKit interface. +func (l *legacyJusticeKit) PlainTextSize() int { + return V0PlaintextSize +} + +// anchorJusticeKit is an implementation of the JusticeKit interface which can +// be used for backing up commitments of anchor channels. It inherits most of +// the methods from the legacyJusticeKit and overrides the +// ToRemoteOutputSpendInfo method since the to-remote output of an anchor +// output is a P2WSH instead of the P2WPKH used by the legacy channels. +type anchorJusticeKit struct { + legacyJusticeKit +} + +// A compile-time check to ensure that legacyJusticeKit implements the +// JusticeKit interface. +var _ JusticeKit = (*anchorJusticeKit)(nil) + +// newAnchorJusticeKit constructs a new anchorJusticeKit. +func newAnchorJusticeKit(sweepScript []byte, + breachInfo *lnwallet.BreachRetribution, + withToRemote bool) *anchorJusticeKit { + + legacyKit := newLegacyJusticeKit(sweepScript, breachInfo, withToRemote) + + return &anchorJusticeKit{ + legacyJusticeKit: *legacyKit, + } +} + +// ToRemoteOutputSpendInfo returns the info required to send the to-remote +// output. It returns the output pub key script, the witness required to spend +// the output and the sequence to apply. +// +// NOTE: This is part of the JusticeKit interface. +func (a *anchorJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript, + wire.TxWitness, uint32, error) { + + if !btcec.IsCompressedPubKey(a.commitToRemotePubKey[:]) { + return nil, nil, 0, ErrNoCommitToRemoteOutput } - witnessStack := make([][]byte, 1) - witnessStack[0] = append(toRemoteSig.Serialize(), - byte(txscript.SigHashAll)) + pk, err := btcec.ParsePubKey(a.commitToRemotePubKey[:]) + if err != nil { + return nil, nil, 0, err + } - return witnessStack, nil + toRemoteScript, err := input.CommitScriptToRemoteConfirmed(pk) + if err != nil { + return nil, nil, 0, err + } + + toRemoteScriptHash, err := input.WitnessScriptHash(toRemoteScript) + if err != nil { + return nil, nil, 0, err + } + + toRemoteSig, err := a.commitToRemoteSig.ToSignature() + if err != nil { + return nil, nil, 0, err + } + + witness := make([][]byte, 2) + witness[0] = append(toRemoteSig.Serialize(), byte(txscript.SigHashAll)) + witness[1] = toRemoteScript + + pkScript, err := txscript.ParsePkScript(toRemoteScriptHash) + if err != nil { + return nil, nil, 0, err + } + + return &pkScript, witness, 1, nil } diff --git a/watchtower/blob/justice_kit_packet.go b/watchtower/blob/justice_kit_packet.go index cf1cb977d..8db3e9c73 100644 --- a/watchtower/blob/justice_kit_packet.go +++ b/watchtower/blob/justice_kit_packet.go @@ -74,22 +74,68 @@ var ( // nonce: 24 bytes // enciphered plaintext: n bytes // MAC: 16 bytes -func Size(blobType Type) int { - return NonceSize + PlaintextSize(blobType) + CiphertextExpansion +func Size(kit JusticeKit) int { + return NonceSize + kit.PlainTextSize() + CiphertextExpansion } -// PlaintextSize returns the size of the encoded-but-unencrypted blob in bytes. -func PlaintextSize(blobType Type) int { - switch { - case blobType.Has(FlagCommitOutputs): - return V0PlaintextSize - default: - return 0 - } +// pubKey is a 33-byte, serialized compressed public key. +type pubKey [33]byte + +// toBlobPubKey serializes the given public key into a pubKey that can be set +// as a field on a JusticeKit. +func toBlobPubKey(pk *btcec.PublicKey) pubKey { + var blobPubKey pubKey + copy(blobPubKey[:], pk.SerializeCompressed()) + return blobPubKey } -// PubKey is a 33-byte, serialized compressed public key. -type PubKey [33]byte +// justiceKitPacketV0 is lé Blob of Justice. The JusticeKit contains information +// required to construct a justice transaction, that sweeps a remote party's +// revoked commitment transaction. It supports encryption and decryption using +// chacha20poly1305, allowing the client to encrypt the contents of the blob, +// and for a watchtower to later decrypt if action must be taken. +type justiceKitPacketV0 struct { + // 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 + // correlated across sessions and/or towers. + // + // NOTE: This is chosen to be the length of a maximally sized witness + // program. + sweepAddress []byte + + // revocationPubKey is the compressed pubkey that guards the revocation + // clause of the remote party's to-local output. + revocationPubKey pubKey + + // localDelayPubKey is the compressed pubkey in the to-local script of + // the remote party, which guards the path where the remote party + // claims their commitment output. + localDelayPubKey pubKey + + // csvDelay is the relative timelock in the remote party's to-local + // output, which the remote party must wait out before sweeping their + // commitment output. + csvDelay uint32 + + // commitToLocalSig is a signature under RevocationPubKey using + // SIGHASH_ALL. + commitToLocalSig lnwire.Sig + + // commitToRemotePubKey is the public key in the to-remote output of the + // revoked commitment transaction. + // + // NOTE: This value is only used if it contains a valid compressed + // public key. + commitToRemotePubKey pubKey + + // commitToRemoteSig is a signature under CommitToRemotePubKey using + // SIGHASH_ALL. + // + // NOTE: This value is only used if CommitToRemotePubKey contains a + // valid compressed public key. + commitToRemoteSig lnwire.Sig +} // Encrypt encodes the blob of justice using encoding version, and then // creates a ciphertext using chacha20poly1305 under the chosen (nonce, key) @@ -97,11 +143,11 @@ type PubKey [33]byte // // 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) ([]byte, error) { +func Encrypt(kit JusticeKit, key BreachKey) ([]byte, error) { // Encode the plaintext using the provided version, to obtain the // plaintext bytes. var ptxtBuf bytes.Buffer - err := b.encode(&ptxtBuf, b.BlobType) + err := kit.encode(&ptxtBuf) if err != nil { return nil, err } @@ -115,7 +161,7 @@ func (b *JusticeKit) Encrypt(key BreachKey) ([]byte, error) { // Allocate the ciphertext, which will contain the nonce, encrypted // plaintext and MAC. plaintext := ptxtBuf.Bytes() - ciphertext := make([]byte, Size(b.BlobType)) + ciphertext := make([]byte, Size(kit)) // Generate a random 24-byte nonce in the ciphertext's prefix. nonce := ciphertext[:NonceSize] @@ -134,7 +180,7 @@ func (b *JusticeKit) Encrypt(key BreachKey) ([]byte, error) { // chacha20poly1305 with the chosen (nonce, key) pair. The internal plaintext is // then deserialized using the given encoding version. func Decrypt(key BreachKey, ciphertext []byte, - blobType Type) (*JusticeKit, error) { + blobType Type) (JusticeKit, error) { // Fail if the blob's overall length is less than required for the nonce // and expansion factor. @@ -161,42 +207,27 @@ func Decrypt(key BreachKey, ciphertext []byte, return nil, err } - // If decryption succeeded, we will then decode the plaintext bytes - // using the specified blob version. - boj := &JusticeKit{ - BlobType: blobType, - } - err = boj.decode(bytes.NewReader(plaintext), blobType) + commitment, err := blobType.CommitmentType(nil) if err != nil { return nil, err } - return boj, nil -} - -// encode serializes the JusticeKit according to the version, returning an -// error if the version is unknown. -func (b *JusticeKit) encode(w io.Writer, blobType Type) error { - switch { - case blobType.Has(FlagCommitOutputs): - return b.encodeV0(w) - default: - return ErrUnknownBlobType + kit, err := commitment.EmptyJusticeKit() + if err != nil { + return nil, err } -} -// decode deserializes the JusticeKit according to the version, returning an -// error if the version is unknown. -func (b *JusticeKit) decode(r io.Reader, blobType Type) error { - switch { - case blobType.Has(FlagCommitOutputs): - return b.decodeV0(r) - default: - return ErrUnknownBlobType + // If decryption succeeded, we will then decode the plaintext bytes + // using the specified blob version. + err = kit.decode(bytes.NewReader(plaintext)) + if err != nil { + return nil, err } + + return kit, nil } -// encodeV0 encodes the JusticeKit using the version 0 encoding scheme to the +// encode encodes the JusticeKit using the version 0 encoding scheme to the // provided io.Writer. The encoding supports sweeping of the commit to-local // output, and optionally the commit to-remote output. The encoding produces a // constant-size plaintext size of 274 bytes. @@ -211,21 +242,21 @@ func (b *JusticeKit) decode(r io.Reader, blobType Type) error { // commit to-local revocation sig: 64 bytes // commit to-remote pubkey: 33 bytes, maybe blank // commit to-remote sig: 64 bytes, maybe blank -func (b *JusticeKit) encodeV0(w io.Writer) error { +func (b *justiceKitPacketV0) encode(w io.Writer) error { // Assert the sweep address length is sane. - if len(b.SweepAddress) > MaxSweepAddrSize { + if len(b.sweepAddress) > MaxSweepAddrSize { return ErrSweepAddressToLong } // Write the actual length of the sweep address as a single byte. - err := binary.Write(w, byteOrder, uint8(len(b.SweepAddress))) + err := binary.Write(w, byteOrder, uint8(len(b.sweepAddress))) if err != nil { return err } // Pad the sweep address to our maximum length of 42 bytes. var sweepAddressBuf [MaxSweepAddrSize]byte - copy(sweepAddressBuf[:], b.SweepAddress) + copy(sweepAddressBuf[:], b.sweepAddress) // Write padded 42-byte sweep address. _, err = w.Write(sweepAddressBuf[:]) @@ -234,42 +265,42 @@ func (b *JusticeKit) encodeV0(w io.Writer) error { } // Write 33-byte revocation public key. - _, err = w.Write(b.RevocationPubKey[:]) + _, err = w.Write(b.revocationPubKey[:]) if err != nil { return err } // Write 33-byte local delay public key. - _, err = w.Write(b.LocalDelayPubKey[:]) + _, err = w.Write(b.localDelayPubKey[:]) if err != nil { return err } // Write 4-byte CSV delay. - err = binary.Write(w, byteOrder, b.CSVDelay) + err = binary.Write(w, byteOrder, b.csvDelay) if err != nil { return err } // Write 64-byte revocation signature for commit to-local output. - _, err = w.Write(b.CommitToLocalSig.RawBytes()) + _, err = w.Write(b.commitToLocalSig.RawBytes()) if err != nil { return err } // Write 33-byte commit to-remote public key, which may be blank. - _, err = w.Write(b.CommitToRemotePubKey[:]) + _, err = w.Write(b.commitToRemotePubKey[:]) if err != nil { return err } // Write 64-byte commit to-remote signature, which may be blank. - _, err = w.Write(b.CommitToRemoteSig.RawBytes()) + _, err = w.Write(b.commitToRemoteSig.RawBytes()) return err } -// decodeV0 reconstructs a JusticeKit from the io.Reader, using version 0 +// decode reconstructs a JusticeKit from the io.Reader, using version 0 // encoding scheme. This will parse a constant size input stream of 274 bytes to // recover information for the commit to-local output, and possibly the commit // to-remote output. @@ -284,7 +315,7 @@ func (b *JusticeKit) encodeV0(w io.Writer) error { // commit to-local revocation sig: 64 bytes // commit to-remote pubkey: 33 bytes, maybe blank // commit to-remote sig: 64 bytes, maybe blank -func (b *JusticeKit) decodeV0(r io.Reader) error { +func (b *justiceKitPacketV0) decode(r io.Reader) error { // Read the sweep address length as a single byte. var sweepAddrLen uint8 err := binary.Read(r, byteOrder, &sweepAddrLen) @@ -305,23 +336,23 @@ func (b *JusticeKit) decodeV0(r io.Reader) error { } // Parse sweep address from padded buffer. - b.SweepAddress = make([]byte, sweepAddrLen) - copy(b.SweepAddress, sweepAddressBuf[:]) + b.sweepAddress = make([]byte, sweepAddrLen) + copy(b.sweepAddress, sweepAddressBuf[:]) // Read 33-byte revocation public key. - _, err = io.ReadFull(r, b.RevocationPubKey[:]) + _, err = io.ReadFull(r, b.revocationPubKey[:]) if err != nil { return err } // Read 33-byte local delay public key. - _, err = io.ReadFull(r, b.LocalDelayPubKey[:]) + _, err = io.ReadFull(r, b.localDelayPubKey[:]) if err != nil { return err } // Read 4-byte CSV delay. - err = binary.Read(r, byteOrder, &b.CSVDelay) + err = binary.Read(r, byteOrder, &b.csvDelay) if err != nil { return err } @@ -333,13 +364,13 @@ func (b *JusticeKit) decodeV0(r io.Reader) error { return err } - b.CommitToLocalSig, err = lnwire.NewSigFromWireECDSA(localSig[:]) + b.commitToLocalSig, err = lnwire.NewSigFromWireECDSA(localSig[:]) if err != nil { return err } var ( - commitToRemotePubkey PubKey + commitToRemotePubkey pubKey commitToRemoteSig [64]byte ) @@ -358,8 +389,8 @@ func (b *JusticeKit) decodeV0(r io.Reader) error { // Only populate the commit to-remote fields in the decoded blob if a // valid compressed public key was read from the reader. if btcec.IsCompressedPubKey(commitToRemotePubkey[:]) { - b.CommitToRemotePubKey = commitToRemotePubkey - b.CommitToRemoteSig, err = lnwire.NewSigFromWireECDSA( + b.commitToRemotePubKey = commitToRemotePubkey + b.commitToRemoteSig, err = lnwire.NewSigFromWireECDSA( commitToRemoteSig[:], ) if err != nil { diff --git a/watchtower/blob/justice_kit_test.go b/watchtower/blob/justice_kit_test.go index 7db8d9a36..4851bbf7e 100644 --- a/watchtower/blob/justice_kit_test.go +++ b/watchtower/blob/justice_kit_test.go @@ -1,30 +1,25 @@ -package blob_test +package blob import ( "bytes" "crypto/rand" "encoding/binary" "io" - "reflect" "testing" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/watchtower/blob" "github.com/stretchr/testify/require" ) -func makePubKey(i uint64) blob.PubKey { - var pk blob.PubKey - pk[0] = 0x02 - if i%2 == 1 { - pk[0] |= 0x01 - } - binary.BigEndian.PutUint64(pk[1:9], i) - return pk +func makePubKey() *btcec.PublicKey { + priv, _ := btcec.NewPrivateKey() + return priv.PubKey() } func makeSig(i int) lnwire.Sig { @@ -46,15 +41,15 @@ func makeAddr(size int) []byte { type descriptorTest struct { name string - encVersion blob.Type - decVersion blob.Type + encVersion Type + decVersion Type sweepAddr []byte - revPubKey blob.PubKey - delayPubKey blob.PubKey + revPubKey *btcec.PublicKey + delayPubKey *btcec.PublicKey csvDelay uint32 commitToLocalSig lnwire.Sig hasCommitToRemote bool - commitToRemotePubKey blob.PubKey + commitToRemotePubKey *btcec.PublicKey commitToRemoteSig lnwire.Sig encErr error decErr error @@ -63,79 +58,79 @@ type descriptorTest struct { var descriptorTests = []descriptorTest{ { name: "to-local only", - encVersion: blob.TypeAltruistCommit, - decVersion: blob.TypeAltruistCommit, + encVersion: TypeAltruistCommit, + decVersion: TypeAltruistCommit, sweepAddr: makeAddr(22), - revPubKey: makePubKey(0), - delayPubKey: makePubKey(1), + revPubKey: makePubKey(), + delayPubKey: makePubKey(), csvDelay: 144, commitToLocalSig: makeSig(1), }, { name: "to-local and p2wkh", - encVersion: blob.TypeRewardCommit, - decVersion: blob.TypeRewardCommit, + encVersion: TypeRewardCommit, + decVersion: TypeRewardCommit, sweepAddr: makeAddr(22), - revPubKey: makePubKey(0), - delayPubKey: makePubKey(1), + revPubKey: makePubKey(), + delayPubKey: makePubKey(), csvDelay: 144, commitToLocalSig: makeSig(1), hasCommitToRemote: true, - commitToRemotePubKey: makePubKey(2), + commitToRemotePubKey: makePubKey(), commitToRemoteSig: makeSig(2), }, { name: "unknown encrypt version", encVersion: 0, - decVersion: blob.TypeAltruistCommit, + decVersion: TypeAltruistCommit, sweepAddr: makeAddr(34), - revPubKey: makePubKey(0), - delayPubKey: makePubKey(1), + revPubKey: makePubKey(), + delayPubKey: makePubKey(), csvDelay: 144, commitToLocalSig: makeSig(1), - encErr: blob.ErrUnknownBlobType, + encErr: ErrUnknownBlobType, }, { name: "unknown decrypt version", - encVersion: blob.TypeAltruistCommit, + encVersion: TypeAltruistCommit, decVersion: 0, sweepAddr: makeAddr(34), - revPubKey: makePubKey(0), - delayPubKey: makePubKey(1), + revPubKey: makePubKey(), + delayPubKey: makePubKey(), csvDelay: 144, commitToLocalSig: makeSig(1), - decErr: blob.ErrUnknownBlobType, + decErr: ErrUnknownBlobType, }, { name: "sweep addr length zero", - encVersion: blob.TypeAltruistCommit, - decVersion: blob.TypeAltruistCommit, + encVersion: TypeAltruistCommit, + decVersion: TypeAltruistCommit, sweepAddr: makeAddr(0), - revPubKey: makePubKey(0), - delayPubKey: makePubKey(1), + revPubKey: makePubKey(), + delayPubKey: makePubKey(), csvDelay: 144, commitToLocalSig: makeSig(1), }, { name: "sweep addr max size", - encVersion: blob.TypeAltruistCommit, - decVersion: blob.TypeAltruistCommit, - sweepAddr: makeAddr(blob.MaxSweepAddrSize), - revPubKey: makePubKey(0), - delayPubKey: makePubKey(1), + encVersion: TypeAltruistCommit, + decVersion: TypeAltruistCommit, + sweepAddr: makeAddr(MaxSweepAddrSize), + revPubKey: makePubKey(), + delayPubKey: makePubKey(), csvDelay: 144, commitToLocalSig: makeSig(1), }, { name: "sweep addr too long", - encVersion: blob.TypeAltruistCommit, - decVersion: blob.TypeAltruistCommit, - sweepAddr: makeAddr(blob.MaxSweepAddrSize + 1), - revPubKey: makePubKey(0), - delayPubKey: makePubKey(1), + encVersion: TypeAltruistCommit, + decVersion: TypeAltruistCommit, + sweepAddr: makeAddr(MaxSweepAddrSize + 1), + revPubKey: makePubKey(), + delayPubKey: makePubKey(), csvDelay: 144, commitToLocalSig: makeSig(1), - encErr: blob.ErrSweepAddressToLong, + encErr: ErrSweepAddressToLong, }, } @@ -152,30 +147,43 @@ 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, - CSVDelay: test.csvDelay, - CommitToLocalSig: test.commitToLocalSig, - CommitToRemotePubKey: test.commitToRemotePubKey, - CommitToRemoteSig: test.commitToRemoteSig, + commitmentType, err := test.encVersion.CommitmentType(nil) + if err != nil { + require.ErrorIs(t, err, test.encErr) + return } + breachInfo := &lnwallet.BreachRetribution{ + RemoteDelay: test.csvDelay, + KeyRing: &lnwallet.CommitmentKeyRing{ + ToLocalKey: test.delayPubKey, + ToRemoteKey: test.commitToRemotePubKey, + RevocationKey: test.revPubKey, + }, + } + + kit, err := commitmentType.NewJusticeKit( + test.sweepAddr, breachInfo, test.hasCommitToRemote, + ) + if err != nil { + return + } + kit.AddToLocalSig(test.commitToLocalSig) + kit.AddToRemoteSig(test.commitToRemoteSig) + // Generate a random encryption key for the blob. The key is // sized at 32 byte, as in practice we will be using the remote // party's commitment txid as the key. - var key blob.BreachKey - _, err := rand.Read(key[:]) + var key BreachKey + _, err = rand.Read(key[:]) require.NoError(t, err, "unable to generate blob encryption key") // Encrypt the blob plaintext using the generated key and // target version for this test. - ctxt, err := boj.Encrypt(key) - if err != test.encErr { - t.Fatalf("unable to encrypt blob: %v", err) - } else if test.encErr != nil { + ctxt, err := Encrypt(kit, key) + require.ErrorIs(t, err, test.encErr) + + if test.encErr != nil { // If the test expected an encryption failure, we can // continue to the next test. return @@ -183,19 +191,15 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) { // Ensure that all encrypted blobs are padded out to the same // size: 282 bytes for version 0. - if len(ctxt) != blob.Size(test.encVersion) { - t.Fatalf("expected blob to have size %d, got %d instead", - blob.Size(test.encVersion), len(ctxt)) - - } + require.Len(t, ctxt, Size(kit)) // Decrypt the encrypted blob, reconstructing the original // blob plaintext from the decrypted contents. We use the target // decryption version specified by this test case. - boj2, err := blob.Decrypt(key, ctxt, test.decVersion) - if err != test.decErr { - t.Fatalf("unable to decrypt blob: %v", err) - } else if test.decErr != nil { + boj2, err := Decrypt(key, ctxt, test.decVersion) + require.ErrorIs(t, err, test.decErr) + + if test.decErr != nil { // If the test expected an decryption failure, we can // continue to the next test. return @@ -210,15 +214,12 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) { // Check that the original blob plaintext matches the // one reconstructed from the encrypted blob. - if !reflect.DeepEqual(boj, boj2) { - t.Fatalf("decrypted plaintext does not match original, "+ - "want: %v, got %v", boj, boj2) - } + require.Equal(t, kit, boj2) } type remoteWitnessTest struct { name string - blobType blob.Type + blobType Type expWitnessScript func(pk *btcec.PublicKey) []byte } @@ -229,15 +230,14 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { tests := []remoteWitnessTest{ { name: "legacy commitment", - blobType: blob.Type(blob.FlagCommitOutputs), + blobType: TypeAltruistCommit, expWitnessScript: func(pk *btcec.PublicKey) []byte { return pk.SerializeCompressed() }, }, { - name: "anchor commitment", - blobType: blob.Type(blob.FlagCommitOutputs | - blob.FlagAnchorChannel), + name: "anchor commitment", + blobType: TypeAltruistAnchorCommit, expWitnessScript: func(pk *btcec.PublicKey) []byte { script, _ := input.CommitScriptToRemoteConfirmed(pk) return script @@ -257,12 +257,13 @@ func testJusticeKitRemoteWitnessConstruction( // Generate the to-remote pubkey. toRemotePrivKey, err := btcec.NewPrivateKey() - require.Nil(t, err) + require.NoError(t, err) - // Copy the to-remote pubkey into the format expected by our justice - // kit. - var toRemotePubKey blob.PubKey - copy(toRemotePubKey[:], toRemotePrivKey.PubKey().SerializeCompressed()) + revKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + toLocalKey, err := btcec.NewPrivateKey() + require.NoError(t, err) // Sign a message using the to-remote private key. The exact message // doesn't matter as we won't be validating the signature's validity. @@ -273,26 +274,29 @@ func testJusticeKitRemoteWitnessConstruction( commitToRemoteSig, err := lnwire.NewSigFromSignature(rawToRemoteSig) 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, + commitType, err := test.blobType.CommitmentType(nil) + require.NoError(t, err) + + breachInfo := &lnwallet.BreachRetribution{ + KeyRing: &lnwallet.CommitmentKeyRing{ + ToRemoteKey: toRemotePrivKey.PubKey(), + RevocationKey: revKey.PubKey(), + ToLocalKey: toLocalKey.PubKey(), + }, } + justiceKit, err := commitType.NewJusticeKit(nil, breachInfo, true) + require.NoError(t, err) + justiceKit.AddToRemoteSig(commitToRemoteSig) + // Now, compute the to-remote witness script returned by the justice // kit. - toRemoteScript, err := justiceKit.CommitToRemoteWitnessScript() - require.Nil(t, err) + _, witness, _, err := justiceKit.ToRemoteOutputSpendInfo() + require.NoError(t, err) // Assert this is exactly the to-remote, compressed pubkey. expToRemoteScript := test.expWitnessScript(toRemotePrivKey.PubKey()) - require.Equal(t, expToRemoteScript, toRemoteScript) - - // Next, compute the to-remote witness stack, which should be a p2wkh - // witness stack consisting solely of a signature. - toRemoteWitnessStack, err := justiceKit.CommitToRemoteWitnessStack() - require.Nil(t, err) + require.Equal(t, expToRemoteScript, witness[1]) // Compute the expected first element, by appending a sighash all byte // to our raw DER-encoded signature. @@ -301,19 +305,10 @@ func testJusticeKitRemoteWitnessConstruction( ) // Assert that the expected witness stack is returned. - expWitnessStack := [][]byte{ + expWitnessStack := wire.TxWitness{ rawToRemoteSigWithSigHash, } - require.Equal(t, expWitnessStack, toRemoteWitnessStack) - - // Finally, set the CommitToRemotePubKey to be a blank value. - justiceKit.CommitToRemotePubKey = blob.PubKey{} - - // When trying to compute the witness script, this should now return - // ErrNoCommitToRemoteOutput since a valid pubkey could not be parsed - // from CommitToRemotePubKey. - _, err = justiceKit.CommitToRemoteWitnessScript() - require.Error(t, blob.ErrNoCommitToRemoteOutput, err) + require.Equal(t, expWitnessStack, witness[:1]) } // TestJusticeKitToLocalWitnessConstruction tests that a JusticeKit returns the @@ -324,18 +319,10 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) { // Generate the revocation and delay private keys. revPrivKey, err := btcec.NewPrivateKey() - require.Nil(t, err) + require.NoError(t, err) delayPrivKey, err := btcec.NewPrivateKey() - require.Nil(t, err) - - // Copy the revocation and delay pubkeys into the format expected by our - // justice kit. - var revPubKey blob.PubKey - copy(revPubKey[:], revPrivKey.PubKey().SerializeCompressed()) - - var delayPubKey blob.PubKey - copy(delayPubKey[:], delayPrivKey.PubKey().SerializeCompressed()) + require.NoError(t, err) // Sign a message using the revocation private key. The exact message // doesn't matter as we won't be validating the signature's validity. @@ -344,33 +331,36 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) { // Convert the DER-encoded signature into a fixed-size sig. commitToLocalSig, err := lnwire.NewSigFromSignature(rawRevSig) - require.Nil(t, err) + require.NoError(t, err) - // Populate the justice kit with fields relevant to the to-local output. - justiceKit := &blob.JusticeKit{ - CSVDelay: csvDelay, - RevocationPubKey: revPubKey, - LocalDelayPubKey: delayPubKey, - CommitToLocalSig: commitToLocalSig, + commitType, err := TypeAltruistCommit.CommitmentType(nil) + require.NoError(t, err) + + breachInfo := &lnwallet.BreachRetribution{ + RemoteDelay: csvDelay, + KeyRing: &lnwallet.CommitmentKeyRing{ + RevocationKey: revPrivKey.PubKey(), + ToLocalKey: delayPrivKey.PubKey(), + }, } + justiceKit, err := commitType.NewJusticeKit(nil, breachInfo, false) + require.NoError(t, err) + justiceKit.AddToLocalSig(commitToLocalSig) + // Compute the expected to-local script, which is a function of the CSV // delay, revocation pubkey and delay pubkey. expToLocalScript, err := input.CommitScriptToSelf( csvDelay, delayPrivKey.PubKey(), revPrivKey.PubKey(), ) - require.Nil(t, err) + require.NoError(t, err) // Compute the to-local script that is returned by the justice kit. - toLocalScript, err := justiceKit.CommitToLocalWitnessScript() - require.Nil(t, err) + _, witness, err := justiceKit.ToLocalOutputSpendInfo() + require.NoError(t, err) // Assert that the expected to-local script matches the actual script. - require.Equal(t, expToLocalScript, toLocalScript) - - // Next, compute the to-local witness stack returned by the justice kit. - toLocalWitnessStack, err := justiceKit.CommitToLocalRevokeWitnessStack() - require.Nil(t, err) + require.Equal(t, expToLocalScript, witness[2]) // Compute the expected signature in the bottom element of the stack, by // appending a sighash all flag to the raw DER signature. @@ -379,9 +369,9 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) { ) // Finally, validate against our expected witness stack. - expWitnessStack := [][]byte{ + expWitnessStack := wire.TxWitness{ rawRevSigWithSigHash, {1}, } - require.Equal(t, expWitnessStack, toLocalWitnessStack) + require.Equal(t, expWitnessStack, witness[:2]) } diff --git a/watchtower/lookout/justice_descriptor.go b/watchtower/lookout/justice_descriptor.go index 2ed63500d..22db70ab5 100644 --- a/watchtower/lookout/justice_descriptor.go +++ b/watchtower/lookout/justice_descriptor.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/txsort" "github.com/btcsuite/btcd/txscript" @@ -41,7 +40,7 @@ type JusticeDescriptor struct { // JusticeKit contains the decrypted blob and information required to // construct the transaction scripts and witnesses. - JusticeKit *blob.JusticeKit + JusticeKit blob.JusticeKit } // breachedInput contains the required information to construct and spend @@ -56,22 +55,17 @@ type breachedInput struct { // commitToLocalInput extracts the information required to spend the commit // to-local output. func (p *JusticeDescriptor) commitToLocalInput() (*breachedInput, error) { - // Retrieve the to-local witness script from the justice kit. - toLocalScript, err := p.JusticeKit.CommitToLocalWitnessScript() - if err != nil { - return nil, err - } + kit := p.JusticeKit - // Compute the witness script hash, which will be used to locate the - // input on the breaching commitment transaction. - toLocalWitnessHash, err := input.WitnessScriptHash(toLocalScript) + // Retrieve the to-local output script and witness from the justice kit. + toLocalPkScript, witness, err := kit.ToLocalOutputSpendInfo() if err != nil { return nil, err } // Locate the to-local output on the breaching commitment transaction. toLocalIndex, toLocalTxOut, err := findTxOutByPkScript( - p.BreachedCommitTx, toLocalWitnessHash, + p.BreachedCommitTx, toLocalPkScript, ) if err != nil { return nil, err @@ -84,63 +78,28 @@ func (p *JusticeDescriptor) commitToLocalInput() (*breachedInput, error) { Index: toLocalIndex, } - // Retrieve to-local witness stack, which primarily includes a signature - // under the revocation pubkey. - witnessStack, err := p.JusticeKit.CommitToLocalRevokeWitnessStack() - if err != nil { - return nil, err - } - return &breachedInput{ txOut: toLocalTxOut, outPoint: toLocalOutPoint, - witness: buildWitness(witnessStack, toLocalScript), + witness: witness, }, nil } // commitToRemoteInput extracts the information required to spend the commit // to-remote output. func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) { - // Retrieve the to-remote witness script from the justice kit. - toRemoteScript, err := p.JusticeKit.CommitToRemoteWitnessScript() + kit := p.JusticeKit + + // Retrieve the to-remote output script, witness script and sequence + // from the justice kit. + toRemotePkScript, witness, seq, err := kit.ToRemoteOutputSpendInfo() if err != nil { return nil, err } - var ( - toRemoteScriptHash []byte - toRemoteSequence uint32 - ) - if p.JusticeKit.BlobType.IsAnchorChannel() { - toRemoteScriptHash, err = input.WitnessScriptHash( - toRemoteScript, - ) - if err != nil { - return nil, err - } - - toRemoteSequence = 1 - } else { - // Since the to-remote witness script should just be a regular p2wkh - // output, we'll parse it to retrieve the public key. - toRemotePubKey, err := btcec.ParsePubKey(toRemoteScript) - 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. toRemoteIndex, toRemoteTxOut, err := findTxOutByPkScript( - p.BreachedCommitTx, toRemoteScriptHash, + p.BreachedCommitTx, toRemotePkScript, ) if err != nil { return nil, err @@ -153,18 +112,11 @@ func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) { Index: toRemoteIndex, } - // Retrieve the to-remote witness stack, which is just a signature under - // the to-remote pubkey. - witnessStack, err := p.JusticeKit.CommitToRemoteWitnessStack() - if err != nil { - return nil, err - } - return &breachedInput{ txOut: toRemoteTxOut, outPoint: toRemoteOutPoint, - witness: buildWitness(witnessStack, toRemoteScript), - sequence: toRemoteSequence, + witness: witness, + sequence: seq, }, nil } @@ -193,7 +145,7 @@ func (p *JusticeDescriptor) assembleJusticeTxn(txWeight int64, // reward sweep, there will be two outputs, one of which pays back to // the victim while the other gives a cut to the tower. outputs, err := p.SessionInfo.Policy.ComputeJusticeTxOuts( - totalAmt, txWeight, p.JusticeKit.SweepAddress[:], + totalAmt, txWeight, p.JusticeKit.SweepAddress(), p.SessionInfo.RewardAddress, ) if err != nil { @@ -271,7 +223,7 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) { // Add the sweep address's contribution, depending on whether it is a // p2wkh or p2wsh output. - switch len(p.JusticeKit.SweepAddress) { + switch len(p.JusticeKit.SweepAddress()) { case input.P2WPKHSize: weightEstimate.AddP2WKHOutput() @@ -344,9 +296,9 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) { // // NOTE: The search stops after the first match is found. func findTxOutByPkScript(txn *wire.MsgTx, - pkScript []byte) (uint32, *wire.TxOut, error) { + pkScript *txscript.PkScript) (uint32, *wire.TxOut, error) { - found, index := input.FindScriptOutputIndex(txn, pkScript) + found, index := input.FindScriptOutputIndex(txn, pkScript.Script()) if !found { return 0, nil, ErrOutputNotFound } @@ -354,15 +306,6 @@ func findTxOutByPkScript(txn *wire.MsgTx, return index, txn.TxOut[index], nil } -// buildWitness appends the witness script to a given witness stack. -func buildWitness(witnessStack [][]byte, witnessScript []byte) [][]byte { - witness := make([][]byte, len(witnessStack)+1) - lastIdx := copy(witness, witnessStack) - witness[lastIdx] = witnessScript - - return witness -} - // prevOutFetcher returns a txscript.MultiPrevOutFetcher for the given set // of inputs. func prevOutFetcher(inputs []*breachedInput) (*txscript.MultiPrevOutFetcher, diff --git a/watchtower/lookout/justice_descriptor_test.go b/watchtower/lookout/justice_descriptor_test.go index 42f8af38a..42317648a 100644 --- a/watchtower/lookout/justice_descriptor_test.go +++ b/watchtower/lookout/justice_descriptor_test.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/lookout" @@ -221,16 +222,19 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { RewardAddress: makeAddrSlice(22), } - // 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, + breachInfo := &lnwallet.BreachRetribution{ + RemoteDelay: csvDelay, + KeyRing: &lnwallet.CommitmentKeyRing{ + ToLocalKey: toLocalPK, + ToRemoteKey: toRemotePK, + RevocationKey: revPK, + }, } - copy(justiceKit.RevocationPubKey[:], revPK.SerializeCompressed()) - copy(justiceKit.LocalDelayPubKey[:], toLocalPK.SerializeCompressed()) - copy(justiceKit.CommitToRemotePubKey[:], toRemotePK.SerializeCompressed()) + + justiceKit, err := commitType.NewJusticeKit( + makeAddrSlice(22), breachInfo, true, + ) + require.NoError(t, err) // Create a transaction spending from the outputs of the breach // transaction created earlier. The inputs are always ordered w/ @@ -256,7 +260,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { } outputs, err := policy.ComputeJusticeTxOuts( - totalAmount, int64(txWeight), justiceKit.SweepAddress, + totalAmount, int64(txWeight), justiceKit.SweepAddress(), sessionInfo.RewardAddress, ) require.NoError(t, err) @@ -316,8 +320,8 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { require.Nil(t, err) // Complete our justice kit by copying the signatures into the payload. - justiceKit.CommitToLocalSig = toLocalSig - justiceKit.CommitToRemoteSig = toRemoteSig + justiceKit.AddToLocalSig(toLocalSig) + justiceKit.AddToRemoteSig(toRemoteSig) justiceDesc := &lookout.JusticeDescriptor{ BreachedCommitTx: breachTxn, diff --git a/watchtower/lookout/lookout_test.go b/watchtower/lookout/lookout_test.go index c7305d9e0..3800a5682 100644 --- a/watchtower/lookout/lookout_test.go +++ b/watchtower/lookout/lookout_test.go @@ -8,8 +8,10 @@ import ( "testing" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/lookout" @@ -30,10 +32,9 @@ func (p *mockPunisher) Punish( return nil } -func makeArray32(i uint64) [32]byte { - var arr [32]byte - binary.BigEndian.PutUint64(arr[:], i) - return arr +func makeRandomPK() *btcec.PublicKey { + pk, _ := btcec.NewPrivateKey() + return pk.PubKey() } func makeArray33(i uint64) [33]byte { @@ -142,32 +143,49 @@ 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), - CSVDelay: 144, - CommitToLocalSig: makeTestSig(1), + breachInfo1 := &lnwallet.BreachRetribution{ + RemoteDelay: 144, + KeyRing: &lnwallet.CommitmentKeyRing{ + ToLocalKey: makeRandomPK(), + RevocationKey: makeRandomPK(), + }, } - blob2 := &blob.JusticeKit{ - BlobType: blobType, - SweepAddress: makeAddrSlice(22), - RevocationPubKey: makePubKey(2), - LocalDelayPubKey: makePubKey(2), - CSVDelay: 144, - CommitToLocalSig: makeTestSig(2), + commitment1, err := blobType.CommitmentType(nil) + require.NoError(t, err) + + blob1, err := commitment1.NewJusticeKit( + makeAddrSlice(22), breachInfo1, false, + ) + require.NoError(t, err) + + blob1.AddToLocalSig(makeTestSig(1)) + + breachInfo2 := &lnwallet.BreachRetribution{ + RemoteDelay: 144, + KeyRing: &lnwallet.CommitmentKeyRing{ + ToLocalKey: makeRandomPK(), + RevocationKey: makeRandomPK(), + }, } + commitment2, err := blobType.CommitmentType(nil) + require.NoError(t, err) + + blob2, err := commitment2.NewJusticeKit( + makeAddrSlice(22), breachInfo2, false, + ) + require.NoError(t, err) + + blob2.AddToLocalSig(makeTestSig(1)) key1 := blob.NewBreachKeyFromHash(&hash1) key2 := blob.NewBreachKeyFromHash(&hash2) // Encrypt the first justice kit under breach key one. - encBlob1, err := blob1.Encrypt(key1) + encBlob1, err := blob.Encrypt(blob1, key1) require.NoError(t, err, "unable to encrypt sweep detail 1") // Encrypt the second justice kit under breach key two. - encBlob2, err := blob2.Encrypt(key2) + encBlob2, err := blob.Encrypt(blob2, key2) require.NoError(t, err, "unable to encrypt sweep detail 2") // Add both state updates to the tower's database. diff --git a/watchtower/wtclient/backup_task.go b/watchtower/wtclient/backup_task.go index d811af4c3..a458afecb 100644 --- a/watchtower/wtclient/backup_task.go +++ b/watchtower/wtclient/backup_task.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/txsort" "github.com/btcsuite/btcd/chaincfg" @@ -247,25 +246,11 @@ func (t *backupTask) craftSessionPayload( var hint blob.BreachHint - // First, copy over the sweep pkscript, the pubkeys used to derive the - // 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), - CSVDelay: t.breachInfo.RemoteDelay, - } - - // If this commitment has an output that pays to us, copy the to-remote - // pubkey into the justice kit. This serves as the indicator to the - // tower that we expect the breaching transaction to have a non-dust - // output to spend from. - if t.toRemoteInput != nil { - justiceKit.CommitToRemotePubKey = toBlobPubKey( - keyRing.ToRemoteKey, - ) + justiceKit, err := t.commitmentType.NewJusticeKit( + t.sweepPkScript, t.breachInfo, t.toRemoteInput != nil, + ) + if err != nil { + return hint, nil, err } // Now, begin construction of the justice transaction. We'll start with @@ -348,9 +333,9 @@ func (t *backupTask) craftSessionPayload( // field switch inp.WitnessType() { case toLocalWitnessType: - justiceKit.CommitToLocalSig = signature + justiceKit.AddToLocalSig(signature) case toRemoteWitnessType: - justiceKit.CommitToRemoteSig = signature + justiceKit.AddToRemoteSig(signature) default: return hint, nil, fmt.Errorf("invalid witness type: %v", inp.WitnessType()) @@ -365,18 +350,10 @@ 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) + encBlob, err := blob.Encrypt(justiceKit, key) if err != nil { return hint, nil, err } return hint, encBlob, nil } - -// toBlobPubKey serializes the given pubkey into a blob.PubKey that can be set -// as a field on a blob.JusticeKit. -func toBlobPubKey(pubKey *btcec.PublicKey) blob.PubKey { - var blobPubKey blob.PubKey - copy(blobPubKey[:], pubKey.SerializeCompressed()) - return blobPubKey -} diff --git a/watchtower/wtclient/backup_task_internal_test.go b/watchtower/wtclient/backup_task_internal_test.go index bfce2811d..6604935ea 100644 --- a/watchtower/wtclient/backup_task_internal_test.go +++ b/watchtower/wtclient/backup_task_internal_test.go @@ -1,7 +1,7 @@ package wtclient import ( - "bytes" + "encoding/binary" "testing" "github.com/btcsuite/btcd/btcec/v2" @@ -25,8 +25,7 @@ import ( const csvDelay uint32 = 144 var ( - zeroPK [33]byte - zeroSig [64]byte + zeroSig = makeSig(0) revPrivBytes = []byte{ 0x8f, 0x4b, 0x51, 0x83, 0xa9, 0x34, 0xbd, 0x5f, @@ -65,6 +64,7 @@ type backupTaskTest struct { expSweepScript []byte signer input.Signer chanType channeldb.ChannelType + commitType blob.CommitmentType } // genTaskTest creates a instance of a backupTaskTest using the passed @@ -210,6 +210,7 @@ func genTaskTest( expSweepScript: sweepAddr, signer: signer, chanType: chanType, + commitType: commitType, } } @@ -567,60 +568,35 @@ func testBackupTask(t *testing.T, test backupTaskTest) { require.NoError(t, err, "unable to decrypt blob") keyRing := test.breachInfo.KeyRing - expToLocalPK := keyRing.ToLocalKey.SerializeCompressed() - expRevPK := keyRing.RevocationKey.SerializeCompressed() - expToRemotePK := keyRing.ToRemoteKey.SerializeCompressed() + expToLocalPK := keyRing.ToLocalKey + expRevPK := keyRing.RevocationKey + expToRemotePK := keyRing.ToRemoteKey - // Assert that the blob contained the serialized revocation and to-local - // pubkeys. - require.Equal(t, expRevPK, jKit.RevocationPubKey[:]) - require.Equal(t, expToLocalPK, jKit.LocalDelayPubKey[:]) - - // Determine if the breach transaction has a to-remote output and/or - // to-local output to spend from. Note the seemingly-reversed - // nomenclature. - hasToRemote := test.breachInfo.LocalOutputSignDesc != nil - hasToLocal := test.breachInfo.RemoteOutputSignDesc != nil - - // If the to-remote output is present, assert that the to-remote public - // key was included in the blob. Otherwise assert that a blank public - // key was inserted. - if hasToRemote { - require.Equal(t, expToRemotePK, jKit.CommitToRemotePubKey[:]) - } else { - require.Equal(t, zeroPK[:], jKit.CommitToRemotePubKey[:]) + breachInfo := &lnwallet.BreachRetribution{ + RemoteDelay: csvDelay, + KeyRing: &lnwallet.CommitmentKeyRing{ + ToLocalKey: expToLocalPK, + RevocationKey: expRevPK, + ToRemoteKey: expToRemotePK, + }, } - // Assert that the CSV is encoded in the blob. - require.Equal(t, test.breachInfo.RemoteDelay, jKit.CSVDelay) - - // Assert that the sweep pkscript is included. - require.Equal(t, test.expSweepScript, jKit.SweepAddress) - - // Finally, verify that the signatures are encoded in the justice kit. - // We don't validate the actual signatures produced here, since at the - // moment, it is tested indirectly by other packages and integration - // tests. - // TODO(conner): include signature validation checks - emptyToLocalSig := bytes.Equal( - jKit.CommitToLocalSig.RawBytes(), zeroSig[:], + expectedKit, err := test.commitType.NewJusticeKit( + test.expSweepScript, breachInfo, test.expToRemoteInput != nil, ) - if hasToLocal { - require.False(t, emptyToLocalSig, "to-local signature should "+ - "not be empty") - } else { - require.True(t, emptyToLocalSig, "to-local signature should "+ - "be empty") - } + require.NoError(t, err) - emptyToRemoteSig := bytes.Equal( - jKit.CommitToRemoteSig.RawBytes(), zeroSig[:], - ) - if hasToRemote { - require.False(t, emptyToRemoteSig, "to-remote signature "+ - "should not be empty") - } else { - require.True(t, emptyToRemoteSig, "to-remote signature "+ - "should be empty") - } + jKit.AddToLocalSig(zeroSig) + jKit.AddToRemoteSig(zeroSig) + + require.Equal(t, expectedKit, jKit) +} + +func makeSig(i int) lnwire.Sig { + var sigBytes [64]byte + binary.BigEndian.PutUint64(sigBytes[:8], uint64(i)) + + sig, _ := lnwire.NewSigFromWireECDSA(sigBytes[:]) + + return sig } diff --git a/watchtower/wtdb/client_db_test.go b/watchtower/wtdb/client_db_test.go index 36cc049a9..9348b845f 100644 --- a/watchtower/wtdb/client_db_test.go +++ b/watchtower/wtdb/client_db_test.go @@ -1201,7 +1201,10 @@ func randCommittedUpdateForChannel(t *testing.T, chanID lnwire.ChannelID, _, err := io.ReadFull(crand.Reader, hint[:]) require.NoError(t, err) - encBlob := make([]byte, blob.Size(blob.FlagCommitOutputs.Type())) + kit, err := blob.AnchorCommitment.EmptyJusticeKit() + require.NoError(t, err) + + encBlob := make([]byte, blob.Size(kit)) _, err = io.ReadFull(crand.Reader, encBlob) require.NoError(t, err) @@ -1229,7 +1232,10 @@ func randCommittedUpdateForChanWithHeight(t *testing.T, chanID lnwire.ChannelID, _, err := io.ReadFull(crand.Reader, hint[:]) require.NoError(t, err) - encBlob := make([]byte, blob.Size(blob.FlagCommitOutputs.Type())) + kit, err := blob.AnchorCommitment.EmptyJusticeKit() + require.NoError(t, err) + + encBlob := make([]byte, blob.Size(kit)) _, err = io.ReadFull(crand.Reader, encBlob) require.NoError(t, err) diff --git a/watchtower/wtdb/tower_db.go b/watchtower/wtdb/tower_db.go index 50d491411..fa43b5bdd 100644 --- a/watchtower/wtdb/tower_db.go +++ b/watchtower/wtdb/tower_db.go @@ -240,9 +240,19 @@ func (t *TowerDB) InsertStateUpdate(update *SessionStateUpdate) (uint16, error) return err } + commitType, err := session.Policy.BlobType.CommitmentType(nil) + if err != nil { + return err + } + + kit, err := commitType.EmptyJusticeKit() + if err != nil { + return err + } + // Assert that the blob is the correct size for the session's // blob type. - expBlobSize := blob.Size(session.Policy.BlobType) + expBlobSize := blob.Size(kit) if len(update.EncryptedBlob) != expBlobSize { return ErrInvalidBlobSize } diff --git a/watchtower/wtdb/tower_db_test.go b/watchtower/wtdb/tower_db_test.go index 9459f34d3..f829a793a 100644 --- a/watchtower/wtdb/tower_db_test.go +++ b/watchtower/wtdb/tower_db_test.go @@ -17,7 +17,10 @@ import ( ) var ( - testBlob = make([]byte, blob.Size(blob.TypeAltruistCommit)) + testBlob = make( + []byte, blob.NonceSize+blob.V0PlaintextSize+ + blob.CiphertextExpansion, + ) ) // dbInit is a closure used to initialize a watchtower.DB instance. @@ -737,7 +740,8 @@ func updateFromInt(id *wtdb.SessionID, i int, copy(hint[:4], id[:4]) binary.BigEndian.PutUint16(hint[4:6], uint16(i)) - blobSize := blob.Size(blob.TypeAltruistCommit) + kit, _ := blob.AnchorCommitment.EmptyJusticeKit() + blobSize := blob.Size(kit) return &wtdb.SessionStateUpdate{ ID: *id, diff --git a/watchtower/wtmock/tower_db.go b/watchtower/wtmock/tower_db.go index 35c01ab1b..60ed2128c 100644 --- a/watchtower/wtmock/tower_db.go +++ b/watchtower/wtmock/tower_db.go @@ -37,12 +37,22 @@ func (db *TowerDB) InsertStateUpdate(update *wtdb.SessionStateUpdate) (uint16, e return 0, wtdb.ErrSessionNotFound } + commitType, err := info.Policy.BlobType.CommitmentType(nil) + if err != nil { + return 0, err + } + + kit, err := commitType.EmptyJusticeKit() + if err != nil { + return 0, err + } + // Assert that the blob is the correct size for the session's blob type. - if len(update.EncryptedBlob) != blob.Size(info.Policy.BlobType) { + if len(update.EncryptedBlob) != blob.Size(kit) { return 0, wtdb.ErrInvalidBlobSize } - err := info.AcceptUpdateSequence(update.SeqNum, update.LastApplied) + err = info.AcceptUpdateSequence(update.SeqNum, update.LastApplied) if err != nil { return info.LastApplied, err } diff --git a/watchtower/wtserver/server_test.go b/watchtower/wtserver/server_test.go index ab83d4255..fa1dfa036 100644 --- a/watchtower/wtserver/server_test.go +++ b/watchtower/wtserver/server_test.go @@ -30,7 +30,10 @@ var ( testnetChainHash = *chaincfg.TestNet3Params.GenesisHash - testBlob = make([]byte, blob.Size(blob.TypeAltruistCommit)) + testBlob = make( + []byte, blob.NonceSize+blob.V0PlaintextSize+ + blob.CiphertextExpansion, + ) ) // randPubKey generates a new secp keypair, and returns the public key.