watchtower: convert JusticeKit to interface

In this commit, we convert the `JusticeKit` struct to an interface.
Then, we add two implementations of that interface:
1) The `legacyJusticeKit` which implements all the methods of
   `JusticeKit`
2) The `anchorJusticKit` which wraps the `legacyJusticeKit` and just
   re-implements the `ToRemoteOutputSpendInfo` method since.
This commit is contained in:
Elle Mouton
2023-08-23 10:51:57 +02:00
parent 048dc54110
commit 154e9fafec
14 changed files with 666 additions and 520 deletions

View File

@@ -172,3 +172,42 @@ func (c CommitmentType) ParseRawSig(witness wire.TxWitness) (lnwire.Sig,
c)
}
}
// NewJusticeKit can be used to construct a new JusticeKit depending on the
// CommitmentType.
func (c CommitmentType) NewJusticeKit(sweepScript []byte,
breachInfo *lnwallet.BreachRetribution, withToRemote bool) (JusticeKit,
error) {
switch c {
case LegacyCommitment, LegacyTweaklessCommitment:
return newLegacyJusticeKit(
sweepScript, breachInfo, withToRemote,
), nil
case AnchorCommitment:
return newAnchorJusticeKit(
sweepScript, breachInfo, withToRemote,
), nil
default:
return nil, fmt.Errorf("unknown commitment type: %v", c)
}
}
// EmptyJusticeKit returns the appropriate empty justice kit for the given
// CommitmentType.
func (c CommitmentType) EmptyJusticeKit() (JusticeKit, error) {
switch c {
case LegacyTweaklessCommitment, LegacyCommitment:
return &legacyJusticeKit{}, nil
case AnchorCommitment:
return &anchorJusticeKit{
legacyJusticeKit: legacyJusticeKit{},
}, nil
default:
return nil, fmt.Errorf("unknown commitment type: %v", c)
}
}

View File

