diff --git a/docs/release-notes/release-notes-0.18.0.md b/docs/release-notes/release-notes-0.18.0.md index b5327c11b..23ab71b38 100644 --- a/docs/release-notes/release-notes-0.18.0.md +++ b/docs/release-notes/release-notes-0.18.0.md @@ -155,6 +155,10 @@ * [Add a watchtower tower client multiplexer](https://github.com/lightningnetwork/lnd/pull/7702) to manage tower clients of different types. + +* [Introduce CommitmentType and JusticeKit + interface](https://github.com/lightningnetwork/lnd/pull/7736) to simplify the + code. ## Breaking Changes ## Performance Improvements diff --git a/watchtower/blob/commitments.go b/watchtower/blob/commitments.go new file mode 100644 index 000000000..0f476e5a3 --- /dev/null +++ b/watchtower/blob/commitments.go @@ -0,0 +1,213 @@ +package blob + +import ( + "fmt" + + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" +) + +// CommitmentType characterises the various properties of the breach commitment +// transaction. +type CommitmentType uint8 + +const ( + // LegacyCommitment represents a legacy commitment transaction where + // anchor outputs are not yet used and so the to_remote output is just + // a regular but tweaked P2WKH. + LegacyCommitment CommitmentType = iota + + // LegacyTweaklessCommitment is similar to the LegacyCommitment with the + // added detail of the to_remote output not being tweaked. + LegacyTweaklessCommitment + + // AnchorCommitment represents the commitment transaction of an + // anchor channel. The key differences are that the to_remote is + // encumbered by a 1 block CSV and so is thus a P2WSH output. + AnchorCommitment +) + +// ToLocalInput constructs the input that will be used to spend the to_local +// output. +func (c CommitmentType) ToLocalInput(info *lnwallet.BreachRetribution) ( + input.Input, error) { + + witnessType, err := c.ToLocalWitnessType() + if err != nil { + return nil, err + } + + return input.NewBaseInput( + &info.RemoteOutpoint, witnessType, info.RemoteOutputSignDesc, 0, + ), nil +} + +// ToRemoteInput constructs the input that will be used to spend the to_remote +// output. +func (c CommitmentType) ToRemoteInput(info *lnwallet.BreachRetribution) ( + input.Input, error) { + + witnessType, err := c.ToRemoteWitnessType() + if err != nil { + return nil, err + } + + switch c { + case LegacyCommitment, LegacyTweaklessCommitment: + return input.NewBaseInput( + &info.LocalOutpoint, witnessType, + info.LocalOutputSignDesc, 0, + ), nil + + case AnchorCommitment: + // Anchor channels have a CSV-encumbered to-remote output. We'll + // construct a CSV input and assign the proper CSV delay of 1. + return input.NewCsvInput( + &info.LocalOutpoint, witnessType, + info.LocalOutputSignDesc, 0, 1, + ), nil + + default: + return nil, fmt.Errorf("unknown commitment type: %v", c) + } +} + +// ToLocalWitnessType is the input type of the to_local output. +func (c CommitmentType) ToLocalWitnessType() (input.WitnessType, error) { + switch c { + case LegacyTweaklessCommitment, LegacyCommitment, AnchorCommitment: + return input.CommitmentRevoke, nil + + default: + return nil, fmt.Errorf("unknown commitment type: %v", c) + } +} + +// ToRemoteWitnessType is the input type of the to_remote output. +func (c CommitmentType) ToRemoteWitnessType() (input.WitnessType, error) { + switch c { + case LegacyTweaklessCommitment: + return input.CommitSpendNoDelayTweakless, nil + + case LegacyCommitment: + return input.CommitmentNoDelay, nil + + case AnchorCommitment: + return input.CommitmentToRemoteConfirmed, nil + + default: + return nil, fmt.Errorf("unknown commitment type: %v", c) + } +} + +// ToRemoteWitnessSize is the size of the witness that will be required to spend +// the to_remote output. +func (c CommitmentType) ToRemoteWitnessSize() (int, error) { + switch c { + // Legacy channels (both tweaked and non-tweaked) spend from P2WKH + // output. + case LegacyTweaklessCommitment, LegacyCommitment: + return input.P2WKHWitnessSize, nil + + // Anchor channels spend a to-remote confirmed P2WSH output. + case AnchorCommitment: + return input.ToRemoteConfirmedWitnessSize, nil + + default: + return 0, fmt.Errorf("unknown commitment type: %v", c) + } +} + +// ToLocalWitnessSize is the size of the witness that will be required to spend +// the to_local output. +func (c CommitmentType) ToLocalWitnessSize() (int, error) { + switch c { + // An older ToLocalPenaltyWitnessSize constant used to underestimate the + // size by one byte. The difference 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. + case LegacyTweaklessCommitment, LegacyCommitment: + return input.ToLocalPenaltyWitnessSize - 1, nil + + case AnchorCommitment: + return input.ToLocalPenaltyWitnessSize, nil + + default: + return 0, fmt.Errorf("unknown commitment type: %v", c) + } +} + +// ParseRawSig parses a wire.TxWitness and creates an lnwire.Sig. +func (c CommitmentType) ParseRawSig(witness wire.TxWitness) (lnwire.Sig, + error) { + + switch c { + case LegacyCommitment, LegacyTweaklessCommitment, AnchorCommitment: + // Check that the witness has at least one item. + if len(witness) < 1 { + return lnwire.Sig{}, fmt.Errorf("the witness should " + + "have at least one element") + } + + // Check that the first witness element is non-nil. This is to + // ensure that the witness length check below does not panic. + if witness[0] == nil { + return lnwire.Sig{}, fmt.Errorf("the first witness " + + "element should not be nil") + } + + // Parse the DER-encoded signature from the first position of + // the resulting witness. We trim an extra byte to remove the + // sighash flag. + rawSignature := witness[0][:len(witness[0])-1] + + // Re-encode the DER signature into a fixed-size 64 byte + // signature. + return lnwire.NewSigFromECDSARawSignature(rawSignature) + + default: + return lnwire.Sig{}, fmt.Errorf("unknown commitment type: %v", + 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 7b39eeb40..7741ff026 100644 --- a/watchtower/blob/justice_kit.go +++ b/watchtower/blob/justice_kit.go @@ -1,516 +1,288 @@ package blob import ( - "bytes" - "crypto/rand" - "encoding/binary" - "errors" - "fmt" "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" - "golang.org/x/crypto/chacha20poly1305" ) -const ( - // NonceSize is the length of a chacha20poly1305 nonce, 24 bytes. - NonceSize = chacha20poly1305.NonceSizeX +// 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) - // KeySize is the length of a chacha20poly1305 key, 32 bytes. - KeySize = chacha20poly1305.KeySize + // 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) - // CiphertextExpansion is the number of bytes padded to a plaintext - // encrypted with chacha20poly1305, which comes from a 16-byte MAC. - CiphertextExpansion = 16 + // HasCommitToRemoteOutput returns true if the kit does include the + // information required to sweep the to-remote output. + HasCommitToRemoteOutput() bool - // V0PlaintextSize is the plaintext size of a version 0 encoded blob. - // sweep address length: 1 byte - // padded sweep address: 42 bytes - // revocation pubkey: 33 bytes - // local delay pubkey: 33 bytes - // csv delay: 4 bytes - // commit to-local revocation sig: 64 bytes - // commit to-remote pubkey: 33 bytes, maybe blank - // commit to-remote sig: 64 bytes, maybe blank - V0PlaintextSize = 274 + // AddToLocalSig adds the to-local signature to the kit. + AddToLocalSig(sig lnwire.Sig) - // MaxSweepAddrSize defines the maximum sweep address size that can be - // encoded in a blob. - MaxSweepAddrSize = 42 -) + // AddToRemoteSig adds the to-remote signature to the kit. + AddToRemoteSig(sig lnwire.Sig) -// Size returns the size of the encoded-and-encrypted blob in bytes. + // SweepAddress returns the sweep address to be used on the justice tx + // output. + SweepAddress() []byte + + // PlainTextSize is the size of the encoded-but-unencrypted blob in + // bytes. + PlainTextSize() int + + encode(w io.Writer) error + decode(r io.Reader) error +} + +// 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 +} + +// 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. // -// nonce: 24 bytes -// enciphered plaintext: n bytes -// MAC: 16 bytes -func Size(blobType Type) int { - return NonceSize + PlaintextSize(blobType) + CiphertextExpansion -} +// NOTE: This is part of the JusticeKit interface. +func (l *legacyJusticeKit) ToLocalOutputSpendInfo() (*txscript.PkScript, + wire.TxWitness, error) { -// 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 + revocationPubKey, err := btcec.ParsePubKey(l.revocationPubKey[:]) + if err != nil { + return nil, nil, err } -} -var ( - // byteOrder specifies a big-endian encoding of all integer values. - byteOrder = binary.BigEndian + localDelayedPubKey, err := btcec.ParsePubKey(l.localDelayPubKey[:]) + if err != nil { + return nil, nil, err + } - // ErrUnknownBlobType signals that we don't understand the requested - // blob encoding scheme. - ErrUnknownBlobType = errors.New("unknown blob type") - - // ErrCiphertextTooSmall is a decryption error signaling that the - // ciphertext is smaller than the ciphertext expansion factor. - ErrCiphertextTooSmall = errors.New( - "ciphertext is too small for chacha20poly1305", - ) - - // ErrNoCommitToRemoteOutput is returned when trying to retrieve the - // commit to-remote output from the blob, though none exists. - ErrNoCommitToRemoteOutput = errors.New( - "cannot obtain commit to-remote p2wkh output script from blob", - ) - - // ErrSweepAddressToLong is returned when trying to encode or decode a - // sweep address with length greater than the maximum length of 42 - // bytes, which supports p2wkh and p2sh addresses. - ErrSweepAddressToLong = fmt.Errorf( - "sweep address must be less than or equal to %d bytes long", - MaxSweepAddrSize, - ) -) - -// PubKey is a 33-byte, serialized compressed public key. -type PubKey [33]byte - -// 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 - - // 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 -} - -// CommitToLocalWitnessScript returns the serialized witness script for the -// commitment to-local output. -func (b *JusticeKit) CommitToLocalWitnessScript() ([]byte, error) { - revocationPubKey, err := btcec.ParsePubKey( - b.RevocationPubKey[:], + script, err := input.CommitScriptToSelf( + l.csvDelay, localDelayedPubKey, revocationPubKey, ) if err != nil { - return nil, err + return nil, nil, err } - localDelayedPubKey, err := btcec.ParsePubKey( - b.LocalDelayPubKey[:], - ) + scriptPubKey, err := input.WitnessScriptHash(script) if err != nil { - return nil, err + return nil, nil, err } - return input.CommitScriptToSelf( - b.CSVDelay, localDelayedPubKey, revocationPubKey, - ) + 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 } -// CommitToLocalRevokeWitnessStack constructs a witness stack spending the -// revocation clause of the commitment to-local output. +// 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. // -// 1 -func (b *JusticeKit) CommitToLocalRevokeWitnessStack() ([][]byte, error) { - toLocalSig, err := b.CommitToLocalSig.ToSignature() - if err != nil { - return nil, err +// 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 } - witnessStack := make([][]byte, 2) - witnessStack[0] = append(toLocalSig.Serialize(), - byte(txscript.SigHashAll)) - witnessStack[1] = []byte{1} + toRemoteScript := l.commitToRemotePubKey[:] - return witnessStack, nil + // 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 - } - - witnessStack := make([][]byte, 1) - witnessStack[0] = append(toRemoteSig.Serialize(), - byte(txscript.SigHashAll)) - - return witnessStack, nil +// NOTE: This is part of the JusticeKit interface. +func (l *legacyJusticeKit) HasCommitToRemoteOutput() bool { + return btcec.IsCompressedPubKey(l.commitToRemotePubKey[:]) } -// Encrypt encodes the blob of justice using encoding version, and then -// creates a ciphertext using chacha20poly1305 under the chosen (nonce, key) -// pair. +// SweepAddress returns the sweep address to be used on the justice tx +// output. // -// 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) { - // Encode the plaintext using the provided version, to obtain the - // plaintext bytes. - var ptxtBuf bytes.Buffer - err := b.encode(&ptxtBuf, b.BlobType) - if err != nil { - return nil, err - } - - // Create a new chacha20poly1305 cipher, using a 32-byte key. - cipher, err := chacha20poly1305.NewX(key[:]) - if err != nil { - return nil, err - } - - // Allocate the ciphertext, which will contain the nonce, encrypted - // plaintext and MAC. - plaintext := ptxtBuf.Bytes() - ciphertext := make([]byte, Size(b.BlobType)) - - // Generate a random 24-byte nonce in the ciphertext's prefix. - nonce := ciphertext[:NonceSize] - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return nil, err - } - - // Finally, encrypt the plaintext using the given nonce, storing the - // result in the ciphertext buffer. - cipher.Seal(ciphertext[NonceSize:NonceSize], nonce, plaintext, nil) - - return ciphertext, nil +// NOTE: This is part of the JusticeKit interface. +func (l *legacyJusticeKit) SweepAddress() []byte { + return l.sweepAddress } -// Decrypt unenciphers a blob of justice by decrypting the ciphertext using -// 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) { - - // Fail if the blob's overall length is less than required for the nonce - // and expansion factor. - if len(ciphertext) < NonceSize+CiphertextExpansion { - return nil, ErrCiphertextTooSmall - } - - // Create a new chacha20poly1305 cipher, using a 32-byte key. - cipher, err := chacha20poly1305.NewX(key[:]) - if err != nil { - return nil, err - } - - // Allocate the final buffer that will contain the blob's plaintext - // bytes, which is computed by subtracting the ciphertext expansion - // factor from the blob's length. - plaintext := make([]byte, len(ciphertext)-CiphertextExpansion) - - // Decrypt the ciphertext, placing the resulting plaintext in our - // plaintext buffer. - nonce := ciphertext[:NonceSize] - _, err = cipher.Open(plaintext[:0], nonce, ciphertext[NonceSize:], nil) - if err != nil { - 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) - 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 - } -} - -// 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 - } -} - -// encodeV0 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. +// AddToLocalSig adds the to-local signature to the kit. // -// blob version 0 plaintext encoding: -// -// sweep address length: 1 byte -// padded sweep address: 42 bytes -// revocation pubkey: 33 bytes -// local delay pubkey: 33 bytes -// csv delay: 4 bytes -// 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 { - // Assert the sweep address length is sane. - 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))) - if err != nil { - return err - } - - // Pad the sweep address to our maximum length of 42 bytes. - var sweepAddressBuf [MaxSweepAddrSize]byte - copy(sweepAddressBuf[:], b.SweepAddress) - - // Write padded 42-byte sweep address. - _, err = w.Write(sweepAddressBuf[:]) - if err != nil { - return err - } - - // Write 33-byte revocation public key. - _, err = w.Write(b.RevocationPubKey[:]) - if err != nil { - return err - } - - // Write 33-byte local delay public key. - _, err = w.Write(b.LocalDelayPubKey[:]) - if err != nil { - return err - } - - // Write 4-byte CSV delay. - 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()) - if err != nil { - return err - } - - // Write 33-byte commit to-remote public key, which may be blank. - _, 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()) - return err +// NOTE: This is part of the JusticeKit interface. +func (l *legacyJusticeKit) AddToLocalSig(sig lnwire.Sig) { + l.commitToLocalSig = sig } -// decodeV0 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. +// AddToRemoteSig adds the to-remote signature to the kit. // -// blob version 0 plaintext encoding: -// -// sweep address length: 1 byte -// padded sweep address: 42 bytes -// revocation pubkey: 33 bytes -// local delay pubkey: 33 bytes -// csv delay: 4 bytes -// 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 { - // Read the sweep address length as a single byte. - var sweepAddrLen uint8 - err := binary.Read(r, byteOrder, &sweepAddrLen) - if err != nil { - return err - } - - // Assert the sweep address length is sane. - if sweepAddrLen > MaxSweepAddrSize { - return ErrSweepAddressToLong - } - - // Read padded 42-byte sweep address. - var sweepAddressBuf [MaxSweepAddrSize]byte - _, err = io.ReadFull(r, sweepAddressBuf[:]) - if err != nil { - return err - } - - // Parse sweep address from padded buffer. - b.SweepAddress = make([]byte, sweepAddrLen) - copy(b.SweepAddress, sweepAddressBuf[:]) - - // Read 33-byte revocation public key. - _, err = io.ReadFull(r, b.RevocationPubKey[:]) - if err != nil { - return err - } - - // Read 33-byte local delay public key. - _, err = io.ReadFull(r, b.LocalDelayPubKey[:]) - if err != nil { - return err - } - - // Read 4-byte CSV delay. - err = binary.Read(r, byteOrder, &b.CSVDelay) - if err != nil { - return err - } - - // Read 64-byte revocation signature for commit to-local output. - var localSig [64]byte - _, err = io.ReadFull(r, localSig[:]) - if err != nil { - return err - } - - b.CommitToLocalSig, err = lnwire.NewSigFromWireECDSA(localSig[:]) - if err != nil { - return err - } - - var ( - commitToRemotePubkey PubKey - commitToRemoteSig [64]byte - ) - - // Read 33-byte commit to-remote public key, which may be discarded. - _, err = io.ReadFull(r, commitToRemotePubkey[:]) - if err != nil { - return err - } - - // Read 64-byte commit to-remote signature, which may be discarded. - _, err = io.ReadFull(r, commitToRemoteSig[:]) - if err != nil { - return err - } - - // 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( - commitToRemoteSig[:], - ) - if err != nil { - return err - } - } - - return nil +// 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 + } + + pk, err := btcec.ParsePubKey(a.commitToRemotePubKey[:]) + if err != nil { + return nil, nil, 0, err + } + + 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 new file mode 100644 index 000000000..8db3e9c73 --- /dev/null +++ b/watchtower/blob/justice_kit_packet.go @@ -0,0 +1,402 @@ +package blob + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/lnwire" + "golang.org/x/crypto/chacha20poly1305" +) + +const ( + // NonceSize is the length of a chacha20poly1305 nonce, 24 bytes. + NonceSize = chacha20poly1305.NonceSizeX + + // KeySize is the length of a chacha20poly1305 key, 32 bytes. + KeySize = chacha20poly1305.KeySize + + // CiphertextExpansion is the number of bytes padded to a plaintext + // encrypted with chacha20poly1305, which comes from a 16-byte MAC. + CiphertextExpansion = 16 + + // V0PlaintextSize is the plaintext size of a version 0 encoded blob. + // sweep address length: 1 byte + // padded sweep address: 42 bytes + // revocation pubkey: 33 bytes + // local delay pubkey: 33 bytes + // csv delay: 4 bytes + // commit to-local revocation sig: 64 bytes + // commit to-remote pubkey: 33 bytes, maybe blank + // commit to-remote sig: 64 bytes, maybe blank + V0PlaintextSize = 274 + + // MaxSweepAddrSize defines the maximum sweep address size that can be + // encoded in a blob. + MaxSweepAddrSize = 42 +) + +var ( + // byteOrder specifies a big-endian encoding of all integer values. + byteOrder = binary.BigEndian + + // ErrUnknownBlobType signals that we don't understand the requested + // blob encoding scheme. + ErrUnknownBlobType = errors.New("unknown blob type") + + // ErrCiphertextTooSmall is a decryption error signaling that the + // ciphertext is smaller than the ciphertext expansion factor. + ErrCiphertextTooSmall = errors.New( + "ciphertext is too small for chacha20poly1305", + ) + + // ErrNoCommitToRemoteOutput is returned when trying to retrieve the + // commit to-remote output from the blob, though none exists. + ErrNoCommitToRemoteOutput = errors.New( + "cannot obtain commit to-remote p2wkh output script from blob", + ) + + // ErrSweepAddressToLong is returned when trying to encode or decode a + // sweep address with length greater than the maximum length of 42 + // bytes, which supports p2wkh and p2sh addresses. + ErrSweepAddressToLong = fmt.Errorf( + "sweep address must be less than or equal to %d bytes long", + MaxSweepAddrSize, + ) +) + +// Size returns the size of the encoded-and-encrypted blob in bytes. +// +// nonce: 24 bytes +// enciphered plaintext: n bytes +// MAC: 16 bytes +func Size(kit JusticeKit) int { + return NonceSize + kit.PlainTextSize() + CiphertextExpansion +} + +// 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 +} + +// 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) +// pair. +// +// NOTE: It is the caller's responsibility to ensure that this method is only +// called once for a given (nonce, key) pair. +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 := kit.encode(&ptxtBuf) + if err != nil { + return nil, err + } + + // Create a new chacha20poly1305 cipher, using a 32-byte key. + cipher, err := chacha20poly1305.NewX(key[:]) + if err != nil { + return nil, err + } + + // Allocate the ciphertext, which will contain the nonce, encrypted + // plaintext and MAC. + plaintext := ptxtBuf.Bytes() + ciphertext := make([]byte, Size(kit)) + + // Generate a random 24-byte nonce in the ciphertext's prefix. + nonce := ciphertext[:NonceSize] + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + // Finally, encrypt the plaintext using the given nonce, storing the + // result in the ciphertext buffer. + cipher.Seal(ciphertext[NonceSize:NonceSize], nonce, plaintext, nil) + + return ciphertext, nil +} + +// Decrypt unenciphers a blob of justice by decrypting the ciphertext using +// 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) { + + // Fail if the blob's overall length is less than required for the nonce + // and expansion factor. + if len(ciphertext) < NonceSize+CiphertextExpansion { + return nil, ErrCiphertextTooSmall + } + + // Create a new chacha20poly1305 cipher, using a 32-byte key. + cipher, err := chacha20poly1305.NewX(key[:]) + if err != nil { + return nil, err + } + + // Allocate the final buffer that will contain the blob's plaintext + // bytes, which is computed by subtracting the ciphertext expansion + // factor from the blob's length. + plaintext := make([]byte, len(ciphertext)-CiphertextExpansion) + + // Decrypt the ciphertext, placing the resulting plaintext in our + // plaintext buffer. + nonce := ciphertext[:NonceSize] + _, err = cipher.Open(plaintext[:0], nonce, ciphertext[NonceSize:], nil) + if err != nil { + return nil, err + } + + commitment, err := blobType.CommitmentType(nil) + if err != nil { + return nil, err + } + + kit, err := commitment.EmptyJusticeKit() + if err != nil { + return nil, err + } + + // 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 +} + +// 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. +// +// blob version 0 plaintext encoding: +// +// sweep address length: 1 byte +// padded sweep address: 42 bytes +// revocation pubkey: 33 bytes +// local delay pubkey: 33 bytes +// csv delay: 4 bytes +// 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 *justiceKitPacketV0) encode(w io.Writer) error { + // Assert the sweep address length is sane. + 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))) + if err != nil { + return err + } + + // Pad the sweep address to our maximum length of 42 bytes. + var sweepAddressBuf [MaxSweepAddrSize]byte + copy(sweepAddressBuf[:], b.sweepAddress) + + // Write padded 42-byte sweep address. + _, err = w.Write(sweepAddressBuf[:]) + if err != nil { + return err + } + + // Write 33-byte revocation public key. + _, err = w.Write(b.revocationPubKey[:]) + if err != nil { + return err + } + + // Write 33-byte local delay public key. + _, err = w.Write(b.localDelayPubKey[:]) + if err != nil { + return err + } + + // Write 4-byte CSV delay. + 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()) + if err != nil { + return err + } + + // Write 33-byte commit to-remote public key, which may be blank. + _, 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()) + + return err +} + +// 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. +// +// blob version 0 plaintext encoding: +// +// sweep address length: 1 byte +// padded sweep address: 42 bytes +// revocation pubkey: 33 bytes +// local delay pubkey: 33 bytes +// csv delay: 4 bytes +// 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 *justiceKitPacketV0) decode(r io.Reader) error { + // Read the sweep address length as a single byte. + var sweepAddrLen uint8 + err := binary.Read(r, byteOrder, &sweepAddrLen) + if err != nil { + return err + } + + // Assert the sweep address length is sane. + if sweepAddrLen > MaxSweepAddrSize { + return ErrSweepAddressToLong + } + + // Read padded 42-byte sweep address. + var sweepAddressBuf [MaxSweepAddrSize]byte + _, err = io.ReadFull(r, sweepAddressBuf[:]) + if err != nil { + return err + } + + // Parse sweep address from padded buffer. + b.sweepAddress = make([]byte, sweepAddrLen) + copy(b.sweepAddress, sweepAddressBuf[:]) + + // Read 33-byte revocation public key. + _, err = io.ReadFull(r, b.revocationPubKey[:]) + if err != nil { + return err + } + + // Read 33-byte local delay public key. + _, err = io.ReadFull(r, b.localDelayPubKey[:]) + if err != nil { + return err + } + + // Read 4-byte CSV delay. + err = binary.Read(r, byteOrder, &b.csvDelay) + if err != nil { + return err + } + + // Read 64-byte revocation signature for commit to-local output. + var localSig [64]byte + _, err = io.ReadFull(r, localSig[:]) + if err != nil { + return err + } + + b.commitToLocalSig, err = lnwire.NewSigFromWireECDSA(localSig[:]) + if err != nil { + return err + } + + var ( + commitToRemotePubkey pubKey + commitToRemoteSig [64]byte + ) + + // Read 33-byte commit to-remote public key, which may be discarded. + _, err = io.ReadFull(r, commitToRemotePubkey[:]) + if err != nil { + return err + } + + // Read 64-byte commit to-remote signature, which may be discarded. + _, err = io.ReadFull(r, commitToRemoteSig[:]) + if err != nil { + return err + } + + // 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( + commitToRemoteSig[:], + ) + if err != nil { + return err + } + } + + return 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/blob/type.go b/watchtower/blob/type.go index 87dd52aab..f77befb89 100644 --- a/watchtower/blob/type.go +++ b/watchtower/blob/type.go @@ -3,6 +3,8 @@ package blob import ( "fmt" "strings" + + "github.com/lightningnetwork/lnd/channeldb" ) // Flag represents a specify option that can be present in a Type. @@ -81,6 +83,27 @@ func (t Type) Identifier() (string, error) { } } +// CommitmentType returns the appropriate CommitmentType for the given blob Type +// and channel type. +func (t Type) CommitmentType(chanType *channeldb.ChannelType) (CommitmentType, + error) { + + switch { + case t.Has(FlagAnchorChannel): + return AnchorCommitment, nil + + case t.Has(FlagCommitOutputs): + if chanType != nil && chanType.IsTweakless() { + return LegacyTweaklessCommitment, nil + } + + return LegacyCommitment, nil + + default: + return 0, ErrUnknownBlobType + } +} + // Has returns true if the Type has the passed flag enabled. func (t Type) Has(flag Flag) bool { return Flag(t)&flag == flag diff --git a/watchtower/lookout/justice_descriptor.go b/watchtower/lookout/justice_descriptor.go index a474d5ccb..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 { @@ -264,9 +216,14 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) { weightEstimate input.TxWeightEstimator ) + commitmentType, err := p.SessionInfo.Policy.BlobType.CommitmentType(nil) + if err != nil { + return nil, err + } + // 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() @@ -290,16 +247,13 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) { return nil, err } - // 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. 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) + // Get the weight for the to-local witness and add that to the + // estimator. + toLocalWitnessSize, err := commitmentType.ToLocalWitnessSize() + if err != nil { + return nil, err } + weightEstimate.AddWitnessInput(toLocalWitnessSize) sweepInputs = append(sweepInputs, toLocalInput) @@ -319,11 +273,14 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) { log.Debugf("Found to remote witness output=%#v, stack=%v", toRemoteInput.txOut, toRemoteInput.witness) - if p.JusticeKit.BlobType.IsAnchorChannel() { - weightEstimate.AddWitnessInput(input.ToRemoteConfirmedWitnessSize) - } else { - weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) + // Get the weight for the to-remote witness and add that to the + // estimator. + toRemoteWitnessSize, err := commitmentType.ToRemoteWitnessSize() + if err != nil { + return nil, err } + + weightEstimate.AddWitnessInput(toRemoteWitnessSize) } // TODO(conner): sweep htlc outputs @@ -339,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 } @@ -349,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 c37ed2693..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" @@ -92,15 +93,13 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { ) // Parse the key pairs for all keys used in the test. - revSK, revPK := btcec.PrivKeyFromBytes( - revPrivBytes, - ) - _, toLocalPK := btcec.PrivKeyFromBytes( - toLocalPrivBytes, - ) - toRemoteSK, toRemotePK := btcec.PrivKeyFromBytes( - toRemotePrivBytes, - ) + revSK, revPK := btcec.PrivKeyFromBytes(revPrivBytes) + _, toLocalPK := btcec.PrivKeyFromBytes(toLocalPrivBytes) + toRemoteSK, toRemotePK := btcec.PrivKeyFromBytes(toRemotePrivBytes) + + // Get the commitment type. + commitType, err := blobType.CommitmentType(nil) + require.NoError(t, err) // Create the signer, and add the revocation and to-remote privkeys. signer := wtmock.NewMockSigner() @@ -113,11 +112,11 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { toLocalScript, err := input.CommitScriptToSelf( csvDelay, toLocalPK, revPK, ) - require.Nil(t, err) + require.NoError(t, err) // Compute the to-local witness script hash. toLocalScriptHash, err := input.WitnessScriptHash(toLocalScript) - require.Nil(t, err) + require.NoError(t, err) // Compute the to-remote redeem script, witness script hash, and // sequence numbers. @@ -147,12 +146,12 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { toRemoteRedeemScript, err = input.CommitScriptToRemoteConfirmed( toRemotePK, ) - require.Nil(t, err) + require.NoError(t, err) toRemoteScriptHash, err = input.WitnessScriptHash( toRemoteRedeemScript, ) - require.Nil(t, err) + require.NoError(t, err) // As it should be. toRemoteSigningScript = toRemoteRedeemScript @@ -161,7 +160,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { toRemoteScriptHash, err = input.CommitScriptUnencumbered( toRemotePK, ) - require.Nil(t, err) + require.NoError(t, err) // NOTE: This is the _pkscript_. toRemoteSigningScript = toRemoteScriptHash @@ -188,26 +187,24 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { // Compute the weight estimate for our justice transaction. var weightEstimate input.TxWeightEstimator - // 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. For anchor - // channels we fix this and use the correct witness size. - if isAnchorChannel { - weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize) - } else { - weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1) - } + // Add the local witness size to the weight estimator. + toLocalWitnessSize, err := commitType.ToLocalWitnessSize() + require.NoError(t, err) + weightEstimate.AddWitnessInput(toLocalWitnessSize) - if isAnchorChannel { - weightEstimate.AddWitnessInput(input.ToRemoteConfirmedWitnessSize) - } else { - weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) - } + // Add the remote witness size to the weight estimator. + toRemoteWitnessSize, err := commitType.ToRemoteWitnessSize() + require.NoError(t, err) + weightEstimate.AddWitnessInput(toRemoteWitnessSize) + + // Add the sweep output to the weight estimator. weightEstimate.AddP2WKHOutput() + + // Add the reward output to the weight estimator. if blobType.Has(blob.FlagReward) { weightEstimate.AddP2WKHOutput() } + txWeight := weightEstimate.Weight() // Create a session info so that simulate agreement of the sweep @@ -225,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/ @@ -260,10 +260,10 @@ 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.Nil(t, err) + require.NoError(t, err) // Attach the txouts and BIP69 sort the resulting transaction. justiceTxn.TxOut = outputs @@ -320,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 62cc3d367..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" @@ -12,7 +11,6 @@ import ( "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/lightningnetwork/lnd/watchtower/wtdb" ) @@ -37,8 +35,9 @@ import ( // necessary components are stripped out and encrypted before being sent to // the tower in a StateUpdate. type backupTask struct { - id wtdb.BackupID - breachInfo *lnwallet.BreachRetribution + id wtdb.BackupID + breachInfo *lnwallet.BreachRetribution + commitmentType blob.CommitmentType // state-dependent variables @@ -127,6 +126,11 @@ func (t *backupTask) bindSession(session *wtdb.ClientSessionBody, return err } + commitType, err := session.Policy.BlobType.CommitmentType(&chanType) + if err != nil { + return err + } + // Parse the non-dust outputs from the breach transaction, // simultaneously computing the total amount contained in the inputs // present. We can't compute the exact output values at this time @@ -147,48 +151,23 @@ func (t *backupTask) bindSession(session *wtdb.ClientSessionBody, // to that output as local, though relative to their commitment, it is // paying to-the-remote party (which is us). if breachInfo.RemoteOutputSignDesc != nil { - toLocalInput = input.NewBaseInput( - &breachInfo.RemoteOutpoint, - input.CommitmentRevoke, - breachInfo.RemoteOutputSignDesc, - 0, - ) + toLocalInput, err = commitType.ToLocalInput(breachInfo) + if err != nil { + return err + } + totalAmt += breachInfo.RemoteOutputSignDesc.Output.Value } if breachInfo.LocalOutputSignDesc != nil { - var witnessType input.WitnessType - switch { - case chanType.HasAnchors(): - witnessType = input.CommitmentToRemoteConfirmed - case chanType.IsTweakless(): - witnessType = input.CommitSpendNoDelayTweakless - default: - witnessType = input.CommitmentNoDelay - } - - // Anchor channels have a CSV-encumbered to-remote output. We'll - // construct a CSV input in that case and assign the proper CSV - // delay of 1, otherwise we fallback to the a regular P2WKH - // to-remote output for tweaked or tweakless channels. - if chanType.HasAnchors() { - toRemoteInput = input.NewCsvInput( - &breachInfo.LocalOutpoint, - witnessType, - breachInfo.LocalOutputSignDesc, - 0, 1, - ) - } else { - toRemoteInput = input.NewBaseInput( - &breachInfo.LocalOutpoint, - witnessType, - breachInfo.LocalOutputSignDesc, - 0, - ) + toRemoteInput, err = commitType.ToRemoteInput(breachInfo) + if err != nil { + return err } totalAmt += breachInfo.LocalOutputSignDesc.Output.Value } + t.commitmentType = commitType t.breachInfo = breachInfo t.toLocalInput = toLocalInput t.toRemoteInput = toRemoteInput @@ -202,34 +181,20 @@ func (t *backupTask) bindSession(session *wtdb.ClientSessionBody, // Next, add the contribution from the inputs that are present on this // breach transaction. if t.toLocalInput != nil { - // 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. For anchor channels we'll go ahead - // an use the correct penalty witness when signing our justice - // transactions. - if chanType.HasAnchors() { - weightEstimate.AddWitnessInput( - input.ToLocalPenaltyWitnessSize, - ) - } else { - weightEstimate.AddWitnessInput( - input.ToLocalPenaltyWitnessSize - 1, - ) + toLocalWitnessSize, err := commitType.ToLocalWitnessSize() + if err != nil { + return err } + + weightEstimate.AddWitnessInput(toLocalWitnessSize) } if t.toRemoteInput != nil { - // Legacy channels (both tweaked and non-tweaked) spend from - // P2WKH output. Anchor channels spend a to-remote confirmed - // P2WSH output. - if chanType.HasAnchors() { - weightEstimate.AddWitnessInput( - input.ToRemoteConfirmedWitnessSize, - ) - } else { - weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) + toRemoteWitnessSize, err := commitType.ToRemoteWitnessSize() + if err != nil { + return err } + + weightEstimate.AddWitnessInput(toRemoteWitnessSize) } // All justice transactions will either use segwit v0 (p2wkh + p2wsh) @@ -281,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 @@ -349,6 +300,7 @@ func (t *backupTask) craftSessionPayload( // Now, iterate through the list of inputs that were initially added to // the transaction and store the computed witness within the justice // kit. + commitType := t.commitmentType for _, inp := range inputs { // Lookup the input's new post-sort position. i := inputIndex[*inp.OutPoint()] @@ -361,17 +313,17 @@ func (t *backupTask) craftSessionPayload( return hint, nil, err } - // Parse the DER-encoded signature from the first position of - // the resulting witness. We trim an extra byte to remove the - // sighash flag. - witness := inputScript.Witness - rawSignature := witness[0][:len(witness[0])-1] + signature, err := commitType.ParseRawSig(inputScript.Witness) + if err != nil { + return hint, nil, err + } - // Re-encode the DER signature into a fixed-size 64 byte - // signature. - signature, err := lnwire.NewSigFromECDSARawSignature( - rawSignature, - ) + toLocalWitnessType, err := commitType.ToLocalWitnessType() + if err != nil { + return hint, nil, err + } + + toRemoteWitnessType, err := commitType.ToRemoteWitnessType() if err != nil { return hint, nil, err } @@ -380,15 +332,10 @@ func (t *backupTask) craftSessionPayload( // using the input's witness type to select the appropriate // field switch inp.WitnessType() { - case input.CommitmentRevoke: - justiceKit.CommitToLocalSig = signature - - case input.CommitSpendNoDelayTweakless: - fallthrough - case input.CommitmentNoDelay: - fallthrough - case input.CommitmentToRemoteConfirmed: - justiceKit.CommitToRemoteSig = signature + case toLocalWitnessType: + justiceKit.AddToLocalSig(signature) + case toRemoteWitnessType: + justiceKit.AddToRemoteSig(signature) default: return hint, nil, fmt.Errorf("invalid witness type: %v", inp.WitnessType()) @@ -403,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 4639b8164..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 @@ -72,6 +72,7 @@ type backupTaskTest struct { // corresponding BreachInfo, as well as setting the wtpolicy.Policy of the given // session. func genTaskTest( + t *testing.T, name string, stateNum uint64, toLocalAmt int64, @@ -91,15 +92,12 @@ func genTaskTest( } // Parse the key pairs for all keys used in the test. - revSK, revPK := btcec.PrivKeyFromBytes( - revPrivBytes, - ) - _, toLocalPK := btcec.PrivKeyFromBytes( - toLocalPrivBytes, - ) - toRemoteSK, toRemotePK := btcec.PrivKeyFromBytes( - toRemotePrivBytes, - ) + revSK, revPK := btcec.PrivKeyFromBytes(revPrivBytes) + _, toLocalPK := btcec.PrivKeyFromBytes(toLocalPrivBytes) + toRemoteSK, toRemotePK := btcec.PrivKeyFromBytes(toRemotePrivBytes) + + commitType, err := blobType.CommitmentType(&chanType) + require.NoError(t, err) // Create the signer, and add the revocation and to-remote privkeys. signer := wtmock.NewMockSigner() @@ -174,12 +172,9 @@ func genTaskTest( Hash: txid, Index: index, } - toLocalInput = input.NewBaseInput( - &breachInfo.RemoteOutpoint, - input.CommitmentRevoke, - breachInfo.RemoteOutputSignDesc, - 0, - ) + toLocalInput, err = commitType.ToLocalInput(breachInfo) + require.NoError(t, err) + index++ } if toRemoteAmt > 0 { @@ -188,31 +183,8 @@ func genTaskTest( Index: index, } - var witnessType input.WitnessType - switch { - case chanType.HasAnchors(): - witnessType = input.CommitmentToRemoteConfirmed - case chanType.IsTweakless(): - witnessType = input.CommitSpendNoDelayTweakless - default: - witnessType = input.CommitmentNoDelay - } - - if chanType.HasAnchors() { - toRemoteInput = input.NewCsvInput( - &breachInfo.LocalOutpoint, - witnessType, - breachInfo.LocalOutputSignDesc, - 0, 1, - ) - } else { - toRemoteInput = input.NewBaseInput( - &breachInfo.LocalOutpoint, - witnessType, - breachInfo.LocalOutputSignDesc, - 0, - ) - } + toRemoteInput, err = commitType.ToRemoteInput(breachInfo) + require.NoError(t, err) } return backupTaskTest{ @@ -238,6 +210,7 @@ func genTaskTest( expSweepScript: sweepAddr, signer: signer, chanType: chanType, + commitType: commitType, } } @@ -312,6 +285,7 @@ func TestBackupTask(t *testing.T) { backupTaskTests = append(backupTaskTests, []backupTaskTest{ genTaskTest( + t, "commit no-reward, both outputs", 100, // stateNum 200000, // toLocalAmt @@ -325,6 +299,7 @@ func TestBackupTask(t *testing.T) { chanType, ), genTaskTest( + t, "commit no-reward, to-local output only", 1000, // stateNum 200000, // toLocalAmt @@ -338,6 +313,7 @@ func TestBackupTask(t *testing.T) { chanType, ), genTaskTest( + t, "commit no-reward, to-remote output only", 1, // stateNum 0, // toLocalAmt @@ -351,6 +327,7 @@ func TestBackupTask(t *testing.T) { chanType, ), genTaskTest( + t, "commit no-reward, to-remote output only, creates dust", 1, // stateNum 0, // toLocalAmt @@ -364,6 +341,7 @@ func TestBackupTask(t *testing.T) { chanType, ), genTaskTest( + t, "commit no-reward, no outputs, fee rate exceeds inputs", 300, // stateNum 0, // toLocalAmt @@ -377,6 +355,7 @@ func TestBackupTask(t *testing.T) { chanType, ), genTaskTest( + t, "commit no-reward, no outputs, fee rate of 0 creates dust", 300, // stateNum 0, // toLocalAmt @@ -390,6 +369,7 @@ func TestBackupTask(t *testing.T) { chanType, ), genTaskTest( + t, "commit reward, both outputs", 100, // stateNum 200000, // toLocalAmt @@ -403,6 +383,7 @@ func TestBackupTask(t *testing.T) { chanType, ), genTaskTest( + t, "commit reward, to-local output only", 1000, // stateNum 200000, // toLocalAmt @@ -416,6 +397,7 @@ func TestBackupTask(t *testing.T) { chanType, ), genTaskTest( + t, "commit reward, to-remote output only", 1, // stateNum 0, // toLocalAmt @@ -429,6 +411,7 @@ func TestBackupTask(t *testing.T) { chanType, ), genTaskTest( + t, "commit reward, to-remote output only, creates dust", 1, // stateNum 0, // toLocalAmt @@ -442,6 +425,7 @@ func TestBackupTask(t *testing.T) { chanType, ), genTaskTest( + t, "commit reward, no outputs, fee rate exceeds inputs", 300, // stateNum 0, // toLocalAmt @@ -455,6 +439,7 @@ func TestBackupTask(t *testing.T) { chanType, ), genTaskTest( + t, "commit reward, no outputs, fee rate of 0 creates dust", 300, // stateNum 0, // toLocalAmt @@ -583,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.