Merge pull request #7736 from ellemouton/towerInterfaces

watchtower+refactor: CommitmentType and JusticeKit interface
This commit is contained in:
Elle authored and GitHub committed 2024-01-04 16:04:11 +02:00
commit 695bfc8414
16 files changed
+1267 -965

No files matched your search

@@ -155,6 +155,10 @@
* [Add a watchtower tower client * [Add a watchtower tower client
multiplexer](https://github.com/lightningnetwork/lnd/pull/7702) to manage multiplexer](https://github.com/lightningnetwork/lnd/pull/7702) to manage
tower clients of different types. tower clients of different types.
* [Introduce CommitmentType and JusticeKit
interface](https://github.com/lightningnetwork/lnd/pull/7736) to simplify the
code.
## Breaking Changes ## Breaking Changes
## Performance Improvements ## Performance Improvements
+213
View File
@@ -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)
}
}
+239 -467
View File
@@ -1,516 +1,288 @@
package blob package blob
import ( import (
"bytes"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io" "io"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"golang.org/x/crypto/chacha20poly1305"
) )
const ( // JusticeKit is an interface that describes lé Blob of Justice. An
// NonceSize is the length of a chacha20poly1305 nonce, 24 bytes. // implementation of the JusticeKit contains information required to construct
NonceSize = chacha20poly1305.NonceSizeX // 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. // ToRemoteOutputSpendInfo returns the info required to send the
KeySize = chacha20poly1305.KeySize // 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 // HasCommitToRemoteOutput returns true if the kit does include the
// encrypted with chacha20poly1305, which comes from a 16-byte MAC. // information required to sweep the to-remote output.
CiphertextExpansion = 16 HasCommitToRemoteOutput() bool
// V0PlaintextSize is the plaintext size of a version 0 encoded blob. // AddToLocalSig adds the to-local signature to the kit.
// sweep address length: 1 byte AddToLocalSig(sig lnwire.Sig)
// 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 // AddToRemoteSig adds the to-remote signature to the kit.
// encoded in a blob. AddToRemoteSig(sig lnwire.Sig)
MaxSweepAddrSize = 42
)
// 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 // NOTE: This is part of the JusticeKit interface.
// enciphered plaintext: n bytes func (l *legacyJusticeKit) ToLocalOutputSpendInfo() (*txscript.PkScript,
// MAC: 16 bytes wire.TxWitness, error) {
func Size(blobType Type) int {
return NonceSize + PlaintextSize(blobType) + CiphertextExpansion
}
// PlaintextSize returns the size of the encoded-but-unencrypted blob in bytes. revocationPubKey, err := btcec.ParsePubKey(l.revocationPubKey[:])
func PlaintextSize(blobType Type) int { if err != nil {
switch { return nil, nil, err
case blobType.Has(FlagCommitOutputs):
return V0PlaintextSize
default:
return 0
} }
}
var ( localDelayedPubKey, err := btcec.ParsePubKey(l.localDelayPubKey[:])
// byteOrder specifies a big-endian encoding of all integer values. if err != nil {
byteOrder = binary.BigEndian return nil, nil, err
}
// ErrUnknownBlobType signals that we don't understand the requested script, err := input.CommitScriptToSelf(
// blob encoding scheme. l.csvDelay, localDelayedPubKey, revocationPubKey,
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[:],
) )
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
localDelayedPubKey, err := btcec.ParsePubKey( scriptPubKey, err := input.WitnessScriptHash(script)
b.LocalDelayPubKey[:],
)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
return input.CommitScriptToSelf( toLocalSig, err := l.commitToLocalSig.ToSignature()
b.CSVDelay, localDelayedPubKey, revocationPubKey, 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 // ToRemoteOutputSpendInfo returns the info required to spend the to-remote
// revocation clause of the commitment to-local output. // output. It returns the output pub key script, the witness required to spend
// the output and the sequence to apply.
// //
// <revocation-sig> 1 // NOTE: This is part of the JusticeKit interface.
func (b *JusticeKit) CommitToLocalRevokeWitnessStack() ([][]byte, error) { func (l *legacyJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript,
toLocalSig, err := b.CommitToLocalSig.ToSignature() wire.TxWitness, uint32, error) {
if err != nil {
return nil, err if !btcec.IsCompressedPubKey(l.commitToRemotePubKey[:]) {
return nil, nil, 0, ErrNoCommitToRemoteOutput
} }
witnessStack := make([][]byte, 2) toRemoteScript := l.commitToRemotePubKey[:]
witnessStack[0] = append(toLocalSig.Serialize(),
byte(txscript.SigHashAll))
witnessStack[1] = []byte{1}
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 // HasCommitToRemoteOutput returns true if the blob contains a to-remote p2wkh
// pubkey. // 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> // NOTE: This is part of the JusticeKit interface.
func (b *JusticeKit) CommitToRemoteWitnessStack() ([][]byte, error) { func (l *legacyJusticeKit) HasCommitToRemoteOutput() bool {
toRemoteSig, err := b.CommitToRemoteSig.ToSignature() return btcec.IsCompressedPubKey(l.commitToRemotePubKey[:])
if err != nil {
return nil, err
}
witnessStack := make([][]byte, 1)
witnessStack[0] = append(toRemoteSig.Serialize(),
byte(txscript.SigHashAll))
return witnessStack, nil
} }
// Encrypt encodes the blob of justice using encoding version, and then // SweepAddress returns the sweep address to be used on the justice tx
// creates a ciphertext using chacha20poly1305 under the chosen (nonce, key) // output.
// pair.
// //
// NOTE: It is the caller's responsibility to ensure that this method is only // NOTE: This is part of the JusticeKit interface.
// called once for a given (nonce, key) pair. func (l *legacyJusticeKit) SweepAddress() []byte {
func (b *JusticeKit) Encrypt(key BreachKey) ([]byte, error) { return l.sweepAddress
// 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
} }
// Decrypt unenciphers a blob of justice by decrypting the ciphertext using // AddToLocalSig adds the to-local signature to the kit.
// 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.
// //
// blob version 0 plaintext encoding: // NOTE: This is part of the JusticeKit interface.
// func (l *legacyJusticeKit) AddToLocalSig(sig lnwire.Sig) {
// sweep address length: 1 byte l.commitToLocalSig = sig
// 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
} }
// decodeV0 reconstructs a JusticeKit from the io.Reader, using version 0 // AddToRemoteSig adds the to-remote signature to the kit.
// 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: // NOTE: This is part of the JusticeKit interface.
// func (l *legacyJusticeKit) AddToRemoteSig(sig lnwire.Sig) {
// sweep address length: 1 byte l.commitToRemoteSig = sig
// padded sweep address: 42 bytes }
// revocation pubkey: 33 bytes
// local delay pubkey: 33 bytes // PlainTextSize is the size of the encoded-but-unencrypted blob in
// csv delay: 4 bytes // bytes.
// commit to-local revocation sig: 64 bytes //
// commit to-remote pubkey: 33 bytes, maybe blank // NOTE: This is part of the JusticeKit interface.
// commit to-remote sig: 64 bytes, maybe blank func (l *legacyJusticeKit) PlainTextSize() int {
func (b *JusticeKit) decodeV0(r io.Reader) error { return V0PlaintextSize
// Read the sweep address length as a single byte. }
var sweepAddrLen uint8
err := binary.Read(r, byteOrder, &sweepAddrLen) // anchorJusticeKit is an implementation of the JusticeKit interface which can
if err != nil { // be used for backing up commitments of anchor channels. It inherits most of
return err // 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.
// Assert the sweep address length is sane. type anchorJusticeKit struct {
if sweepAddrLen > MaxSweepAddrSize { legacyJusticeKit
return ErrSweepAddressToLong }
}
// A compile-time check to ensure that legacyJusticeKit implements the
// Read padded 42-byte sweep address. // JusticeKit interface.
var sweepAddressBuf [MaxSweepAddrSize]byte var _ JusticeKit = (*anchorJusticeKit)(nil)
_, err = io.ReadFull(r, sweepAddressBuf[:])
if err != nil { // newAnchorJusticeKit constructs a new anchorJusticeKit.
return err func newAnchorJusticeKit(sweepScript []byte,
} breachInfo *lnwallet.BreachRetribution,
withToRemote bool) *anchorJusticeKit {
// Parse sweep address from padded buffer.
b.SweepAddress = make([]byte, sweepAddrLen) legacyKit := newLegacyJusticeKit(sweepScript, breachInfo, withToRemote)
copy(b.SweepAddress, sweepAddressBuf[:])
return &anchorJusticeKit{
// Read 33-byte revocation public key. legacyJusticeKit: *legacyKit,
_, err = io.ReadFull(r, b.RevocationPubKey[:]) }
if err != nil { }
return err
} // ToRemoteOutputSpendInfo returns the info required to send the to-remote
// output. It returns the output pub key script, the witness required to spend
// Read 33-byte local delay public key. // the output and the sequence to apply.
_, err = io.ReadFull(r, b.LocalDelayPubKey[:]) //
if err != nil { // NOTE: This is part of the JusticeKit interface.
return err func (a *anchorJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript,
} wire.TxWitness, uint32, error) {
// Read 4-byte CSV delay. if !btcec.IsCompressedPubKey(a.commitToRemotePubKey[:]) {
err = binary.Read(r, byteOrder, &b.CSVDelay) return nil, nil, 0, ErrNoCommitToRemoteOutput
if err != nil { }
return err
} pk, err := btcec.ParsePubKey(a.commitToRemotePubKey[:])
if err != nil {
// Read 64-byte revocation signature for commit to-local output. return nil, nil, 0, err
var localSig [64]byte }
_, err = io.ReadFull(r, localSig[:])
if err != nil { toRemoteScript, err := input.CommitScriptToRemoteConfirmed(pk)
return err if err != nil {
} return nil, nil, 0, err
}
b.CommitToLocalSig, err = lnwire.NewSigFromWireECDSA(localSig[:])
if err != nil { toRemoteScriptHash, err := input.WitnessScriptHash(toRemoteScript)
return err if err != nil {
} return nil, nil, 0, err
}
var (
commitToRemotePubkey PubKey toRemoteSig, err := a.commitToRemoteSig.ToSignature()
commitToRemoteSig [64]byte if err != nil {
) return nil, nil, 0, err
}
// Read 33-byte commit to-remote public key, which may be discarded.
_, err = io.ReadFull(r, commitToRemotePubkey[:]) witness := make([][]byte, 2)
if err != nil { witness[0] = append(toRemoteSig.Serialize(), byte(txscript.SigHashAll))
return err witness[1] = toRemoteScript
}
pkScript, err := txscript.ParsePkScript(toRemoteScriptHash)
// Read 64-byte commit to-remote signature, which may be discarded. if err != nil {
_, err = io.ReadFull(r, commitToRemoteSig[:]) return nil, nil, 0, err
if err != nil { }
return err
} return &pkScript, witness, 1, nil
// 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
} }
+402
View File
@@ -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
}
+127 -137
View File
@@ -1,30 +1,25 @@
package blob_test package blob
import ( import (
"bytes" "bytes"
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"io" "io"
"reflect"
"testing" "testing"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func makePubKey(i uint64) blob.PubKey { func makePubKey() *btcec.PublicKey {
var pk blob.PubKey priv, _ := btcec.NewPrivateKey()
pk[0] = 0x02 return priv.PubKey()
if i%2 == 1 {
pk[0] |= 0x01
}
binary.BigEndian.PutUint64(pk[1:9], i)
return pk
} }
func makeSig(i int) lnwire.Sig { func makeSig(i int) lnwire.Sig {
@@ -46,15 +41,15 @@ func makeAddr(size int) []byte {
type descriptorTest struct { type descriptorTest struct {
name string name string
encVersion blob.Type encVersion Type
decVersion blob.Type decVersion Type
sweepAddr []byte sweepAddr []byte
revPubKey blob.PubKey revPubKey *btcec.PublicKey
delayPubKey blob.PubKey delayPubKey *btcec.PublicKey
csvDelay uint32 csvDelay uint32
commitToLocalSig lnwire.Sig commitToLocalSig lnwire.Sig
hasCommitToRemote bool hasCommitToRemote bool
commitToRemotePubKey blob.PubKey commitToRemotePubKey *btcec.PublicKey
commitToRemoteSig lnwire.Sig commitToRemoteSig lnwire.Sig
encErr error encErr error
decErr error decErr error
@@ -63,79 +58,79 @@ type descriptorTest struct {
var descriptorTests = []descriptorTest{ var descriptorTests = []descriptorTest{
{ {
name: "to-local only", name: "to-local only",
encVersion: blob.TypeAltruistCommit, encVersion: TypeAltruistCommit,
decVersion: blob.TypeAltruistCommit, decVersion: TypeAltruistCommit,
sweepAddr: makeAddr(22), sweepAddr: makeAddr(22),
revPubKey: makePubKey(0), revPubKey: makePubKey(),
delayPubKey: makePubKey(1), delayPubKey: makePubKey(),
csvDelay: 144, csvDelay: 144,
commitToLocalSig: makeSig(1), commitToLocalSig: makeSig(1),
}, },
{ {
name: "to-local and p2wkh", name: "to-local and p2wkh",
encVersion: blob.TypeRewardCommit, encVersion: TypeRewardCommit,
decVersion: blob.TypeRewardCommit, decVersion: TypeRewardCommit,
sweepAddr: makeAddr(22), sweepAddr: makeAddr(22),
revPubKey: makePubKey(0), revPubKey: makePubKey(),
delayPubKey: makePubKey(1), delayPubKey: makePubKey(),
csvDelay: 144, csvDelay: 144,
commitToLocalSig: makeSig(1), commitToLocalSig: makeSig(1),
hasCommitToRemote: true, hasCommitToRemote: true,
commitToRemotePubKey: makePubKey(2), commitToRemotePubKey: makePubKey(),
commitToRemoteSig: makeSig(2), commitToRemoteSig: makeSig(2),
}, },
{ {
name: "unknown encrypt version", name: "unknown encrypt version",
encVersion: 0, encVersion: 0,
decVersion: blob.TypeAltruistCommit, decVersion: TypeAltruistCommit,
sweepAddr: makeAddr(34), sweepAddr: makeAddr(34),
revPubKey: makePubKey(0), revPubKey: makePubKey(),
delayPubKey: makePubKey(1), delayPubKey: makePubKey(),
csvDelay: 144, csvDelay: 144,
commitToLocalSig: makeSig(1), commitToLocalSig: makeSig(1),
encErr: blob.ErrUnknownBlobType, encErr: ErrUnknownBlobType,
}, },
{ {
name: "unknown decrypt version", name: "unknown decrypt version",
encVersion: blob.TypeAltruistCommit, encVersion: TypeAltruistCommit,
decVersion: 0, decVersion: 0,
sweepAddr: makeAddr(34), sweepAddr: makeAddr(34),
revPubKey: makePubKey(0), revPubKey: makePubKey(),
delayPubKey: makePubKey(1), delayPubKey: makePubKey(),
csvDelay: 144, csvDelay: 144,
commitToLocalSig: makeSig(1), commitToLocalSig: makeSig(1),
decErr: blob.ErrUnknownBlobType, decErr: ErrUnknownBlobType,
}, },
{ {
name: "sweep addr length zero", name: "sweep addr length zero",
encVersion: blob.TypeAltruistCommit, encVersion: TypeAltruistCommit,
decVersion: blob.TypeAltruistCommit, decVersion: TypeAltruistCommit,
sweepAddr: makeAddr(0), sweepAddr: makeAddr(0),
revPubKey: makePubKey(0), revPubKey: makePubKey(),
delayPubKey: makePubKey(1), delayPubKey: makePubKey(),
csvDelay: 144, csvDelay: 144,
commitToLocalSig: makeSig(1), commitToLocalSig: makeSig(1),
}, },
{ {
name: "sweep addr max size", name: "sweep addr max size",
encVersion: blob.TypeAltruistCommit, encVersion: TypeAltruistCommit,
decVersion: blob.TypeAltruistCommit, decVersion: TypeAltruistCommit,
sweepAddr: makeAddr(blob.MaxSweepAddrSize), sweepAddr: makeAddr(MaxSweepAddrSize),
revPubKey: makePubKey(0), revPubKey: makePubKey(),
delayPubKey: makePubKey(1), delayPubKey: makePubKey(),
csvDelay: 144, csvDelay: 144,
commitToLocalSig: makeSig(1), commitToLocalSig: makeSig(1),
}, },
{ {
name: "sweep addr too long", name: "sweep addr too long",
encVersion: blob.TypeAltruistCommit, encVersion: TypeAltruistCommit,
decVersion: blob.TypeAltruistCommit, decVersion: TypeAltruistCommit,
sweepAddr: makeAddr(blob.MaxSweepAddrSize + 1), sweepAddr: makeAddr(MaxSweepAddrSize + 1),
revPubKey: makePubKey(0), revPubKey: makePubKey(),
delayPubKey: makePubKey(1), delayPubKey: makePubKey(),
csvDelay: 144, csvDelay: 144,
commitToLocalSig: makeSig(1), commitToLocalSig: makeSig(1),
encErr: blob.ErrSweepAddressToLong, encErr: ErrSweepAddressToLong,
}, },
} }
@@ -152,30 +147,43 @@ func TestBlobJusticeKitEncryptDecrypt(t *testing.T) {
} }
func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) { func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
boj := &blob.JusticeKit{ commitmentType, err := test.encVersion.CommitmentType(nil)
BlobType: test.encVersion, if err != nil {
SweepAddress: test.sweepAddr, require.ErrorIs(t, err, test.encErr)
RevocationPubKey: test.revPubKey, return
LocalDelayPubKey: test.delayPubKey,
CSVDelay: test.csvDelay,
CommitToLocalSig: test.commitToLocalSig,
CommitToRemotePubKey: test.commitToRemotePubKey,
CommitToRemoteSig: test.commitToRemoteSig,
} }
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 // Generate a random encryption key for the blob. The key is
// sized at 32 byte, as in practice we will be using the remote // sized at 32 byte, as in practice we will be using the remote
// party's commitment txid as the key. // party's commitment txid as the key.
var key blob.BreachKey var key BreachKey
_, err := rand.Read(key[:]) _, err = rand.Read(key[:])
require.NoError(t, err, "unable to generate blob encryption key") require.NoError(t, err, "unable to generate blob encryption key")
// Encrypt the blob plaintext using the generated key and // Encrypt the blob plaintext using the generated key and
// target version for this test. // target version for this test.
ctxt, err := boj.Encrypt(key) ctxt, err := Encrypt(kit, key)
if err != test.encErr { require.ErrorIs(t, err, test.encErr)
t.Fatalf("unable to encrypt blob: %v", err)
} else if test.encErr != nil { if test.encErr != nil {
// If the test expected an encryption failure, we can // If the test expected an encryption failure, we can
// continue to the next test. // continue to the next test.
return return
@@ -183,19 +191,15 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
// Ensure that all encrypted blobs are padded out to the same // Ensure that all encrypted blobs are padded out to the same
// size: 282 bytes for version 0. // size: 282 bytes for version 0.
if len(ctxt) != blob.Size(test.encVersion) { require.Len(t, ctxt, Size(kit))
t.Fatalf("expected blob to have size %d, got %d instead",
blob.Size(test.encVersion), len(ctxt))
}
// Decrypt the encrypted blob, reconstructing the original // Decrypt the encrypted blob, reconstructing the original
// blob plaintext from the decrypted contents. We use the target // blob plaintext from the decrypted contents. We use the target
// decryption version specified by this test case. // decryption version specified by this test case.
boj2, err := blob.Decrypt(key, ctxt, test.decVersion) boj2, err := Decrypt(key, ctxt, test.decVersion)
if err != test.decErr { require.ErrorIs(t, err, test.decErr)
t.Fatalf("unable to decrypt blob: %v", err)
} else if test.decErr != nil { if test.decErr != nil {
// If the test expected an decryption failure, we can // If the test expected an decryption failure, we can
// continue to the next test. // continue to the next test.
return return
@@ -210,15 +214,12 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
// Check that the original blob plaintext matches the // Check that the original blob plaintext matches the
// one reconstructed from the encrypted blob. // one reconstructed from the encrypted blob.
if !reflect.DeepEqual(boj, boj2) { require.Equal(t, kit, boj2)
t.Fatalf("decrypted plaintext does not match original, "+
"want: %v, got %v", boj, boj2)
}
} }
type remoteWitnessTest struct { type remoteWitnessTest struct {
name string name string
blobType blob.Type blobType Type
expWitnessScript func(pk *btcec.PublicKey) []byte expWitnessScript func(pk *btcec.PublicKey) []byte
} }
@@ -229,15 +230,14 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
tests := []remoteWitnessTest{ tests := []remoteWitnessTest{
{ {
name: "legacy commitment", name: "legacy commitment",
blobType: blob.Type(blob.FlagCommitOutputs), blobType: TypeAltruistCommit,
expWitnessScript: func(pk *btcec.PublicKey) []byte { expWitnessScript: func(pk *btcec.PublicKey) []byte {
return pk.SerializeCompressed() return pk.SerializeCompressed()
}, },
}, },
{ {
name: "anchor commitment", name: "anchor commitment",
blobType: blob.Type(blob.FlagCommitOutputs | blobType: TypeAltruistAnchorCommit,
blob.FlagAnchorChannel),
expWitnessScript: func(pk *btcec.PublicKey) []byte { expWitnessScript: func(pk *btcec.PublicKey) []byte {
script, _ := input.CommitScriptToRemoteConfirmed(pk) script, _ := input.CommitScriptToRemoteConfirmed(pk)
return script return script
@@ -257,12 +257,13 @@ func testJusticeKitRemoteWitnessConstruction(
// Generate the to-remote pubkey. // Generate the to-remote pubkey.
toRemotePrivKey, err := btcec.NewPrivateKey() toRemotePrivKey, err := btcec.NewPrivateKey()
require.Nil(t, err) require.NoError(t, err)
// Copy the to-remote pubkey into the format expected by our justice revKey, err := btcec.NewPrivateKey()
// kit. require.NoError(t, err)
var toRemotePubKey blob.PubKey
copy(toRemotePubKey[:], toRemotePrivKey.PubKey().SerializeCompressed()) toLocalKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
// Sign a message using the to-remote private key. The exact message // 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. // doesn't matter as we won't be validating the signature's validity.
@@ -273,26 +274,29 @@ func testJusticeKitRemoteWitnessConstruction(
commitToRemoteSig, err := lnwire.NewSigFromSignature(rawToRemoteSig) commitToRemoteSig, err := lnwire.NewSigFromSignature(rawToRemoteSig)
require.Nil(t, err) require.Nil(t, err)
// Populate the justice kit fields relevant to the to-remote output. commitType, err := test.blobType.CommitmentType(nil)
justiceKit := &blob.JusticeKit{ require.NoError(t, err)
BlobType: test.blobType,
CommitToRemotePubKey: toRemotePubKey, breachInfo := &lnwallet.BreachRetribution{
CommitToRemoteSig: commitToRemoteSig, 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 // Now, compute the to-remote witness script returned by the justice
// kit. // kit.
toRemoteScript, err := justiceKit.CommitToRemoteWitnessScript() _, witness, _, err := justiceKit.ToRemoteOutputSpendInfo()
require.Nil(t, err) require.NoError(t, err)
// Assert this is exactly the to-remote, compressed pubkey. // Assert this is exactly the to-remote, compressed pubkey.
expToRemoteScript := test.expWitnessScript(toRemotePrivKey.PubKey()) expToRemoteScript := test.expWitnessScript(toRemotePrivKey.PubKey())
require.Equal(t, expToRemoteScript, toRemoteScript) require.Equal(t, expToRemoteScript, witness[1])
// 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)
// Compute the expected first element, by appending a sighash all byte // Compute the expected first element, by appending a sighash all byte
// to our raw DER-encoded signature. // to our raw DER-encoded signature.
@@ -301,19 +305,10 @@ func testJusticeKitRemoteWitnessConstruction(
) )
// Assert that the expected witness stack is returned. // Assert that the expected witness stack is returned.
expWitnessStack := [][]byte{ expWitnessStack := wire.TxWitness{
rawToRemoteSigWithSigHash, rawToRemoteSigWithSigHash,
} }
require.Equal(t, expWitnessStack, toRemoteWitnessStack) require.Equal(t, expWitnessStack, witness[:1])
// 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)
} }
// TestJusticeKitToLocalWitnessConstruction tests that a JusticeKit returns the // TestJusticeKitToLocalWitnessConstruction tests that a JusticeKit returns the
@@ -324,18 +319,10 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) {
// Generate the revocation and delay private keys. // Generate the revocation and delay private keys.
revPrivKey, err := btcec.NewPrivateKey() revPrivKey, err := btcec.NewPrivateKey()
require.Nil(t, err) require.NoError(t, err)
delayPrivKey, err := btcec.NewPrivateKey() delayPrivKey, err := btcec.NewPrivateKey()
require.Nil(t, err) require.NoError(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())
// Sign a message using the revocation private key. The exact message // Sign a message using the revocation private key. The exact message
// doesn't matter as we won't be validating the signature's validity. // 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. // Convert the DER-encoded signature into a fixed-size sig.
commitToLocalSig, err := lnwire.NewSigFromSignature(rawRevSig) 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. commitType, err := TypeAltruistCommit.CommitmentType(nil)
justiceKit := &blob.JusticeKit{ require.NoError(t, err)
CSVDelay: csvDelay,
RevocationPubKey: revPubKey, breachInfo := &lnwallet.BreachRetribution{
LocalDelayPubKey: delayPubKey, RemoteDelay: csvDelay,
CommitToLocalSig: commitToLocalSig, 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 // Compute the expected to-local script, which is a function of the CSV
// delay, revocation pubkey and delay pubkey. // delay, revocation pubkey and delay pubkey.
expToLocalScript, err := input.CommitScriptToSelf( expToLocalScript, err := input.CommitScriptToSelf(
csvDelay, delayPrivKey.PubKey(), revPrivKey.PubKey(), 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. // Compute the to-local script that is returned by the justice kit.
toLocalScript, err := justiceKit.CommitToLocalWitnessScript() _, witness, err := justiceKit.ToLocalOutputSpendInfo()
require.Nil(t, err) require.NoError(t, err)
// Assert that the expected to-local script matches the actual script. // Assert that the expected to-local script matches the actual script.
require.Equal(t, expToLocalScript, toLocalScript) require.Equal(t, expToLocalScript, witness[2])
// Next, compute the to-local witness stack returned by the justice kit.
toLocalWitnessStack, err := justiceKit.CommitToLocalRevokeWitnessStack()
require.Nil(t, err)
// Compute the expected signature in the bottom element of the stack, by // Compute the expected signature in the bottom element of the stack, by
// appending a sighash all flag to the raw DER signature. // 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. // Finally, validate against our expected witness stack.
expWitnessStack := [][]byte{ expWitnessStack := wire.TxWitness{
rawRevSigWithSigHash, rawRevSigWithSigHash,
{1}, {1},
} }
require.Equal(t, expWitnessStack, toLocalWitnessStack) require.Equal(t, expWitnessStack, witness[:2])
} }
+23
View File
@@ -3,6 +3,8 @@ package blob
import ( import (
"fmt" "fmt"
"strings" "strings"
"github.com/lightningnetwork/lnd/channeldb"
) )
// Flag represents a specify option that can be present in a Type. // 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. // Has returns true if the Type has the passed flag enabled.
func (t Type) Has(flag Flag) bool { func (t Type) Has(flag Flag) bool {
return Flag(t)&flag == flag return Flag(t)&flag == flag
+36 -88
View File
@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/txsort" "github.com/btcsuite/btcd/btcutil/txsort"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
@@ -41,7 +40,7 @@ type JusticeDescriptor struct {
// JusticeKit contains the decrypted blob and information required to // JusticeKit contains the decrypted blob and information required to
// construct the transaction scripts and witnesses. // construct the transaction scripts and witnesses.
JusticeKit *blob.JusticeKit JusticeKit blob.JusticeKit
} }
// breachedInput contains the required information to construct and spend // 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 // commitToLocalInput extracts the information required to spend the commit
// to-local output. // to-local output.
func (p *JusticeDescriptor) commitToLocalInput() (*breachedInput, error) { func (p *JusticeDescriptor) commitToLocalInput() (*breachedInput, error) {
// Retrieve the to-local witness script from the justice kit. kit := p.JusticeKit
toLocalScript, err := p.JusticeKit.CommitToLocalWitnessScript()
if err != nil {
return nil, err
}
// Compute the witness script hash, which will be used to locate the // Retrieve the to-local output script and witness from the justice kit.
// input on the breaching commitment transaction. toLocalPkScript, witness, err := kit.ToLocalOutputSpendInfo()
toLocalWitnessHash, err := input.WitnessScriptHash(toLocalScript)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Locate the to-local output on the breaching commitment transaction. // Locate the to-local output on the breaching commitment transaction.
toLocalIndex, toLocalTxOut, err := findTxOutByPkScript( toLocalIndex, toLocalTxOut, err := findTxOutByPkScript(
p.BreachedCommitTx, toLocalWitnessHash, p.BreachedCommitTx, toLocalPkScript,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -84,63 +78,28 @@ func (p *JusticeDescriptor) commitToLocalInput() (*breachedInput, error) {
Index: toLocalIndex, 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{ return &breachedInput{
txOut: toLocalTxOut, txOut: toLocalTxOut,
outPoint: toLocalOutPoint, outPoint: toLocalOutPoint,
witness: buildWitness(witnessStack, toLocalScript), witness: witness,
}, nil }, nil
} }
// commitToRemoteInput extracts the information required to spend the commit // commitToRemoteInput extracts the information required to spend the commit
// to-remote output. // to-remote output.
func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) { func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) {
// Retrieve the to-remote witness script from the justice kit. kit := p.JusticeKit
toRemoteScript, err := p.JusticeKit.CommitToRemoteWitnessScript()
// Retrieve the to-remote output script, witness script and sequence
// from the justice kit.
toRemotePkScript, witness, seq, err := kit.ToRemoteOutputSpendInfo()
if err != nil { if err != nil {
return nil, err 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. // Locate the to-remote output on the breaching commitment transaction.
toRemoteIndex, toRemoteTxOut, err := findTxOutByPkScript( toRemoteIndex, toRemoteTxOut, err := findTxOutByPkScript(
p.BreachedCommitTx, toRemoteScriptHash, p.BreachedCommitTx, toRemotePkScript,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -153,18 +112,11 @@ func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) {
Index: toRemoteIndex, 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{ return &breachedInput{
txOut: toRemoteTxOut, txOut: toRemoteTxOut,
outPoint: toRemoteOutPoint, outPoint: toRemoteOutPoint,
witness: buildWitness(witnessStack, toRemoteScript), witness: witness,
sequence: toRemoteSequence, sequence: seq,
}, nil }, nil
} }
@@ -193,7 +145,7 @@ func (p *JusticeDescriptor) assembleJusticeTxn(txWeight int64,
// reward sweep, there will be two outputs, one of which pays back to // reward sweep, there will be two outputs, one of which pays back to
// the victim while the other gives a cut to the tower. // the victim while the other gives a cut to the tower.
outputs, err := p.SessionInfo.Policy.ComputeJusticeTxOuts( outputs, err := p.SessionInfo.Policy.ComputeJusticeTxOuts(
totalAmt, txWeight, p.JusticeKit.SweepAddress[:], totalAmt, txWeight, p.JusticeKit.SweepAddress(),
p.SessionInfo.RewardAddress, p.SessionInfo.RewardAddress,
) )
if err != nil { if err != nil {
@@ -264,9 +216,14 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) {
weightEstimate input.TxWeightEstimator 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 // Add the sweep address's contribution, depending on whether it is a
// p2wkh or p2wsh output. // p2wkh or p2wsh output.
switch len(p.JusticeKit.SweepAddress) { switch len(p.JusticeKit.SweepAddress()) {
case input.P2WPKHSize: case input.P2WPKHSize:
weightEstimate.AddP2WKHOutput() weightEstimate.AddP2WKHOutput()
@@ -290,16 +247,13 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) {
return nil, err return nil, err
} }
// An older ToLocalPenaltyWitnessSize constant used to underestimate the // Get the weight for the to-local witness and add that to the
// size by one byte. The diferrence in weight can cause different output // estimator.
// values on the sweep transaction, so we mimic the original bug to toLocalWitnessSize, err := commitmentType.ToLocalWitnessSize()
// avoid invalidating signatures by older clients. For anchor channels if err != nil {
// we correct this and use the correct witness size. return nil, err
if p.JusticeKit.BlobType.IsAnchorChannel() {
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize)
} else {
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1)
} }
weightEstimate.AddWitnessInput(toLocalWitnessSize)
sweepInputs = append(sweepInputs, toLocalInput) 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", log.Debugf("Found to remote witness output=%#v, stack=%v",
toRemoteInput.txOut, toRemoteInput.witness) toRemoteInput.txOut, toRemoteInput.witness)
if p.JusticeKit.BlobType.IsAnchorChannel() { // Get the weight for the to-remote witness and add that to the
weightEstimate.AddWitnessInput(input.ToRemoteConfirmedWitnessSize) // estimator.
} else { toRemoteWitnessSize, err := commitmentType.ToRemoteWitnessSize()
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) if err != nil {
return nil, err
} }
weightEstimate.AddWitnessInput(toRemoteWitnessSize)
} }
// TODO(conner): sweep htlc outputs // 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. // NOTE: The search stops after the first match is found.
func findTxOutByPkScript(txn *wire.MsgTx, 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 { if !found {
return 0, nil, ErrOutputNotFound return 0, nil, ErrOutputNotFound
} }
@@ -349,15 +306,6 @@ func findTxOutByPkScript(txn *wire.MsgTx,
return index, txn.TxOut[index], nil 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 // prevOutFetcher returns a txscript.MultiPrevOutFetcher for the given set
// of inputs. // of inputs.
func prevOutFetcher(inputs []*breachedInput) (*txscript.MultiPrevOutFetcher, func prevOutFetcher(inputs []*breachedInput) (*txscript.MultiPrevOutFetcher,
+42 -42
View File
@@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/lightningnetwork/lnd/watchtower/lookout" "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. // Parse the key pairs for all keys used in the test.
revSK, revPK := btcec.PrivKeyFromBytes( revSK, revPK := btcec.PrivKeyFromBytes(revPrivBytes)
revPrivBytes, _, toLocalPK := btcec.PrivKeyFromBytes(toLocalPrivBytes)
) toRemoteSK, toRemotePK := btcec.PrivKeyFromBytes(toRemotePrivBytes)
_, toLocalPK := btcec.PrivKeyFromBytes(
toLocalPrivBytes, // Get the commitment type.
) commitType, err := blobType.CommitmentType(nil)
toRemoteSK, toRemotePK := btcec.PrivKeyFromBytes( require.NoError(t, err)
toRemotePrivBytes,
)
// Create the signer, and add the revocation and to-remote privkeys. // Create the signer, and add the revocation and to-remote privkeys.
signer := wtmock.NewMockSigner() signer := wtmock.NewMockSigner()
@@ -113,11 +112,11 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
toLocalScript, err := input.CommitScriptToSelf( toLocalScript, err := input.CommitScriptToSelf(
csvDelay, toLocalPK, revPK, csvDelay, toLocalPK, revPK,
) )
require.Nil(t, err) require.NoError(t, err)
// Compute the to-local witness script hash. // Compute the to-local witness script hash.
toLocalScriptHash, err := input.WitnessScriptHash(toLocalScript) toLocalScriptHash, err := input.WitnessScriptHash(toLocalScript)
require.Nil(t, err) require.NoError(t, err)
// Compute the to-remote redeem script, witness script hash, and // Compute the to-remote redeem script, witness script hash, and
// sequence numbers. // sequence numbers.
@@ -147,12 +146,12 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
toRemoteRedeemScript, err = input.CommitScriptToRemoteConfirmed( toRemoteRedeemScript, err = input.CommitScriptToRemoteConfirmed(
toRemotePK, toRemotePK,
) )
require.Nil(t, err) require.NoError(t, err)
toRemoteScriptHash, err = input.WitnessScriptHash( toRemoteScriptHash, err = input.WitnessScriptHash(
toRemoteRedeemScript, toRemoteRedeemScript,
) )
require.Nil(t, err) require.NoError(t, err)
// As it should be. // As it should be.
toRemoteSigningScript = toRemoteRedeemScript toRemoteSigningScript = toRemoteRedeemScript
@@ -161,7 +160,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
toRemoteScriptHash, err = input.CommitScriptUnencumbered( toRemoteScriptHash, err = input.CommitScriptUnencumbered(
toRemotePK, toRemotePK,
) )
require.Nil(t, err) require.NoError(t, err)
// NOTE: This is the _pkscript_. // NOTE: This is the _pkscript_.
toRemoteSigningScript = toRemoteScriptHash toRemoteSigningScript = toRemoteScriptHash
@@ -188,26 +187,24 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
// Compute the weight estimate for our justice transaction. // Compute the weight estimate for our justice transaction.
var weightEstimate input.TxWeightEstimator var weightEstimate input.TxWeightEstimator
// An older ToLocalPenaltyWitnessSize constant used to underestimate the // Add the local witness size to the weight estimator.
// size by one byte. The diferrence in weight can cause different output toLocalWitnessSize, err := commitType.ToLocalWitnessSize()
// values on the sweep transaction, so we mimic the original bug and require.NoError(t, err)
// create signatures using the original weight estimate. For anchor weightEstimate.AddWitnessInput(toLocalWitnessSize)
// channels we fix this and use the correct witness size.
if isAnchorChannel {
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize)
} else {
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1)
}
if isAnchorChannel { // Add the remote witness size to the weight estimator.
weightEstimate.AddWitnessInput(input.ToRemoteConfirmedWitnessSize) toRemoteWitnessSize, err := commitType.ToRemoteWitnessSize()
} else { require.NoError(t, err)
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) weightEstimate.AddWitnessInput(toRemoteWitnessSize)
}
// Add the sweep output to the weight estimator.
weightEstimate.AddP2WKHOutput() weightEstimate.AddP2WKHOutput()
// Add the reward output to the weight estimator.
if blobType.Has(blob.FlagReward) { if blobType.Has(blob.FlagReward) {
weightEstimate.AddP2WKHOutput() weightEstimate.AddP2WKHOutput()
} }
txWeight := weightEstimate.Weight() txWeight := weightEstimate.Weight()
// Create a session info so that simulate agreement of the sweep // 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), RewardAddress: makeAddrSlice(22),
} }
// Begin to assemble the justice kit, starting with the sweep address, breachInfo := &lnwallet.BreachRetribution{
// pubkeys, and csv delay. RemoteDelay: csvDelay,
justiceKit := &blob.JusticeKit{ KeyRing: &lnwallet.CommitmentKeyRing{
BlobType: blobType, ToLocalKey: toLocalPK,
SweepAddress: makeAddrSlice(22), ToRemoteKey: toRemotePK,
CSVDelay: csvDelay, RevocationKey: revPK,
},
} }
copy(justiceKit.RevocationPubKey[:], revPK.SerializeCompressed())
copy(justiceKit.LocalDelayPubKey[:], toLocalPK.SerializeCompressed()) justiceKit, err := commitType.NewJusticeKit(
copy(justiceKit.CommitToRemotePubKey[:], toRemotePK.SerializeCompressed()) makeAddrSlice(22), breachInfo, true,
)
require.NoError(t, err)
// Create a transaction spending from the outputs of the breach // Create a transaction spending from the outputs of the breach
// transaction created earlier. The inputs are always ordered w/ // 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( outputs, err := policy.ComputeJusticeTxOuts(
totalAmount, int64(txWeight), justiceKit.SweepAddress, totalAmount, int64(txWeight), justiceKit.SweepAddress(),
sessionInfo.RewardAddress, sessionInfo.RewardAddress,
) )
require.Nil(t, err) require.NoError(t, err)
// Attach the txouts and BIP69 sort the resulting transaction. // Attach the txouts and BIP69 sort the resulting transaction.
justiceTxn.TxOut = outputs justiceTxn.TxOut = outputs
@@ -320,8 +320,8 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
require.Nil(t, err) require.Nil(t, err)
// Complete our justice kit by copying the signatures into the payload. // Complete our justice kit by copying the signatures into the payload.
justiceKit.CommitToLocalSig = toLocalSig justiceKit.AddToLocalSig(toLocalSig)
justiceKit.CommitToRemoteSig = toRemoteSig justiceKit.AddToRemoteSig(toRemoteSig)
justiceDesc := &lookout.JusticeDescriptor{ justiceDesc := &lookout.JusticeDescriptor{
BreachedCommitTx: breachTxn, BreachedCommitTx: breachTxn,
+38 -20
View File
@@ -8,8 +8,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/lightningnetwork/lnd/watchtower/lookout" "github.com/lightningnetwork/lnd/watchtower/lookout"
@@ -30,10 +32,9 @@ func (p *mockPunisher) Punish(
return nil return nil
} }
func makeArray32(i uint64) [32]byte { func makeRandomPK() *btcec.PublicKey {
var arr [32]byte pk, _ := btcec.NewPrivateKey()
binary.BigEndian.PutUint64(arr[:], i) return pk.PubKey()
return arr
} }
func makeArray33(i uint64) [33]byte { func makeArray33(i uint64) [33]byte {
@@ -142,32 +143,49 @@ func TestLookoutBreachMatching(t *testing.T) {
// Construct a justice kit for each possible breach transaction. // Construct a justice kit for each possible breach transaction.
blobType := blob.FlagCommitOutputs.Type() blobType := blob.FlagCommitOutputs.Type()
blob1 := &blob.JusticeKit{ breachInfo1 := &lnwallet.BreachRetribution{
BlobType: blobType, RemoteDelay: 144,
SweepAddress: makeAddrSlice(22), KeyRing: &lnwallet.CommitmentKeyRing{
RevocationPubKey: makePubKey(1), ToLocalKey: makeRandomPK(),
LocalDelayPubKey: makePubKey(1), RevocationKey: makeRandomPK(),
CSVDelay: 144, },
CommitToLocalSig: makeTestSig(1),
} }
blob2 := &blob.JusticeKit{ commitment1, err := blobType.CommitmentType(nil)
BlobType: blobType, require.NoError(t, err)
SweepAddress: makeAddrSlice(22),
RevocationPubKey: makePubKey(2), blob1, err := commitment1.NewJusticeKit(
LocalDelayPubKey: makePubKey(2), makeAddrSlice(22), breachInfo1, false,
CSVDelay: 144, )
CommitToLocalSig: makeTestSig(2), 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) key1 := blob.NewBreachKeyFromHash(&hash1)
key2 := blob.NewBreachKeyFromHash(&hash2) key2 := blob.NewBreachKeyFromHash(&hash2)
// Encrypt the first justice kit under breach key one. // 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") require.NoError(t, err, "unable to encrypt sweep detail 1")
// Encrypt the second justice kit under breach key two. // 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") require.NoError(t, err, "unable to encrypt sweep detail 2")
// Add both state updates to the tower's database. // Add both state updates to the tower's database.
+48 -109
View File
@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/txsort" "github.com/btcsuite/btcd/btcutil/txsort"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
@@ -12,7 +11,6 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/lightningnetwork/lnd/watchtower/wtdb" "github.com/lightningnetwork/lnd/watchtower/wtdb"
) )
@@ -37,8 +35,9 @@ import (
// necessary components are stripped out and encrypted before being sent to // necessary components are stripped out and encrypted before being sent to
// the tower in a StateUpdate. // the tower in a StateUpdate.
type backupTask struct { type backupTask struct {
id wtdb.BackupID id wtdb.BackupID
breachInfo *lnwallet.BreachRetribution breachInfo *lnwallet.BreachRetribution
commitmentType blob.CommitmentType
// state-dependent variables // state-dependent variables
@@ -127,6 +126,11 @@ func (t *backupTask) bindSession(session *wtdb.ClientSessionBody,
return err return err
} }
commitType, err := session.Policy.BlobType.CommitmentType(&chanType)
if err != nil {
return err
}
// Parse the non-dust outputs from the breach transaction, // Parse the non-dust outputs from the breach transaction,
// simultaneously computing the total amount contained in the inputs // simultaneously computing the total amount contained in the inputs
// present. We can't compute the exact output values at this time // 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 // to that output as local, though relative to their commitment, it is
// paying to-the-remote party (which is us). // paying to-the-remote party (which is us).
if breachInfo.RemoteOutputSignDesc != nil { if breachInfo.RemoteOutputSignDesc != nil {
toLocalInput = input.NewBaseInput( toLocalInput, err = commitType.ToLocalInput(breachInfo)
&breachInfo.RemoteOutpoint, if err != nil {
input.CommitmentRevoke, return err
breachInfo.RemoteOutputSignDesc, }
0,
)
totalAmt += breachInfo.RemoteOutputSignDesc.Output.Value totalAmt += breachInfo.RemoteOutputSignDesc.Output.Value
} }
if breachInfo.LocalOutputSignDesc != nil { if breachInfo.LocalOutputSignDesc != nil {
var witnessType input.WitnessType toRemoteInput, err = commitType.ToRemoteInput(breachInfo)
switch { if err != nil {
case chanType.HasAnchors(): return err
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,
)
} }
totalAmt += breachInfo.LocalOutputSignDesc.Output.Value totalAmt += breachInfo.LocalOutputSignDesc.Output.Value
} }
t.commitmentType = commitType
t.breachInfo = breachInfo t.breachInfo = breachInfo
t.toLocalInput = toLocalInput t.toLocalInput = toLocalInput
t.toRemoteInput = toRemoteInput 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 // Next, add the contribution from the inputs that are present on this
// breach transaction. // breach transaction.
if t.toLocalInput != nil { if t.toLocalInput != nil {
// An older ToLocalPenaltyWitnessSize constant used to toLocalWitnessSize, err := commitType.ToLocalWitnessSize()
// underestimate the size by one byte. The diferrence in weight if err != nil {
// can cause different output values on the sweep transaction, return err
// 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,
)
} }
weightEstimate.AddWitnessInput(toLocalWitnessSize)
} }
if t.toRemoteInput != nil { if t.toRemoteInput != nil {
// Legacy channels (both tweaked and non-tweaked) spend from toRemoteWitnessSize, err := commitType.ToRemoteWitnessSize()
// P2WKH output. Anchor channels spend a to-remote confirmed if err != nil {
// P2WSH output. return err
if chanType.HasAnchors() {
weightEstimate.AddWitnessInput(
input.ToRemoteConfirmedWitnessSize,
)
} else {
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
} }
weightEstimate.AddWitnessInput(toRemoteWitnessSize)
} }
// All justice transactions will either use segwit v0 (p2wkh + p2wsh) // All justice transactions will either use segwit v0 (p2wkh + p2wsh)
@@ -281,25 +246,11 @@ func (t *backupTask) craftSessionPayload(
var hint blob.BreachHint var hint blob.BreachHint
// First, copy over the sweep pkscript, the pubkeys used to derive the justiceKit, err := t.commitmentType.NewJusticeKit(
// to-local script, and the remote CSV delay. t.sweepPkScript, t.breachInfo, t.toRemoteInput != nil,
keyRing := t.breachInfo.KeyRing )
justiceKit := &blob.JusticeKit{ if err != nil {
BlobType: t.blobType, return hint, nil, err
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,
)
} }
// Now, begin construction of the justice transaction. We'll start with // 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 // Now, iterate through the list of inputs that were initially added to
// the transaction and store the computed witness within the justice // the transaction and store the computed witness within the justice
// kit. // kit.
commitType := t.commitmentType
for _, inp := range inputs { for _, inp := range inputs {
// Lookup the input's new post-sort position. // Lookup the input's new post-sort position.
i := inputIndex[*inp.OutPoint()] i := inputIndex[*inp.OutPoint()]
@@ -361,17 +313,17 @@ func (t *backupTask) craftSessionPayload(
return hint, nil, err return hint, nil, err
} }
// Parse the DER-encoded signature from the first position of signature, err := commitType.ParseRawSig(inputScript.Witness)
// the resulting witness. We trim an extra byte to remove the if err != nil {
// sighash flag. return hint, nil, err
witness := inputScript.Witness }
rawSignature := witness[0][:len(witness[0])-1]
// Re-encode the DER signature into a fixed-size 64 byte toLocalWitnessType, err := commitType.ToLocalWitnessType()
// signature. if err != nil {
signature, err := lnwire.NewSigFromECDSARawSignature( return hint, nil, err
rawSignature, }
)
toRemoteWitnessType, err := commitType.ToRemoteWitnessType()
if err != nil { if err != nil {
return hint, nil, err return hint, nil, err
} }
@@ -380,15 +332,10 @@ func (t *backupTask) craftSessionPayload(
// using the input's witness type to select the appropriate // using the input's witness type to select the appropriate
// field // field
switch inp.WitnessType() { switch inp.WitnessType() {
case input.CommitmentRevoke: case toLocalWitnessType:
justiceKit.CommitToLocalSig = signature justiceKit.AddToLocalSig(signature)
case toRemoteWitnessType:
case input.CommitSpendNoDelayTweakless: justiceKit.AddToRemoteSig(signature)
fallthrough
case input.CommitmentNoDelay:
fallthrough
case input.CommitmentToRemoteConfirmed:
justiceKit.CommitToRemoteSig = signature
default: default:
return hint, nil, fmt.Errorf("invalid witness type: %v", return hint, nil, fmt.Errorf("invalid witness type: %v",
inp.WitnessType()) inp.WitnessType())
@@ -403,18 +350,10 @@ func (t *backupTask) craftSessionPayload(
// Then, we'll encrypt the computed justice kit using the full breach // Then, we'll encrypt the computed justice kit using the full breach
// transaction id, which will allow the tower to recover the contents // transaction id, which will allow the tower to recover the contents
// after the transaction is seen in the chain or mempool. // after the transaction is seen in the chain or mempool.
encBlob, err := justiceKit.Encrypt(key) encBlob, err := blob.Encrypt(justiceKit, key)
if err != nil { if err != nil {
return hint, nil, err return hint, nil, err
} }
return hint, encBlob, nil 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
}
@@ -1,7 +1,7 @@
package wtclient package wtclient
import ( import (
"bytes" "encoding/binary"
"testing" "testing"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
@@ -25,8 +25,7 @@ import (
const csvDelay uint32 = 144 const csvDelay uint32 = 144
var ( var (
zeroPK [33]byte zeroSig = makeSig(0)
zeroSig [64]byte
revPrivBytes = []byte{ revPrivBytes = []byte{
0x8f, 0x4b, 0x51, 0x83, 0xa9, 0x34, 0xbd, 0x5f, 0x8f, 0x4b, 0x51, 0x83, 0xa9, 0x34, 0xbd, 0x5f,
@@ -65,6 +64,7 @@ type backupTaskTest struct {
expSweepScript []byte expSweepScript []byte
signer input.Signer signer input.Signer
chanType channeldb.ChannelType chanType channeldb.ChannelType
commitType blob.CommitmentType
} }
// genTaskTest creates a instance of a backupTaskTest using the passed // 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 // corresponding BreachInfo, as well as setting the wtpolicy.Policy of the given
// session. // session.
func genTaskTest( func genTaskTest(
t *testing.T,
name string, name string,
stateNum uint64, stateNum uint64,
toLocalAmt int64, toLocalAmt int64,
@@ -91,15 +92,12 @@ func genTaskTest(
} }
// Parse the key pairs for all keys used in the test. // Parse the key pairs for all keys used in the test.
revSK, revPK := btcec.PrivKeyFromBytes( revSK, revPK := btcec.PrivKeyFromBytes(revPrivBytes)
revPrivBytes, _, toLocalPK := btcec.PrivKeyFromBytes(toLocalPrivBytes)
) toRemoteSK, toRemotePK := btcec.PrivKeyFromBytes(toRemotePrivBytes)
_, toLocalPK := btcec.PrivKeyFromBytes(
toLocalPrivBytes, commitType, err := blobType.CommitmentType(&chanType)
) require.NoError(t, err)
toRemoteSK, toRemotePK := btcec.PrivKeyFromBytes(
toRemotePrivBytes,
)
// Create the signer, and add the revocation and to-remote privkeys. // Create the signer, and add the revocation and to-remote privkeys.
signer := wtmock.NewMockSigner() signer := wtmock.NewMockSigner()
@@ -174,12 +172,9 @@ func genTaskTest(
Hash: txid, Hash: txid,
Index: index, Index: index,
} }
toLocalInput = input.NewBaseInput( toLocalInput, err = commitType.ToLocalInput(breachInfo)
&breachInfo.RemoteOutpoint, require.NoError(t, err)
input.CommitmentRevoke,
breachInfo.RemoteOutputSignDesc,
0,
)
index++ index++
} }
if toRemoteAmt > 0 { if toRemoteAmt > 0 {
@@ -188,31 +183,8 @@ func genTaskTest(
Index: index, Index: index,
} }
var witnessType input.WitnessType toRemoteInput, err = commitType.ToRemoteInput(breachInfo)
switch { require.NoError(t, err)
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,
)
}
} }
return backupTaskTest{ return backupTaskTest{
@@ -238,6 +210,7 @@ func genTaskTest(
expSweepScript: sweepAddr, expSweepScript: sweepAddr,
signer: signer, signer: signer,
chanType: chanType, chanType: chanType,
commitType: commitType,
} }
} }
@@ -312,6 +285,7 @@ func TestBackupTask(t *testing.T) {
backupTaskTests = append(backupTaskTests, []backupTaskTest{ backupTaskTests = append(backupTaskTests, []backupTaskTest{
genTaskTest( genTaskTest(
t,
"commit no-reward, both outputs", "commit no-reward, both outputs",
100, // stateNum 100, // stateNum
200000, // toLocalAmt 200000, // toLocalAmt
@@ -325,6 +299,7 @@ func TestBackupTask(t *testing.T) {
chanType, chanType,
), ),
genTaskTest( genTaskTest(
t,
"commit no-reward, to-local output only", "commit no-reward, to-local output only",
1000, // stateNum 1000, // stateNum
200000, // toLocalAmt 200000, // toLocalAmt
@@ -338,6 +313,7 @@ func TestBackupTask(t *testing.T) {
chanType, chanType,
), ),
genTaskTest( genTaskTest(
t,
"commit no-reward, to-remote output only", "commit no-reward, to-remote output only",
1, // stateNum 1, // stateNum
0, // toLocalAmt 0, // toLocalAmt
@@ -351,6 +327,7 @@ func TestBackupTask(t *testing.T) {
chanType, chanType,
), ),
genTaskTest( genTaskTest(
t,
"commit no-reward, to-remote output only, creates dust", "commit no-reward, to-remote output only, creates dust",
1, // stateNum 1, // stateNum
0, // toLocalAmt 0, // toLocalAmt
@@ -364,6 +341,7 @@ func TestBackupTask(t *testing.T) {
chanType, chanType,
), ),
genTaskTest( genTaskTest(
t,
"commit no-reward, no outputs, fee rate exceeds inputs", "commit no-reward, no outputs, fee rate exceeds inputs",
300, // stateNum 300, // stateNum
0, // toLocalAmt 0, // toLocalAmt
@@ -377,6 +355,7 @@ func TestBackupTask(t *testing.T) {
chanType, chanType,
), ),
genTaskTest( genTaskTest(
t,
"commit no-reward, no outputs, fee rate of 0 creates dust", "commit no-reward, no outputs, fee rate of 0 creates dust",
300, // stateNum 300, // stateNum
0, // toLocalAmt 0, // toLocalAmt
@@ -390,6 +369,7 @@ func TestBackupTask(t *testing.T) {
chanType, chanType,
), ),
genTaskTest( genTaskTest(
t,
"commit reward, both outputs", "commit reward, both outputs",
100, // stateNum 100, // stateNum
200000, // toLocalAmt 200000, // toLocalAmt
@@ -403,6 +383,7 @@ func TestBackupTask(t *testing.T) {
chanType, chanType,
), ),
genTaskTest( genTaskTest(
t,
"commit reward, to-local output only", "commit reward, to-local output only",
1000, // stateNum 1000, // stateNum
200000, // toLocalAmt 200000, // toLocalAmt
@@ -416,6 +397,7 @@ func TestBackupTask(t *testing.T) {
chanType, chanType,
), ),
genTaskTest( genTaskTest(
t,
"commit reward, to-remote output only", "commit reward, to-remote output only",
1, // stateNum 1, // stateNum
0, // toLocalAmt 0, // toLocalAmt
@@ -429,6 +411,7 @@ func TestBackupTask(t *testing.T) {
chanType, chanType,
), ),
genTaskTest( genTaskTest(
t,
"commit reward, to-remote output only, creates dust", "commit reward, to-remote output only, creates dust",
1, // stateNum 1, // stateNum
0, // toLocalAmt 0, // toLocalAmt
@@ -442,6 +425,7 @@ func TestBackupTask(t *testing.T) {
chanType, chanType,
), ),
genTaskTest( genTaskTest(
t,
"commit reward, no outputs, fee rate exceeds inputs", "commit reward, no outputs, fee rate exceeds inputs",
300, // stateNum 300, // stateNum
0, // toLocalAmt 0, // toLocalAmt
@@ -455,6 +439,7 @@ func TestBackupTask(t *testing.T) {
chanType, chanType,
), ),
genTaskTest( genTaskTest(
t,
"commit reward, no outputs, fee rate of 0 creates dust", "commit reward, no outputs, fee rate of 0 creates dust",
300, // stateNum 300, // stateNum
0, // toLocalAmt 0, // toLocalAmt
@@ -583,60 +568,35 @@ func testBackupTask(t *testing.T, test backupTaskTest) {
require.NoError(t, err, "unable to decrypt blob") require.NoError(t, err, "unable to decrypt blob")
keyRing := test.breachInfo.KeyRing keyRing := test.breachInfo.KeyRing
expToLocalPK := keyRing.ToLocalKey.SerializeCompressed() expToLocalPK := keyRing.ToLocalKey
expRevPK := keyRing.RevocationKey.SerializeCompressed() expRevPK := keyRing.RevocationKey
expToRemotePK := keyRing.ToRemoteKey.SerializeCompressed() expToRemotePK := keyRing.ToRemoteKey
// Assert that the blob contained the serialized revocation and to-local breachInfo := &lnwallet.BreachRetribution{
// pubkeys. RemoteDelay: csvDelay,
require.Equal(t, expRevPK, jKit.RevocationPubKey[:]) KeyRing: &lnwallet.CommitmentKeyRing{
require.Equal(t, expToLocalPK, jKit.LocalDelayPubKey[:]) ToLocalKey: expToLocalPK,
RevocationKey: expRevPK,
// Determine if the breach transaction has a to-remote output and/or ToRemoteKey: expToRemotePK,
// 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[:])
} }
// Assert that the CSV is encoded in the blob. expectedKit, err := test.commitType.NewJusticeKit(
require.Equal(t, test.breachInfo.RemoteDelay, jKit.CSVDelay) test.expSweepScript, breachInfo, test.expToRemoteInput != nil,
// 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[:],
) )
if hasToLocal { require.NoError(t, err)
require.False(t, emptyToLocalSig, "to-local signature should "+
"not be empty")
} else {
require.True(t, emptyToLocalSig, "to-local signature should "+
"be empty")
}
emptyToRemoteSig := bytes.Equal( jKit.AddToLocalSig(zeroSig)
jKit.CommitToRemoteSig.RawBytes(), zeroSig[:], jKit.AddToRemoteSig(zeroSig)
)
if hasToRemote { require.Equal(t, expectedKit, jKit)
require.False(t, emptyToRemoteSig, "to-remote signature "+ }
"should not be empty")
} else { func makeSig(i int) lnwire.Sig {
require.True(t, emptyToRemoteSig, "to-remote signature "+ var sigBytes [64]byte
"should be empty") binary.BigEndian.PutUint64(sigBytes[:8], uint64(i))
}
sig, _ := lnwire.NewSigFromWireECDSA(sigBytes[:])
return sig
} }
+8 -2
View File
@@ -1201,7 +1201,10 @@ func randCommittedUpdateForChannel(t *testing.T, chanID lnwire.ChannelID,
_, err := io.ReadFull(crand.Reader, hint[:]) _, err := io.ReadFull(crand.Reader, hint[:])
require.NoError(t, err) 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) _, err = io.ReadFull(crand.Reader, encBlob)
require.NoError(t, err) require.NoError(t, err)
@@ -1229,7 +1232,10 @@ func randCommittedUpdateForChanWithHeight(t *testing.T, chanID lnwire.ChannelID,
_, err := io.ReadFull(crand.Reader, hint[:]) _, err := io.ReadFull(crand.Reader, hint[:])
require.NoError(t, err) 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) _, err = io.ReadFull(crand.Reader, encBlob)
require.NoError(t, err) require.NoError(t, err)
+11 -1
View File
@@ -240,9 +240,19 @@ func (t *TowerDB) InsertStateUpdate(update *SessionStateUpdate) (uint16, error)
return err 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 // Assert that the blob is the correct size for the session's
// blob type. // blob type.
expBlobSize := blob.Size(session.Policy.BlobType) expBlobSize := blob.Size(kit)
if len(update.EncryptedBlob) != expBlobSize { if len(update.EncryptedBlob) != expBlobSize {
return ErrInvalidBlobSize return ErrInvalidBlobSize
} }
+6 -2
View File
@@ -17,7 +17,10 @@ import (
) )
var ( 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. // 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]) copy(hint[:4], id[:4])
binary.BigEndian.PutUint16(hint[4:6], uint16(i)) binary.BigEndian.PutUint16(hint[4:6], uint16(i))
blobSize := blob.Size(blob.TypeAltruistCommit) kit, _ := blob.AnchorCommitment.EmptyJusticeKit()
blobSize := blob.Size(kit)
return &wtdb.SessionStateUpdate{ return &wtdb.SessionStateUpdate{
ID: *id, ID: *id,
+12 -2
View File
@@ -37,12 +37,22 @@ func (db *TowerDB) InsertStateUpdate(update *wtdb.SessionStateUpdate) (uint16, e
return 0, wtdb.ErrSessionNotFound 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. // 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 return 0, wtdb.ErrInvalidBlobSize
} }
err := info.AcceptUpdateSequence(update.SeqNum, update.LastApplied) err = info.AcceptUpdateSequence(update.SeqNum, update.LastApplied)
if err != nil { if err != nil {
return info.LastApplied, err return info.LastApplied, err
} }
+4 -1
View File
@@ -30,7 +30,10 @@ var (
testnetChainHash = *chaincfg.TestNet3Params.GenesisHash 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. // randPubKey generates a new secp keypair, and returns the public key.