Merge pull request #7736 from ellemouton/towerInterfaces
watchtower+refactor: CommitmentType and JusticeKit interface
This commit is contained in:
16 files changed
+1267
-965
No files matched your search
@@ -156,6 +156,10 @@
|
|||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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])
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in new issue
Block a user