@@ -1,153 +1,288 @@
package blob
import (
"io"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
)
// JusticeKit is lé Blob of Justice. The JusticeKit contains information
// required to construct a justice transaction, that sweeps a remote party's
// revoked commitment transaction. It supports encryption and decryption using
// chacha20poly1305, allowing the client to encrypt the contents of the blob,
// and for a watchtower to later decrypt if action must be taken. The encoding
// format is versioned to allow future extensions.
type JusticeKit struct {
// BlobType encodes a bitfield that inform the tower of various features
// requested by the client when resolving a breach. Examples include
// whether the justice transaction contains a reward for the tower, or
// whether the channel is a legacy or anchor channel.
//
// NOTE: This value is not serialized in the encrypted payload. It is
// stored separately and added to the JusticeKit after decryption.
BlobType Type
// JusticeKit is an interface that describes lé Blob of Justice. An
// implementation of the JusticeKit contains information required to construct
// a justice transaction, that sweeps a remote party's revoked commitment
// transaction. It supports encryption and decryption using chacha20poly1305,
// allowing the client to encrypt the contents of the blob, and for a
// watchtower to later decrypt if action must be taken.
type JusticeKit interface {
// ToLocalOutputSpendInfo returns the info required to send the to-local
// output. It returns the output pub key script and the witness required
// to spend the output.
ToLocalOutputSpendInfo() (*txscript.PkScript, wire.TxWitness, error)
// SweepAddress is the witness program of the output where the client's
// fund will be deposited. This value is included in the blobs, as
// opposed to the session info, such that the sweep addresses can't be
// correlated across sessions and/or towers.
//
// NOTE: This is chosen to be the length of a maximally sized witness
// program.
SweepAddress []byte
// ToRemoteOutputSpendInfo returns the info required to send the
// to-remote output. It returns the output pub key script, the witness
// required to spend the output and the sequence to apply.
ToRemoteOutputSpendInfo() (*txscript.PkScript, wire.TxWitness, uint32,
error)
// RevocationPubKey is the compressed pubkey that guards the revocation
// clause of the remote party's to-local output.
RevocationPubKey PubKey
// HasCommitToRemoteOutput returns true if the kit does include the
// information required to sweep the to-remote output.
HasCommitToRemoteOutput() bool
// LocalDelayPubKey is the compressed pubkey in the to-local script of
// the remote party, which guards the path where the remote party
// claims their commitment output.
LocalDelayPubKey PubKey
// AddToLocalSig adds the to-local signature to the kit.
AddToLocalSig(sig lnwire.Sig)
// CSVDelay is the relative timelock in the remote party's to-local
// output, which the remote party must wait out before sweeping their
// commitment output.
CSVDelay uint32
// AddToRemoteSig adds the to-remote signature to the kit.
AddToRemoteSig(sig lnwire.Sig)
// CommitToLocalSig is a signature under RevocationPubKey using
// SIGHASH_ALL.
CommitToLocalSig lnwire.Sig
// SweepAddress returns the sweep address to be used on the justice tx
// output.
SweepAddress() []byte
// CommitToRemotePubKey is the public key in the to-remote output of the
// revoked commitment transaction.
//
// NOTE: This value is only used if it contains a valid compressed
// public key.
CommitToRemotePubKey PubKey
// PlainTextSize is the size of the encoded-but-unencrypted blob in
// bytes.
PlainTextSize() int
// CommitToRemoteSig is a signature under CommitToRemotePubKey using
// SIGHASH_ALL.
//
// NOTE: This value is only used if CommitToRemotePubKey contains a
// valid compressed public key.
CommitToRemoteSig lnwire.Sig
encode(w io.Writer) error
decode(r io.Reader) error
}
// CommitToLocalWitnessScript returns the serialized witness script for the
// commitment to-local output.
func (b *JusticeKit) CommitToLocalWitnessScript() ([]byte, error) {
revocationPubKey, err := btcec.ParsePubKey(
b.RevocationPubKey[:],
)
if err != nil {
return nil, err
}
localDelayedPubKey, err := btcec.ParsePubKey(
b.LocalDelayPubKey[:],
)
if err != nil {
return nil, err
}
return input.CommitScriptToSelf(
b.CSVDelay, localDelayedPubKey, revocationPubKey,
)
// legacyJusticeKit is an implementation of the JusticeKit interface which can
// be used for backing up commitments of legacy (pre-anchor) channels.
type legacyJusticeKit struct {
justiceKitPacketV0
}
// CommitToLocalRevokeWitnessStack constructs a witness stack spending the
// revocation clause of the commitment to-local output.
// A compile-time check to ensure that legacyJusticeKit implements the
// JusticeKit interface.
var _ JusticeKit = (*legacyJusticeKit)(nil)
// newLegacyJusticeKit constructs a new legacyJusticeKit.
func newLegacyJusticeKit(sweepScript []byte,
breachInfo *lnwallet.BreachRetribution,
withToRemote bool) *legacyJusticeKit {
keyRing := breachInfo.KeyRing
packet := justiceKitPacketV0{
sweepAddress: sweepScript,
revocationPubKey: toBlobPubKey(keyRing.RevocationKey),
localDelayPubKey: toBlobPubKey(keyRing.ToLocalKey),
csvDelay: breachInfo.RemoteDelay,
commitToRemotePubKey: pubKey{},
}
if withToRemote {
packet.commitToRemotePubKey = toBlobPubKey(
keyRing.ToRemoteKey,
)
}
return &legacyJusticeKit{packet}
}
// ToLocalOutputSpendInfo returns the info required to send the to-local output.
// It returns the output pub key script and the witness required to spend the
// output.
//
// <revocation-sig> 1
func (b *JusticeKit) CommitToLocalRevokeWitnessStack() ([][]byte, error) {
toLocalSig, err := b.CommitToLocalSig.ToSignature()
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) ToLocalOutputSpendInfo() (*txscript.PkScript,
wire.TxWitness, error) {
revocationPubKey, err := btcec.ParsePubKey(l.revocationPubKey[:])
if err != nil {
return nil, err
return nil, nil, err
}
witnessStack := make([][]byte, 2)
witnessStack[0] = append(toLocalSig.Serialize(),
byte(txscript.SigHashAll))
witnessStack[1] = []byte{1}
localDelayedPubKey, err := btcec.ParsePubKey(l.localDelayPubKey[:])
if err != nil {
return nil, nil, err
}
return witnessStack, nil
script, err := input.CommitScriptToSelf(
l.csvDelay, localDelayedPubKey, revocationPubKey,
)
if err != nil {
return nil, nil, err
}
scriptPubKey, err := input.WitnessScriptHash(script)
if err != nil {
return nil, nil, err
}
toLocalSig, err := l.commitToLocalSig.ToSignature()
if err != nil {
return nil, nil, err
}
witness := make(wire.TxWitness, 3)
witness[0] = append(toLocalSig.Serialize(), byte(txscript.SigHashAll))
witness[1] = []byte{1}
witness[2] = script
pkScript, err := txscript.ParsePkScript(scriptPubKey)
if err != nil {
return nil, nil, err
}
return &pkScript, witness, nil
}
// ToRemoteOutputSpendInfo returns the info required to spend the to-remote
// output. It returns the output pub key script, the witness required to spend
// the output and the sequence to apply.
//
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript,
wire.TxWitness, uint32, error) {
if !btcec.IsCompressedPubKey(l.commitToRemotePubKey[:]) {
return nil, nil, 0, ErrNoCommitToRemoteOutput
}
toRemoteScript := l.commitToRemotePubKey[:]
// Since the to-remote witness script should just be a regular p2wkh
// output, we'll parse it to retrieve the public key.
toRemotePubKey, err := btcec.ParsePubKey(toRemoteScript)
if err != nil {
return nil, nil, 0, err
}
// Compute the witness script hash from the to-remote pubkey, which will
// be used to locate the output on the breach commitment transaction.
toRemoteScriptHash, err := input.CommitScriptUnencumbered(
toRemotePubKey,
)
if err != nil {
return nil, nil, 0, err
}
toRemoteSig, err := l.commitToRemoteSig.ToSignature()
if err != nil {
return nil, nil, 0, err
}
witness := make(wire.TxWitness, 2)
witness[0] = append(toRemoteSig.Serialize(), byte(txscript.SigHashAll))
witness[1] = toRemoteScript
pkScript, err := txscript.ParsePkScript(toRemoteScriptHash)
if err != nil {
return nil, nil, 0, err
}
return &pkScript, witness, 0, nil
}
// HasCommitToRemoteOutput returns true if the blob contains a to-remote p2wkh
// pubkey.
func (b *JusticeKit) HasCommitToRemoteOutput() bool {
return btcec.IsCompressedPubKey(b.CommitToRemotePubKey[:])
}
// CommitToRemoteWitnessScript returns the witness script for the commitment
// to-remote output given the blob type. The script returned will either be for
// a p2wpkh to-remote output or an p2wsh anchor to-remote output which includes
// a CSV delay.
func (b *JusticeKit) CommitToRemoteWitnessScript() ([]byte, error) {
if !btcec.IsCompressedPubKey(b.CommitToRemotePubKey[:]) {
return nil, ErrNoCommitToRemoteOutput
}
// If this is a blob for an anchor channel, we'll return the p2wsh
// output containing a CSV delay of 1.
if b.BlobType.IsAnchorChannel() {
pk, err := btcec.ParsePubKey(b.CommitToRemotePubKey[:])
if err != nil {
return nil, err
}
return input.CommitScriptToRemoteConfirmed(pk)
}
return b.CommitToRemotePubKey[:], nil
}
// CommitToRemoteWitnessStack returns a witness stack spending the commitment
// to-remote output, which consists of a single signature satisfying either the
// legacy or anchor witness scripts.
//
// <to-remote-sig>
func (b *JusticeKit) CommitToRemoteWitnessStack() ([][]byte, error) {
toRemoteSig, err := b.CommitToRemoteSig.ToSignature()
if err != nil {
return nil, err
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) HasCommitToRemoteOutput() bool {
return btcec.IsCompressedPubKey(l.commitToRemotePubKey[:])
}
// SweepAddress returns the sweep address to be used on the justice tx
// output.
//
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) SweepAddress() []byte {
return l.sweepAddress
}
// AddToLocalSig adds the to-local signature to the kit.
//
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) AddToLocalSig(sig lnwire.Sig) {
l.commitToLocalSig = sig
}
// AddToRemoteSig adds the to-remote signature to the kit.
//
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) AddToRemoteSig(sig lnwire.Sig) {
l.commitToRemoteSig = sig
}
// PlainTextSize is the size of the encoded-but-unencrypted blob in
// bytes.
//
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) PlainTextSize() int {
return V0PlaintextSize
}
// anchorJusticeKit is an implementation of the JusticeKit interface which can
// be used for backing up commitments of anchor channels. It inherits most of
// the methods from the legacyJusticeKit and overrides the
// ToRemoteOutputSpendInfo method since the to-remote output of an anchor
// output is a P2WSH instead of the P2WPKH used by the legacy channels.
type anchorJusticeKit struct {
legacyJusticeKit
}
// A compile-time check to ensure that legacyJusticeKit implements the
// JusticeKit interface.
var _ JusticeKit = (*anchorJusticeKit)(nil)
// newAnchorJusticeKit constructs a new anchorJusticeKit.
func newAnchorJusticeKit(sweepScript []byte,
breachInfo *lnwallet.BreachRetribution,
withToRemote bool) *anchorJusticeKit {
legacyKit := newLegacyJusticeKit(sweepScript, breachInfo, withToRemote)
return &anchorJusticeKit{
legacyJusticeKit: *legacyKit,
}
}
// ToRemoteOutputSpendInfo returns the info required to send the to-remote
// output. It returns the output pub key script, the witness required to spend
// the output and the sequence to apply.
//
// NOTE: This is part of the JusticeKit interface.
func (a *anchorJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript,
wire.TxWitness, uint32, error) {
if !btcec.IsCompressedPubKey(a.commitToRemotePubKey[:]) {
return nil, nil, 0, ErrNoCommitToRemoteOutput
}
witnessStack := make([][]byte, 1)
witnessStack[0] = append(toRemoteSig.Serialize(),
byte(txscript.SigHashAll))
pk, err := btcec.ParsePubKey(a.commitToRemotePubKey[:])
if err != nil {
return nil, nil, 0, err
}
return witnessStack, nil
toRemoteScript, err := input.CommitScriptToRemoteConfirmed(pk)
if err != nil {
return nil, nil, 0, err
}
toRemoteScriptHash, err := input.WitnessScriptHash(toRemoteScript)
if err != nil {
return nil, nil, 0, err
}
toRemoteSig, err := a.commitToRemoteSig.ToSignature()
if err != nil {
return nil, nil, 0, err
}
witness := make([][]byte, 2)
witness[0] = append(toRemoteSig.Serialize(), byte(txscript.SigHashAll))
witness[1] = toRemoteScript
pkScript, err := txscript.ParsePkScript(toRemoteScriptHash)
if err != nil {
return nil, nil, 0, err
}
return &pkScript, witness, 1, nil
}

View File

@@ -74,22 +74,68 @@ var (
// nonce: 24 bytes
// enciphered plaintext: n bytes
// MAC: 16 bytes
func Size(blobType Type) int {
return NonceSize + PlaintextSize(blobType) + CiphertextExpansion
func Size(kit JusticeKit) int {
return NonceSize + kit.PlainTextSize() + CiphertextExpansion
}
// PlaintextSize returns the size of the encoded-but-unencrypted blob in bytes.
func PlaintextSize(blobType Type) int {
switch {
case blobType.Has(FlagCommitOutputs):
return V0PlaintextSize
default:
return 0
}
// pubKey is a 33-byte, serialized compressed public key.
type pubKey [33]byte
// toBlobPubKey serializes the given public key into a pubKey that can be set
// as a field on a JusticeKit.
func toBlobPubKey(pk *btcec.PublicKey) pubKey {
var blobPubKey pubKey
copy(blobPubKey[:], pk.SerializeCompressed())
return blobPubKey
}
// PubKey is a 33-byte, serialized compressed public key.
type PubKey [33]byte
// justiceKitPacketV0 is lé Blob of Justice. The JusticeKit contains information
// required to construct a justice transaction, that sweeps a remote party's
// revoked commitment transaction. It supports encryption and decryption using
// chacha20poly1305, allowing the client to encrypt the contents of the blob,
// and for a watchtower to later decrypt if action must be taken.
type justiceKitPacketV0 struct {
// sweepAddress is the witness program of the output where the client's
// fund will be deposited. This value is included in the blobs, as
// opposed to the session info, such that the sweep addresses can't be
// correlated across sessions and/or towers.
//
// NOTE: This is chosen to be the length of a maximally sized witness
// program.
sweepAddress []byte
// revocationPubKey is the compressed pubkey that guards the revocation
// clause of the remote party's to-local output.
revocationPubKey pubKey
// localDelayPubKey is the compressed pubkey in the to-local script of
// the remote party, which guards the path where the remote party
// claims their commitment output.
localDelayPubKey pubKey
// csvDelay is the relative timelock in the remote party's to-local
// output, which the remote party must wait out before sweeping their
// commitment output.
csvDelay uint32
// commitToLocalSig is a signature under RevocationPubKey using
// SIGHASH_ALL.
commitToLocalSig lnwire.Sig
// commitToRemotePubKey is the public key in the to-remote output of the
// revoked commitment transaction.
//
// NOTE: This value is only used if it contains a valid compressed
// public key.
commitToRemotePubKey pubKey
// commitToRemoteSig is a signature under CommitToRemotePubKey using
// SIGHASH_ALL.
//
// NOTE: This value is only used if CommitToRemotePubKey contains a
// valid compressed public key.
commitToRemoteSig lnwire.Sig
}
// Encrypt encodes the blob of justice using encoding version, and then
// creates a ciphertext using chacha20poly1305 under the chosen (nonce, key)
@@ -97,11 +143,11 @@ type PubKey [33]byte
//
// NOTE: It is the caller's responsibility to ensure that this method is only
// called once for a given (nonce, key) pair.
func (b *JusticeKit) Encrypt(key BreachKey) ([]byte, error) {
func Encrypt(kit JusticeKit, key BreachKey) ([]byte, error) {
// Encode the plaintext using the provided version, to obtain the
// plaintext bytes.
var ptxtBuf bytes.Buffer
err := b.encode(&ptxtBuf, b.BlobType)
err := kit.encode(&ptxtBuf)
if err != nil {
return nil, err
}
@@ -115,7 +161,7 @@ func (b *JusticeKit) Encrypt(key BreachKey) ([]byte, error) {
// Allocate the ciphertext, which will contain the nonce, encrypted
// plaintext and MAC.
plaintext := ptxtBuf.Bytes()
ciphertext := make([]byte, Size(b.BlobType))
ciphertext := make([]byte, Size(kit))
// Generate a random 24-byte nonce in the ciphertext's prefix.
nonce := ciphertext[:NonceSize]
@@ -134,7 +180,7 @@ func (b *JusticeKit) Encrypt(key BreachKey) ([]byte, error) {
// chacha20poly1305 with the chosen (nonce, key) pair. The internal plaintext is
// then deserialized using the given encoding version.
func Decrypt(key BreachKey, ciphertext []byte,
blobType Type) (*JusticeKit, error) {
blobType Type) (JusticeKit, error) {
// Fail if the blob's overall length is less than required for the nonce
// and expansion factor.
@@ -161,42 +207,27 @@ func Decrypt(key BreachKey, ciphertext []byte,
return nil, err
}
// If decryption succeeded, we will then decode the plaintext bytes
// using the specified blob version.
boj := &JusticeKit{
BlobType: blobType,
}
err = boj.decode(bytes.NewReader(plaintext), blobType)
commitment, err := blobType.CommitmentType(nil)
if err != nil {
return nil, err
}
return boj, nil
}
// encode serializes the JusticeKit according to the version, returning an
// error if the version is unknown.
func (b *JusticeKit) encode(w io.Writer, blobType Type) error {
switch {
case blobType.Has(FlagCommitOutputs):
return b.encodeV0(w)
default:
return ErrUnknownBlobType
kit, err := commitment.EmptyJusticeKit()
if err != nil {
return nil, err
}
}
// decode deserializes the JusticeKit according to the version, returning an
// error if the version is unknown.
func (b *JusticeKit) decode(r io.Reader, blobType Type) error {
switch {
case blobType.Has(FlagCommitOutputs):
return b.decodeV0(r)
default:
return ErrUnknownBlobType
// If decryption succeeded, we will then decode the plaintext bytes
// using the specified blob version.
err = kit.decode(bytes.NewReader(plaintext))
if err != nil {
return nil, err
}
return kit, nil
}
// encodeV0 encodes the JusticeKit using the version 0 encoding scheme to the
// encode encodes the JusticeKit using the version 0 encoding scheme to the
// provided io.Writer. The encoding supports sweeping of the commit to-local
// output, and optionally the commit to-remote output. The encoding produces a
// constant-size plaintext size of 274 bytes.
@@ -211,21 +242,21 @@ func (b *JusticeKit) decode(r io.Reader, blobType Type) error {
// commit to-local revocation sig: 64 bytes
// commit to-remote pubkey: 33 bytes, maybe blank
// commit to-remote sig: 64 bytes, maybe blank
func (b *JusticeKit) encodeV0(w io.Writer) error {
func (b *justiceKitPacketV0) encode(w io.Writer) error {
// Assert the sweep address length is sane.
if len(b.SweepAddress) > MaxSweepAddrSize {
if len(b.sweepAddress) > MaxSweepAddrSize {
return ErrSweepAddressToLong
}
// Write the actual length of the sweep address as a single byte.
err := binary.Write(w, byteOrder, uint8(len(b.SweepAddress)))
err := binary.Write(w, byteOrder, uint8(len(b.sweepAddress)))
if err != nil {
return err
}
// Pad the sweep address to our maximum length of 42 bytes.
var sweepAddressBuf [MaxSweepAddrSize]byte
copy(sweepAddressBuf[:], b.SweepAddress)
copy(sweepAddressBuf[:], b.sweepAddress)
// Write padded 42-byte sweep address.
_, err = w.Write(sweepAddressBuf[:])
@@ -234,42 +265,42 @@ func (b *JusticeKit) encodeV0(w io.Writer) error {
}
// Write 33-byte revocation public key.
_, err = w.Write(b.RevocationPubKey[:])
_, err = w.Write(b.revocationPubKey[:])
if err != nil {
return err
}
// Write 33-byte local delay public key.
_, err = w.Write(b.LocalDelayPubKey[:])
_, err = w.Write(b.localDelayPubKey[:])
if err != nil {
return err
}
// Write 4-byte CSV delay.
err = binary.Write(w, byteOrder, b.CSVDelay)
err = binary.Write(w, byteOrder, b.csvDelay)
if err != nil {
return err
}
// Write 64-byte revocation signature for commit to-local output.
_, err = w.Write(b.CommitToLocalSig.RawBytes())
_, err = w.Write(b.commitToLocalSig.RawBytes())
if err != nil {
return err
}
// Write 33-byte commit to-remote public key, which may be blank.
_, err = w.Write(b.CommitToRemotePubKey[:])
_, err = w.Write(b.commitToRemotePubKey[:])
if err != nil {
return err
}
// Write 64-byte commit to-remote signature, which may be blank.
_, err = w.Write(b.CommitToRemoteSig.RawBytes())
_, err = w.Write(b.commitToRemoteSig.RawBytes())
return err
}
// decodeV0 reconstructs a JusticeKit from the io.Reader, using version 0
// decode reconstructs a JusticeKit from the io.Reader, using version 0
// encoding scheme. This will parse a constant size input stream of 274 bytes to
// recover information for the commit to-local output, and possibly the commit
// to-remote output.
@@ -284,7 +315,7 @@ func (b *JusticeKit) encodeV0(w io.Writer) error {
// commit to-local revocation sig: 64 bytes
// commit to-remote pubkey: 33 bytes, maybe blank
// commit to-remote sig: 64 bytes, maybe blank
func (b *JusticeKit) decodeV0(r io.Reader) error {
func (b *justiceKitPacketV0) decode(r io.Reader) error {
// Read the sweep address length as a single byte.
var sweepAddrLen uint8
err := binary.Read(r, byteOrder, &sweepAddrLen)
@@ -305,23 +336,23 @@ func (b *JusticeKit) decodeV0(r io.Reader) error {
}
// Parse sweep address from padded buffer.
b.SweepAddress = make([]byte, sweepAddrLen)
copy(b.SweepAddress, sweepAddressBuf[:])
b.sweepAddress = make([]byte, sweepAddrLen)
copy(b.sweepAddress, sweepAddressBuf[:])
// Read 33-byte revocation public key.
_, err = io.ReadFull(r, b.RevocationPubKey[:])
_, err = io.ReadFull(r, b.revocationPubKey[:])
if err != nil {
return err
}
// Read 33-byte local delay public key.
_, err = io.ReadFull(r, b.LocalDelayPubKey[:])
_, err = io.ReadFull(r, b.localDelayPubKey[:])
if err != nil {
return err
}
// Read 4-byte CSV delay.
err = binary.Read(r, byteOrder, &b.CSVDelay)
err = binary.Read(r, byteOrder, &b.csvDelay)
if err != nil {
return err
}
@@ -333,13 +364,13 @@ func (b *JusticeKit) decodeV0(r io.Reader) error {
return err
}
b.CommitToLocalSig, err = lnwire.NewSigFromWireECDSA(localSig[:])
b.commitToLocalSig, err = lnwire.NewSigFromWireECDSA(localSig[:])
if err != nil {
return err
}
var (
commitToRemotePubkey PubKey
commitToRemotePubkey pubKey
commitToRemoteSig [64]byte
)
@@ -358,8 +389,8 @@ func (b *JusticeKit) decodeV0(r io.Reader) error {
// Only populate the commit to-remote fields in the decoded blob if a
// valid compressed public key was read from the reader.
if btcec.IsCompressedPubKey(commitToRemotePubkey[:]) {
b.CommitToRemotePubKey = commitToRemotePubkey
b.CommitToRemoteSig, err = lnwire.NewSigFromWireECDSA(
b.commitToRemotePubKey = commitToRemotePubkey
b.commitToRemoteSig, err = lnwire.NewSigFromWireECDSA(
commitToRemoteSig[:],
)
if err != nil {

View File

@@ -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])
}