Merge pull request #2124 from cfromknecht/wtlookout
[watchtower/lookout]: on-chain breach monitoring
This commit is contained in:
commit
2f0bc5c370
@ -2,6 +2,7 @@ package blob
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -22,8 +23,8 @@ const (
|
||||
// MaxVersion is the maximumm blob version supported by this package.
|
||||
MaxVersion = 0
|
||||
|
||||
// NonceSize is the length of a chacha20poly1305 nonce, 12 bytes.
|
||||
NonceSize = chacha20poly1305.NonceSize
|
||||
// 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
|
||||
@ -49,10 +50,11 @@ const (
|
||||
)
|
||||
|
||||
// Size returns the size of the encoded-and-encrypted blob in bytes.
|
||||
// enciphered plaintext: n bytes
|
||||
// MAC: 16 bytes
|
||||
// nonce: 24 bytes
|
||||
// enciphered plaintext: n bytes
|
||||
// MAC: 16 bytes
|
||||
func Size(ver uint16) int {
|
||||
return PlaintextSize(ver) + CiphertextExpansion
|
||||
return NonceSize + PlaintextSize(ver) + CiphertextExpansion
|
||||
}
|
||||
|
||||
// PlaintextSize returns the size of the encoded-but-unencrypted blob in bytes.
|
||||
@ -79,11 +81,6 @@ var (
|
||||
"ciphertext is too small for chacha20poly1305",
|
||||
)
|
||||
|
||||
// ErrNonceSize signals that the provided nonce is improperly sized.
|
||||
ErrNonceSize = fmt.Errorf(
|
||||
"chacha20poly1305 nonce must be %d bytes", NonceSize,
|
||||
)
|
||||
|
||||
// ErrKeySize signals that the provided key is improperly sized.
|
||||
ErrKeySize = fmt.Errorf(
|
||||
"chacha20poly1305 key size must be %d bytes", KeySize,
|
||||
@ -180,12 +177,18 @@ func (b *JusticeKit) CommitToLocalWitnessScript() ([]byte, error) {
|
||||
// CommitToLocalRevokeWitnessStack constructs a witness stack spending the
|
||||
// revocation clause of the commitment to-local output.
|
||||
// <revocation-sig> 1
|
||||
func (b *JusticeKit) CommitToLocalRevokeWitnessStack() [][]byte {
|
||||
func (b *JusticeKit) CommitToLocalRevokeWitnessStack() ([][]byte, error) {
|
||||
toLocalSig, err := b.CommitToLocalSig.ToSignature()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
witnessStack := make([][]byte, 2)
|
||||
witnessStack[0] = append(b.CommitToLocalSig[:], byte(txscript.SigHashAll))
|
||||
witnessStack[0] = append(toLocalSig.Serialize(),
|
||||
byte(txscript.SigHashAll))
|
||||
witnessStack[1] = []byte{1}
|
||||
|
||||
return witnessStack
|
||||
return witnessStack, nil
|
||||
}
|
||||
|
||||
// HasCommitToRemoteOutput returns true if the blob contains a to-remote p2wkh
|
||||
@ -207,11 +210,17 @@ func (b *JusticeKit) CommitToRemoteWitnessScript() ([]byte, error) {
|
||||
// CommitToRemoteWitnessStack returns a witness stack spending the commitment
|
||||
// to-remote output, which is a regular p2wkh.
|
||||
// <to-remote-sig>
|
||||
func (b *JusticeKit) CommitToRemoteWitnessStack() [][]byte {
|
||||
witnessStack := make([][]byte, 1)
|
||||
witnessStack[0] = append(b.CommitToRemoteSig[:], byte(txscript.SigHashAll))
|
||||
func (b *JusticeKit) CommitToRemoteWitnessStack() ([][]byte, error) {
|
||||
toRemoteSig, err := b.CommitToRemoteSig.ToSignature()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return witnessStack
|
||||
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
|
||||
@ -220,15 +229,9 @@ func (b *JusticeKit) CommitToRemoteWitnessStack() [][]byte {
|
||||
//
|
||||
// NOTE: It is the caller's responsibility to ensure that this method is only
|
||||
// called once for a given (nonce, key) pair.
|
||||
func (b *JusticeKit) Encrypt(nonce, key []byte, version uint16) ([]byte, error) {
|
||||
switch {
|
||||
|
||||
// Fail if the nonce is not 12-bytes.
|
||||
case len(nonce) != NonceSize:
|
||||
return nil, ErrNonceSize
|
||||
|
||||
func (b *JusticeKit) Encrypt(key []byte, version uint16) ([]byte, error) {
|
||||
// Fail if the nonce is not 32-bytes.
|
||||
case len(key) != KeySize:
|
||||
if len(key) != KeySize {
|
||||
return nil, ErrKeySize
|
||||
}
|
||||
|
||||
@ -241,19 +244,25 @@ func (b *JusticeKit) Encrypt(nonce, key []byte, version uint16) ([]byte, error)
|
||||
}
|
||||
|
||||
// Create a new chacha20poly1305 cipher, using a 32-byte key.
|
||||
cipher, err := chacha20poly1305.New(key)
|
||||
cipher, err := chacha20poly1305.NewX(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Allocate the ciphertext, which will contain the encrypted plaintext
|
||||
// and MAC.
|
||||
// Allocate the ciphertext, which will contain the nonce, encrypted
|
||||
// plaintext and MAC.
|
||||
plaintext := ptxtBuf.Bytes()
|
||||
ciphertext := make([]byte, len(plaintext)+CiphertextExpansion)
|
||||
ciphertext := make([]byte, Size(version))
|
||||
|
||||
// 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[:0], nonce, plaintext, nil)
|
||||
cipher.Seal(ciphertext[NonceSize:NonceSize], nonce, plaintext, nil)
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
@ -261,24 +270,21 @@ func (b *JusticeKit) Encrypt(nonce, key []byte, version uint16) ([]byte, error)
|
||||
// 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(nonce, key, ciphertext []byte, version uint16) (*JusticeKit, error) {
|
||||
func Decrypt(key, ciphertext []byte, version uint16) (*JusticeKit, error) {
|
||||
switch {
|
||||
|
||||
// Fail if the blob's overall length is less than the expansion factor.
|
||||
case len(ciphertext) < CiphertextExpansion:
|
||||
// Fail if the blob's overall length is less than required for the nonce
|
||||
// and expansion factor.
|
||||
case len(ciphertext) < NonceSize+CiphertextExpansion:
|
||||
return nil, ErrCiphertextTooSmall
|
||||
|
||||
// Fail if the nonce is not 12-bytes.
|
||||
case len(nonce) != NonceSize:
|
||||
return nil, ErrNonceSize
|
||||
|
||||
// Fail if the key is not 32-bytes.
|
||||
case len(key) != KeySize:
|
||||
return nil, ErrKeySize
|
||||
}
|
||||
|
||||
// Create a new chacha20poly1305 cipher, using a 32-byte key.
|
||||
cipher, err := chacha20poly1305.New(key)
|
||||
cipher, err := chacha20poly1305.NewX(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -290,7 +296,8 @@ func Decrypt(nonce, key, ciphertext []byte, version uint16) (*JusticeKit, error)
|
||||
|
||||
// Decrypt the ciphertext, placing the resulting plaintext in our
|
||||
// plaintext buffer.
|
||||
_, err = cipher.Open(plaintext[:0], nonce, ciphertext, nil)
|
||||
nonce := ciphertext[:NonceSize]
|
||||
_, err = cipher.Open(plaintext[:0], nonce, ciphertext[NonceSize:], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -163,15 +163,9 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
|
||||
t.Fatalf("unable to generate blob encryption key: %v", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, blob.NonceSize)
|
||||
_, err = io.ReadFull(rand.Reader, nonce)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate nonce nonce: %v", err)
|
||||
}
|
||||
|
||||
// Encrypt the blob plaintext using the generated key and
|
||||
// target version for this test.
|
||||
ctxt, err := boj.Encrypt(nonce, key, test.encVersion)
|
||||
ctxt, err := boj.Encrypt(key, test.encVersion)
|
||||
if err != test.encErr {
|
||||
t.Fatalf("unable to encrypt blob: %v", err)
|
||||
} else if test.encErr != nil {
|
||||
@ -191,7 +185,7 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
|
||||
// Decrypt the encrypted blob, reconstructing the original
|
||||
// blob plaintext from the decrypted contents. We use the target
|
||||
// decryption version specified by this test case.
|
||||
boj2, err := blob.Decrypt(nonce, key, ctxt, test.decVersion)
|
||||
boj2, err := blob.Decrypt(key, ctxt, test.decVersion)
|
||||
if err != test.decErr {
|
||||
t.Fatalf("unable to decrypt blob: %v", err)
|
||||
} else if test.decErr != nil {
|
||||
|
68
watchtower/lookout/interface.go
Normal file
68
watchtower/lookout/interface.go
Normal file
@ -0,0 +1,68 @@
|
||||
package lookout
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
||||
)
|
||||
|
||||
// Service abstracts the lookout functionality, supporting the ability to start
|
||||
// and stop. All communication and actions are driven via the database or chain
|
||||
// events.
|
||||
type Service interface {
|
||||
// Start safely starts up the Interface.
|
||||
Start() error
|
||||
|
||||
// Stop safely stops the Interface.
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// BlockFetcher supports the ability to fetch blocks from the backend or
|
||||
// network.
|
||||
type BlockFetcher interface {
|
||||
// GetBlock fetches the block given the target block hash.
|
||||
GetBlock(*chainhash.Hash) (*wire.MsgBlock, error)
|
||||
}
|
||||
|
||||
// DB abstracts the required persistent calls expected by the lookout. DB
|
||||
// provides the ability to search for state updates that correspond to breach
|
||||
// transactions confirmed in a particular block.
|
||||
type DB interface {
|
||||
// GetLookoutTip returns the last block epoch at which the tower
|
||||
// performed a match. If no match has been done, a nil epoch will be
|
||||
// returned.
|
||||
GetLookoutTip() (*chainntnfs.BlockEpoch, error)
|
||||
|
||||
// QueryMatches searches its database for any state updates matching the
|
||||
// provided breach hints. If any matches are found, they will be
|
||||
// returned along with encrypted blobs so that justice can be exacted.
|
||||
QueryMatches([]wtdb.BreachHint) ([]wtdb.Match, error)
|
||||
|
||||
// SetLookoutTip writes the best epoch for which the watchtower has
|
||||
// queried for breach hints.
|
||||
SetLookoutTip(*chainntnfs.BlockEpoch) error
|
||||
}
|
||||
|
||||
// EpochRegistrar supports the ability to register for events corresponding to
|
||||
// newly created blocks.
|
||||
type EpochRegistrar interface {
|
||||
// RegisterBlockEpochNtfn registers for a new block epoch subscription.
|
||||
// The implementation must support historical dispatch, starting from
|
||||
// the provided chainntnfs.BlockEpoch when it is non-nil. The
|
||||
// notifications should be delivered in-order, and deliver reorged
|
||||
// blocks.
|
||||
RegisterBlockEpochNtfn(
|
||||
*chainntnfs.BlockEpoch) (*chainntnfs.BlockEpochEvent, error)
|
||||
}
|
||||
|
||||
// Punisher handles the construction and publication of justice transactions
|
||||
// once they have been detected by the Service.
|
||||
type Punisher interface {
|
||||
// Punish accepts a JusticeDescriptor, constructs the justice
|
||||
// transaction, and publishes the transaction to the network so it can
|
||||
// be mined. The second parameter is a quit channel so that long-running
|
||||
// operations required to track the confirmation of the transaction can
|
||||
// be canceled on shutdown.
|
||||
Punish(*JusticeDescriptor, <-chan struct{}) error
|
||||
}
|
299
watchtower/lookout/justice_descriptor.go
Normal file
299
watchtower/lookout/justice_descriptor.go
Normal file
@ -0,0 +1,299 @@
|
||||
package lookout
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/btcsuite/btcd/blockchain"
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrOutputNotFound signals that the breached output could not be found
|
||||
// on the commitment transaction.
|
||||
ErrOutputNotFound = errors.New("unable to find output on commit tx")
|
||||
|
||||
// ErrUnknownSweepAddrType signals that client provided an output that
|
||||
// was not p2wkh or p2wsh.
|
||||
ErrUnknownSweepAddrType = errors.New("sweep addr is not p2wkh or p2wsh")
|
||||
)
|
||||
|
||||
// JusticeDescriptor contains the information required to sweep a breached
|
||||
// channel on behalf of a victim. It supports the ability to create the justice
|
||||
// transaction that sweeps the commitments and recover a cut of the channel for
|
||||
// the watcher's eternal vigilance.
|
||||
type JusticeDescriptor struct {
|
||||
// BreachedCommitTx is the commitment transaction that caused the breach
|
||||
// to be detected.
|
||||
BreachedCommitTx *wire.MsgTx
|
||||
|
||||
// SessionInfo contains the contract with the watchtower client and
|
||||
// the prenegotiated terms they agreed to.
|
||||
SessionInfo *wtdb.SessionInfo
|
||||
|
||||
// JusticeKit contains the decrypted blob and information required to
|
||||
// construct the transaction scripts and witnesses.
|
||||
JusticeKit *blob.JusticeKit
|
||||
}
|
||||
|
||||
// breachedInput contains the required information to construct and spend
|
||||
// breached outputs on a commitment transaction.
|
||||
type breachedInput struct {
|
||||
txOut *wire.TxOut
|
||||
outPoint wire.OutPoint
|
||||
witness [][]byte
|
||||
}
|
||||
|
||||
// commitToLocalInput extracts the information required to spend the commit
|
||||
// to-local output.
|
||||
func (p *JusticeDescriptor) commitToLocalInput() (*breachedInput, error) {
|
||||
// Retrieve the to-local witness script from the justice kit.
|
||||
toLocalScript, err := p.JusticeKit.CommitToLocalWitnessScript()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compute the witness script hash, which will be used to locate the
|
||||
// input on the breaching commitment transaction.
|
||||
toLocalWitnessHash, err := lnwallet.WitnessScriptHash(toLocalScript)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Locate the to-local output on the breaching commitment transaction.
|
||||
toLocalIndex, toLocalTxOut, err := findTxOutByPkScript(
|
||||
p.BreachedCommitTx, toLocalWitnessHash,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Construct the to-local outpoint that will be spent in the justice
|
||||
// transaction.
|
||||
toLocalOutPoint := wire.OutPoint{
|
||||
Hash: p.BreachedCommitTx.TxHash(),
|
||||
Index: toLocalIndex,
|
||||
}
|
||||
|
||||
// Retrieve to-local witness stack, which primarily includes a signature
|
||||
// under the revocation pubkey.
|
||||
witnessStack, err := p.JusticeKit.CommitToLocalRevokeWitnessStack()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &breachedInput{
|
||||
txOut: toLocalTxOut,
|
||||
outPoint: toLocalOutPoint,
|
||||
witness: buildWitness(witnessStack, toLocalScript),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// commitToRemoteInput extracts the information required to spend the commit
|
||||
// to-remote output.
|
||||
func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) {
|
||||
// Retrieve the to-remote witness script from the justice kit.
|
||||
toRemoteScript, err := p.JusticeKit.CommitToRemoteWitnessScript()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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, btcec.S256())
|
||||
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 := lnwallet.CommitScriptUnencumbered(
|
||||
toRemotePubKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Locate the to-remote output on the breaching commitment transaction.
|
||||
toRemoteIndex, toRemoteTxOut, err := findTxOutByPkScript(
|
||||
p.BreachedCommitTx, toRemoteScriptHash,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Construct the to-remote outpoint which will be spent in the justice
|
||||
// transaction.
|
||||
toRemoteOutPoint := wire.OutPoint{
|
||||
Hash: p.BreachedCommitTx.TxHash(),
|
||||
Index: toRemoteIndex,
|
||||
}
|
||||
|
||||
// Retrieve the to-remote witness stack, which is just a signature under
|
||||
// the to-remote pubkey.
|
||||
witnessStack, err := p.JusticeKit.CommitToRemoteWitnessStack()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &breachedInput{
|
||||
txOut: toRemoteTxOut,
|
||||
outPoint: toRemoteOutPoint,
|
||||
witness: buildWitness(witnessStack, toRemoteScript),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// assembleJusticeTxn accepts the breached inputs recovered from state update
|
||||
// and attempts to construct the justice transaction that sweeps the victims
|
||||
// funds to their wallet and claims the watchtower's reward.
|
||||
func (p *JusticeDescriptor) assembleJusticeTxn(txWeight int64,
|
||||
inputs ...*breachedInput) (*wire.MsgTx, error) {
|
||||
|
||||
justiceTxn := wire.NewMsgTx(2)
|
||||
|
||||
// First, construct add the breached inputs to our justice transaction
|
||||
// and compute the total amount that will be swept.
|
||||
var totalAmt btcutil.Amount
|
||||
for _, input := range inputs {
|
||||
totalAmt += btcutil.Amount(input.txOut.Value)
|
||||
justiceTxn.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: input.outPoint,
|
||||
})
|
||||
}
|
||||
|
||||
// Using the total input amount and the transaction's weight, compute
|
||||
// the sweep and reward amounts. This corresponds to the amount returned
|
||||
// to the victim and the amount paid to the tower, respectively. To do
|
||||
// so, the required transaction fee is subtracted from the total, and
|
||||
// the remaining amount is divided according to the prenegotiated reward
|
||||
// rate from the client's session info.
|
||||
sweepAmt, rewardAmt, err := p.SessionInfo.ComputeSweepOutputs(
|
||||
totalAmt, txWeight,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(conner): abort/don't add if outputs are dusty
|
||||
|
||||
// Add the sweep and reward outputs to the justice transaction.
|
||||
justiceTxn.AddTxOut(&wire.TxOut{
|
||||
PkScript: p.JusticeKit.SweepAddress[:],
|
||||
Value: int64(sweepAmt),
|
||||
})
|
||||
justiceTxn.AddTxOut(&wire.TxOut{
|
||||
PkScript: p.SessionInfo.RewardAddress,
|
||||
Value: int64(rewardAmt),
|
||||
})
|
||||
|
||||
// TODO(conner): apply and handle BIP69 sort
|
||||
|
||||
btx := btcutil.NewTx(justiceTxn)
|
||||
if err := blockchain.CheckTransactionSanity(btx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Attach each of the provided witnesses to the transaction.
|
||||
for i, input := range inputs {
|
||||
justiceTxn.TxIn[i].Witness = input.witness
|
||||
|
||||
// Validate the reconstructed witnesses to ensure they are valid
|
||||
// for the breached inputs.
|
||||
vm, err := txscript.NewEngine(
|
||||
input.txOut.PkScript, justiceTxn, i,
|
||||
txscript.StandardVerifyFlags,
|
||||
nil, nil, input.txOut.Value,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := vm.Execute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return justiceTxn, nil
|
||||
}
|
||||
|
||||
// CreateJusticeTxn computes the justice transaction that sweeps a breaching
|
||||
// commitment transaction. The justice transaction is constructed by assembling
|
||||
// the witnesses using data provided by the client in a prior state update.
|
||||
func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) {
|
||||
var (
|
||||
sweepInputs = make([]*breachedInput, 0, 2)
|
||||
weightEstimate lnwallet.TxWeightEstimator
|
||||
)
|
||||
|
||||
// Add our reward address to the weight estimate.
|
||||
weightEstimate.AddP2WKHOutput()
|
||||
|
||||
// Add the sweep address's contribution, depending on whether it is a
|
||||
// p2wkh or p2wsh output.
|
||||
switch len(p.JusticeKit.SweepAddress) {
|
||||
case lnwallet.P2WPKHSize:
|
||||
weightEstimate.AddP2WKHOutput()
|
||||
|
||||
case lnwallet.P2WSHSize:
|
||||
weightEstimate.AddP2WSHOutput()
|
||||
|
||||
default:
|
||||
return nil, ErrUnknownSweepAddrType
|
||||
}
|
||||
|
||||
// Assemble the breached to-local output from the justice descriptor and
|
||||
// add it to our weight estimate.
|
||||
toLocalInput, err := p.commitToLocalInput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
weightEstimate.AddWitnessInput(lnwallet.ToLocalPenaltyWitnessSize)
|
||||
sweepInputs = append(sweepInputs, toLocalInput)
|
||||
|
||||
// If the justice kit specifies that we have to sweep the to-remote
|
||||
// output, we'll also try to assemble the output and add it to weight
|
||||
// estimate if successful.
|
||||
if p.JusticeKit.HasCommitToRemoteOutput() {
|
||||
toRemoteInput, err := p.commitToRemoteInput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
weightEstimate.AddWitnessInput(lnwallet.P2WKHWitnessSize)
|
||||
sweepInputs = append(sweepInputs, toRemoteInput)
|
||||
}
|
||||
|
||||
// TODO(conner): sweep htlc outputs
|
||||
|
||||
txWeight := int64(weightEstimate.Weight())
|
||||
|
||||
return p.assembleJusticeTxn(txWeight, sweepInputs...)
|
||||
}
|
||||
|
||||
// findTxOutByPkScript searches the given transaction for an output whose
|
||||
// pkscript matches the query. If one is found, the TxOut is returned along with
|
||||
// the index.
|
||||
//
|
||||
// NOTE: The search stops after the first match is found.
|
||||
func findTxOutByPkScript(txn *wire.MsgTx,
|
||||
pkScript []byte) (uint32, *wire.TxOut, error) {
|
||||
|
||||
found, index := lnwallet.FindScriptOutputIndex(txn, pkScript)
|
||||
if !found {
|
||||
return 0, nil, ErrOutputNotFound
|
||||
}
|
||||
|
||||
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
|
||||
}
|
348
watchtower/lookout/justice_descriptor_test.go
Normal file
348
watchtower/lookout/justice_descriptor_test.go
Normal file
@ -0,0 +1,348 @@
|
||||
// +build dev
|
||||
|
||||
package lookout_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/blockchain"
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||
"github.com/lightningnetwork/lnd/watchtower/lookout"
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
||||
)
|
||||
|
||||
const csvDelay uint32 = 144
|
||||
|
||||
var (
|
||||
revPrivBytes = []byte{
|
||||
0x8f, 0x4b, 0x51, 0x83, 0xa9, 0x34, 0xbd, 0x5f,
|
||||
0x74, 0x6c, 0x9d, 0x5c, 0xae, 0x88, 0x2d, 0x31,
|
||||
0x06, 0x90, 0xdd, 0x8c, 0x9b, 0x31, 0xbc, 0xd1,
|
||||
0x78, 0x91, 0x88, 0x2a, 0xf9, 0x74, 0xa0, 0xef,
|
||||
}
|
||||
|
||||
toLocalPrivBytes = []byte{
|
||||
0xde, 0x17, 0xc1, 0x2f, 0xdc, 0x1b, 0xc0, 0xc6,
|
||||
0x59, 0x5d, 0xf9, 0xc1, 0x3e, 0x89, 0xbc, 0x6f,
|
||||
0x01, 0x85, 0x45, 0x76, 0x26, 0xce, 0x9c, 0x55,
|
||||
0x3b, 0xc9, 0xec, 0x3d, 0xd8, 0x8b, 0xac, 0xa8,
|
||||
}
|
||||
|
||||
toRemotePrivBytes = []byte{
|
||||
0x28, 0x59, 0x6f, 0x36, 0xb8, 0x9f, 0x19, 0x5d,
|
||||
0xcb, 0x07, 0x48, 0x8a, 0xe5, 0x89, 0x71, 0x74,
|
||||
0x70, 0x4c, 0xff, 0x1e, 0x9c, 0x00, 0x93, 0xbe,
|
||||
0xe2, 0x2e, 0x68, 0x08, 0x4c, 0xb4, 0x0f, 0x4f,
|
||||
}
|
||||
)
|
||||
|
||||
type mockSigner struct {
|
||||
index uint32
|
||||
keys map[keychain.KeyLocator]*btcec.PrivateKey
|
||||
}
|
||||
|
||||
func newMockSigner() *mockSigner {
|
||||
return &mockSigner{
|
||||
keys: make(map[keychain.KeyLocator]*btcec.PrivateKey),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *mockSigner) SignOutputRaw(tx *wire.MsgTx,
|
||||
signDesc *lnwallet.SignDescriptor) ([]byte, error) {
|
||||
|
||||
witnessScript := signDesc.WitnessScript
|
||||
amt := signDesc.Output.Value
|
||||
|
||||
privKey, ok := s.keys[signDesc.KeyDesc.KeyLocator]
|
||||
if !ok {
|
||||
panic("cannot sign w/ unknown key")
|
||||
}
|
||||
|
||||
sig, err := txscript.RawTxInWitnessSignature(
|
||||
tx, signDesc.SigHashes, signDesc.InputIndex, amt,
|
||||
witnessScript, signDesc.HashType, privKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sig[:len(sig)-1], nil
|
||||
}
|
||||
|
||||
func (s *mockSigner) ComputeInputScript(tx *wire.MsgTx,
|
||||
signDesc *lnwallet.SignDescriptor) (*lnwallet.InputScript, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *mockSigner) addPrivKey(privKey *btcec.PrivateKey) keychain.KeyLocator {
|
||||
keyLoc := keychain.KeyLocator{
|
||||
Index: s.index,
|
||||
}
|
||||
s.index++
|
||||
|
||||
s.keys[keyLoc] = privKey
|
||||
|
||||
return keyLoc
|
||||
}
|
||||
|
||||
func TestJusticeDescriptor(t *testing.T) {
|
||||
const (
|
||||
localAmount = btcutil.Amount(100000)
|
||||
remoteAmount = btcutil.Amount(200000)
|
||||
totalAmount = localAmount + remoteAmount
|
||||
)
|
||||
|
||||
// Parse the key pairs for all keys used in the test.
|
||||
revSK, revPK := btcec.PrivKeyFromBytes(
|
||||
btcec.S256(), revPrivBytes,
|
||||
)
|
||||
_, toLocalPK := btcec.PrivKeyFromBytes(
|
||||
btcec.S256(), toLocalPrivBytes,
|
||||
)
|
||||
toRemoteSK, toRemotePK := btcec.PrivKeyFromBytes(
|
||||
btcec.S256(), toRemotePrivBytes,
|
||||
)
|
||||
|
||||
// Create the signer, and add the revocation and to-remote privkeys.
|
||||
signer := newMockSigner()
|
||||
var (
|
||||
revKeyLoc = signer.addPrivKey(revSK)
|
||||
toRemoteKeyLoc = signer.addPrivKey(toRemoteSK)
|
||||
)
|
||||
|
||||
// Construct the to-local witness script.
|
||||
toLocalScript, err := lnwallet.CommitScriptToSelf(
|
||||
csvDelay, toLocalPK, revPK,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create to-local script: %v", err)
|
||||
}
|
||||
|
||||
// Compute the to-local witness script hash.
|
||||
toLocalScriptHash, err := lnwallet.WitnessScriptHash(toLocalScript)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create to-local witness script hash: %v", err)
|
||||
}
|
||||
|
||||
// Compute the to-remote witness script hash.
|
||||
toRemoteScriptHash, err := lnwallet.CommitScriptUnencumbered(toRemotePK)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create to-remote script: %v", err)
|
||||
}
|
||||
|
||||
// Construct the breaching commitment txn, containing the to-local and
|
||||
// to-remote outputs. We don't need any inputs for this test.
|
||||
breachTxn := &wire.MsgTx{
|
||||
Version: 2,
|
||||
TxIn: []*wire.TxIn{},
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: int64(localAmount),
|
||||
PkScript: toLocalScriptHash,
|
||||
},
|
||||
{
|
||||
Value: int64(remoteAmount),
|
||||
PkScript: toRemoteScriptHash,
|
||||
},
|
||||
},
|
||||
}
|
||||
breachTxID := breachTxn.TxHash()
|
||||
|
||||
// Compute the weight estimate for our justice transaction.
|
||||
var weightEstimate lnwallet.TxWeightEstimator
|
||||
weightEstimate.AddP2WKHOutput()
|
||||
weightEstimate.AddP2WKHOutput()
|
||||
weightEstimate.AddWitnessInput(lnwallet.ToLocalPenaltyWitnessSize)
|
||||
weightEstimate.AddWitnessInput(lnwallet.P2WKHWitnessSize)
|
||||
txWeight := weightEstimate.Weight()
|
||||
|
||||
// Create a session info so that simulate agreement of the sweep
|
||||
// parameters that should be used in constructing the justice
|
||||
// transaction.
|
||||
sessionInfo := &wtdb.SessionInfo{
|
||||
SweepFeeRate: 2000,
|
||||
RewardRate: 900000,
|
||||
RewardAddress: makeAddrSlice(22),
|
||||
}
|
||||
|
||||
// Given the total input amount and the weight estimate, compute the
|
||||
// amount that should be swept for the victim and the amount taken as a
|
||||
// reward by the watchtower.
|
||||
sweepAmt, rewardAmt, err := sessionInfo.ComputeSweepOutputs(
|
||||
totalAmount, int64(txWeight),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to compute sweep outputs: %v", err)
|
||||
}
|
||||
|
||||
// Begin to assemble the justice kit, starting with the sweep address,
|
||||
// pubkeys, and csv delay.
|
||||
justiceKit := &blob.JusticeKit{
|
||||
SweepAddress: makeAddrSlice(22),
|
||||
CSVDelay: csvDelay,
|
||||
}
|
||||
copy(justiceKit.RevocationPubKey[:], revPK.SerializeCompressed())
|
||||
copy(justiceKit.LocalDelayPubKey[:], toLocalPK.SerializeCompressed())
|
||||
copy(justiceKit.CommitToRemotePubKey[:], toRemotePK.SerializeCompressed())
|
||||
|
||||
// Create a transaction spending from the outputs of the breach
|
||||
// transaction created earlier. The inputs are always ordered w/
|
||||
// to-local and then to-remote. The outputs are always added as the
|
||||
// sweep address then reward address.
|
||||
justiceTxn := &wire.MsgTx{
|
||||
Version: 2,
|
||||
TxIn: []*wire.TxIn{
|
||||
{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: breachTxID,
|
||||
Index: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: breachTxID,
|
||||
Index: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
|
||||
Value: int64(sweepAmt),
|
||||
PkScript: justiceKit.SweepAddress,
|
||||
},
|
||||
{
|
||||
|
||||
Value: int64(rewardAmt),
|
||||
PkScript: sessionInfo.RewardAddress,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
hashCache := txscript.NewTxSigHashes(justiceTxn)
|
||||
|
||||
// Create the sign descriptor used to sign for the to-local input.
|
||||
toLocalSignDesc := &lnwallet.SignDescriptor{
|
||||
KeyDesc: keychain.KeyDescriptor{
|
||||
KeyLocator: revKeyLoc,
|
||||
},
|
||||
WitnessScript: toLocalScript,
|
||||
Output: breachTxn.TxOut[0],
|
||||
SigHashes: hashCache,
|
||||
InputIndex: 0,
|
||||
HashType: txscript.SigHashAll,
|
||||
}
|
||||
|
||||
// Create the sign descriptor used to sign for the to-remote input.
|
||||
toRemoteSignDesc := &lnwallet.SignDescriptor{
|
||||
KeyDesc: keychain.KeyDescriptor{
|
||||
KeyLocator: toRemoteKeyLoc,
|
||||
PubKey: toRemotePK,
|
||||
},
|
||||
WitnessScript: toRemoteScriptHash,
|
||||
Output: breachTxn.TxOut[1],
|
||||
SigHashes: hashCache,
|
||||
InputIndex: 1,
|
||||
HashType: txscript.SigHashAll,
|
||||
}
|
||||
|
||||
// Verify that our test justice transaction is sane.
|
||||
btx := btcutil.NewTx(justiceTxn)
|
||||
if err := blockchain.CheckTransactionSanity(btx); err != nil {
|
||||
t.Fatalf("justice txn is not sane: %v", err)
|
||||
}
|
||||
|
||||
// Compute a DER-encoded signature for the to-local input.
|
||||
toLocalSigRaw, err := signer.SignOutputRaw(justiceTxn, toLocalSignDesc)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to sign to-local input: %v", err)
|
||||
}
|
||||
|
||||
// Compute the witness for the to-remote input. The first element is a
|
||||
// DER-encoded signature under the to-remote pubkey. The sighash flag is
|
||||
// also present, so we trim it.
|
||||
toRemoteWitness, err := lnwallet.CommitSpendNoDelay(
|
||||
signer, toRemoteSignDesc, justiceTxn,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to sign to-remote input: %v", err)
|
||||
}
|
||||
toRemoteSigRaw := toRemoteWitness[0][:len(toRemoteWitness[0])-1]
|
||||
|
||||
// Convert the DER to-local sig into a fixed-size signature.
|
||||
toLocalSig, err := lnwire.NewSigFromRawSignature(toLocalSigRaw)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to parse to-local signature: %v", err)
|
||||
}
|
||||
|
||||
// Convert the DER to-remote sig into a fixed-size signature.
|
||||
toRemoteSig, err := lnwire.NewSigFromRawSignature(toRemoteSigRaw)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to parse to-remote signature: %v", err)
|
||||
}
|
||||
|
||||
// Complete our justice kit by copying the signatures into the payload.
|
||||
copy(justiceKit.CommitToLocalSig[:], toLocalSig[:])
|
||||
copy(justiceKit.CommitToRemoteSig[:], toRemoteSig[:])
|
||||
|
||||
justiceDesc := &lookout.JusticeDescriptor{
|
||||
BreachedCommitTx: breachTxn,
|
||||
SessionInfo: sessionInfo,
|
||||
JusticeKit: justiceKit,
|
||||
}
|
||||
|
||||
// Construct a breach punisher that will feed published transactions
|
||||
// over the buffered channel.
|
||||
publications := make(chan *wire.MsgTx, 1)
|
||||
punisher := lookout.NewBreachPunisher(&lookout.PunisherConfig{
|
||||
PublishTx: func(tx *wire.MsgTx) error {
|
||||
publications <- tx
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
// Exact retribution on the offender. If no error is returned, we expect
|
||||
// the justice transaction to be published via the channel.
|
||||
err = punisher.Punish(justiceDesc, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to punish breach: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve the published justice transaction.
|
||||
var wtJusticeTxn *wire.MsgTx
|
||||
select {
|
||||
case wtJusticeTxn = <-publications:
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
t.Fatalf("punisher did not publish justice txn")
|
||||
}
|
||||
|
||||
// Construct the test's to-local witness.
|
||||
justiceTxn.TxIn[0].Witness = make([][]byte, 3)
|
||||
justiceTxn.TxIn[0].Witness[0] = append(toLocalSigRaw,
|
||||
byte(txscript.SigHashAll))
|
||||
justiceTxn.TxIn[0].Witness[1] = []byte{1}
|
||||
justiceTxn.TxIn[0].Witness[2] = toLocalScript
|
||||
|
||||
// Construct the test's to-remote witness.
|
||||
justiceTxn.TxIn[1].Witness = make([][]byte, 2)
|
||||
justiceTxn.TxIn[1].Witness[0] = append(toRemoteSigRaw,
|
||||
byte(txscript.SigHashAll))
|
||||
justiceTxn.TxIn[1].Witness[1] = toRemotePK.SerializeCompressed()
|
||||
|
||||
// Assert that the watchtower derives the same justice txn.
|
||||
if !reflect.DeepEqual(justiceTxn, wtJusticeTxn) {
|
||||
t.Fatalf("expected justice txn: %v\ngot %v",
|
||||
spew.Sdump(justiceTxn),
|
||||
spew.Sdump(wtJusticeTxn))
|
||||
}
|
||||
}
|
29
watchtower/lookout/log.go
Normal file
29
watchtower/lookout/log.go
Normal file
@ -0,0 +1,29 @@
|
||||
package lookout
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
)
|
||||
|
||||
// log is a logger that is initialized with no output filters. This
|
||||
// means the package will not perform any logging by default until the caller
|
||||
// requests it.
|
||||
var log btclog.Logger
|
||||
|
||||
// The default amount of logging is none.
|
||||
func init() {
|
||||
UseLogger(build.NewSubLogger("WTWR", nil))
|
||||
}
|
||||
|
||||
// DisableLog disables all library log output. Logging output is disabled
|
||||
// by default until UseLogger is called.
|
||||
func DisableLog() {
|
||||
UseLogger(btclog.Disabled)
|
||||
}
|
||||
|
||||
// UseLogger uses a specified Logger to output package logging info.
|
||||
// This should be used in preference to SetLogWriter if the caller is also
|
||||
// using btclog.
|
||||
func UseLogger(logger btclog.Logger) {
|
||||
log = logger
|
||||
}
|
272
watchtower/lookout/lookout.go
Normal file
272
watchtower/lookout/lookout.go
Normal file
@ -0,0 +1,272 @@
|
||||
package lookout
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
||||
)
|
||||
|
||||
// Config houses the Lookout's required resources to properly fulfill it's duty,
|
||||
// including block fetching, querying accepted state updates, and construction
|
||||
// and publication of justice transactions.
|
||||
type Config struct {
|
||||
// DB provides persistent access to the watchtower's accepted state
|
||||
// updates such that they can be queried as new blocks arrive from the
|
||||
// network.
|
||||
DB DB
|
||||
|
||||
// EpochRegistrar supports the ability to register for events corresponding to
|
||||
// newly created blocks.
|
||||
EpochRegistrar EpochRegistrar
|
||||
|
||||
// BlockFetcher supports the ability to fetch blocks from the backend or
|
||||
// network.
|
||||
BlockFetcher BlockFetcher
|
||||
|
||||
// Punisher handles the responsibility of crafting and broadcasting
|
||||
// justice transaction for any breached transactions.
|
||||
Punisher Punisher
|
||||
}
|
||||
|
||||
// Lookout will check any incoming blocks against the transactions found in the
|
||||
// database, and in case of matches send the information needed to create a
|
||||
// penalty transaction to the punisher.
|
||||
type Lookout struct {
|
||||
started int32 // atomic
|
||||
shutdown int32 // atomic
|
||||
|
||||
cfg *Config
|
||||
|
||||
wg sync.WaitGroup
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
// New constructs a new Lookout from the given LookoutConfig.
|
||||
func New(cfg *Config) *Lookout {
|
||||
return &Lookout{
|
||||
cfg: cfg,
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start safely spins up the Lookout and begins monitoring for breaches.
|
||||
func (l *Lookout) Start() error {
|
||||
if !atomic.CompareAndSwapInt32(&l.started, 0, 1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("Starting lookout")
|
||||
|
||||
startEpoch, err := l.cfg.DB.GetLookoutTip()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if startEpoch == nil {
|
||||
log.Infof("Starting lookout from chain tip")
|
||||
} else {
|
||||
log.Infof("Starting lookout from epoch(height=%d hash=%x)",
|
||||
startEpoch.Height, startEpoch.Hash)
|
||||
}
|
||||
|
||||
events, err := l.cfg.EpochRegistrar.RegisterBlockEpochNtfn(startEpoch)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to register for block epochs: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
l.wg.Add(1)
|
||||
go l.watchBlocks(events)
|
||||
|
||||
log.Infof("Lookout started successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop safely shuts down the Lookout.
|
||||
func (l *Lookout) Stop() error {
|
||||
if !atomic.CompareAndSwapInt32(&l.shutdown, 0, 1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("Stopping lookout")
|
||||
|
||||
close(l.quit)
|
||||
l.wg.Wait()
|
||||
|
||||
log.Infof("Lookout stopped successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// watchBlocks serially pulls incoming epochs from the epoch source and searches
|
||||
// our accepted state updates for any breached transactions. If any are found,
|
||||
// we will attempt to decrypt the state updates' encrypted blobs and exact
|
||||
// justice for the victim.
|
||||
//
|
||||
// This method MUST be run as a goroutine.
|
||||
func (l *Lookout) watchBlocks(epochs *chainntnfs.BlockEpochEvent) {
|
||||
defer l.wg.Done()
|
||||
defer epochs.Cancel()
|
||||
|
||||
for {
|
||||
select {
|
||||
case epoch := <-epochs.Epochs:
|
||||
log.Debugf("Fetching block for (height=%d, hash=%x)",
|
||||
epoch.Height, epoch.Hash)
|
||||
|
||||
// Fetch the full block from the backend corresponding
|
||||
// to the newly arriving epoch.
|
||||
block, err := l.cfg.BlockFetcher.GetBlock(epoch.Hash)
|
||||
if err != nil {
|
||||
// TODO(conner): add retry logic?
|
||||
log.Errorf("Unable to fetch block for "+
|
||||
"(height=%x, hash=%x): %v",
|
||||
epoch.Height, epoch.Hash, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Process the block to see if it contains any breaches
|
||||
// that we are monitoring on behalf of our clients.
|
||||
err = l.processEpoch(epoch, block)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to process %s: %v",
|
||||
epoch, err)
|
||||
}
|
||||
|
||||
case <-l.quit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processEpoch accepts an Epoch and queries the database for any matching state
|
||||
// updates for the confirmed transactions. If any are found, the lookout
|
||||
// responds by attempting to decrypt the encrypted blob and publishing the
|
||||
// justice transaction.
|
||||
func (l *Lookout) processEpoch(epoch *chainntnfs.BlockEpoch,
|
||||
block *wire.MsgBlock) error {
|
||||
|
||||
numTxnsInBlock := len(block.Transactions)
|
||||
|
||||
log.Debugf("Scanning %d transaction in block (height=%d, hash=%x) "+
|
||||
"for breaches", numTxnsInBlock, epoch.Height, epoch.Hash)
|
||||
|
||||
// Iterate over the transactions contained in the block, deriving a
|
||||
// breach hint for each transaction and constructing an index mapping
|
||||
// the hint back to it's original transaction.
|
||||
hintToTx := make(map[wtdb.BreachHint]*wire.MsgTx, numTxnsInBlock)
|
||||
txHints := make([]wtdb.BreachHint, 0, numTxnsInBlock)
|
||||
for _, tx := range block.Transactions {
|
||||
hash := tx.TxHash()
|
||||
hint := wtdb.NewBreachHintFromHash(&hash)
|
||||
|
||||
txHints = append(txHints, hint)
|
||||
hintToTx[hint] = tx
|
||||
}
|
||||
|
||||
// Query the database to see if any of the breach hints cause a match
|
||||
// with any of our accepted state updates.
|
||||
matches, err := l.cfg.DB.QueryMatches(txHints)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// No matches were found, we are done.
|
||||
if len(matches) == 0 {
|
||||
log.Debugf("No breaches found in (height=%d, hash=%x)",
|
||||
epoch.Height, epoch.Hash)
|
||||
return nil
|
||||
}
|
||||
|
||||
breachCountStr := "breach"
|
||||
if len(matches) > 1 {
|
||||
breachCountStr = "breaches"
|
||||
}
|
||||
|
||||
log.Infof("Found %d %s in (height=%d, hash=%x)",
|
||||
len(matches), breachCountStr, epoch.Height, epoch.Hash)
|
||||
|
||||
// For each match, use our index to retrieve the original transaction,
|
||||
// which corresponds to the breaching commitment transaction. If the
|
||||
// decryption succeeds, we will accumlate the assembled justice
|
||||
// descriptors in a single slice
|
||||
var successes []*JusticeDescriptor
|
||||
for _, match := range matches {
|
||||
commitTx := hintToTx[match.Hint]
|
||||
log.Infof("Dispatching punisher for client %s, breach-txid=%s",
|
||||
match.ID, commitTx.TxHash().String())
|
||||
|
||||
// The decryption key for the state update should be the full
|
||||
// txid of the breaching commitment transaction.
|
||||
commitTxID := commitTx.TxHash()
|
||||
|
||||
// Now, decrypt the blob of justice that we received in the
|
||||
// state update. This will contain all information required to
|
||||
// sweep the breached commitment outputs.
|
||||
justiceKit, err := blob.Decrypt(
|
||||
commitTxID[:], match.EncryptedBlob,
|
||||
match.SessionInfo.Version,
|
||||
)
|
||||
if err != nil {
|
||||
// If the decryption fails, this implies either that the
|
||||
// client sent an invalid blob, or that the breach hint
|
||||
// caused a match on the txid, but this isn't actually
|
||||
// the right transaction.
|
||||
log.Debugf("Unable to decrypt blob for client %s, "+
|
||||
"breach-txid %s: %v", match.ID,
|
||||
commitTx.TxHash().String(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
justiceDesc := &JusticeDescriptor{
|
||||
BreachedCommitTx: commitTx,
|
||||
SessionInfo: match.SessionInfo,
|
||||
JusticeKit: justiceKit,
|
||||
}
|
||||
successes = append(successes, justiceDesc)
|
||||
}
|
||||
|
||||
// TODO(conner): mark successfully decrypted blob so that we can
|
||||
// reliably rebroadcast on startup
|
||||
|
||||
// Now, we'll dispatch a punishment for each successful match in
|
||||
// parallel. This will assemble the justice transaction for each and
|
||||
// watch for their confirmation on chain.
|
||||
for _, justiceDesc := range successes {
|
||||
l.wg.Add(1)
|
||||
go l.dispatchPunisher(justiceDesc)
|
||||
}
|
||||
|
||||
return l.cfg.DB.SetLookoutTip(epoch)
|
||||
}
|
||||
|
||||
// dispatchPunisher accepts a justice descriptor corresponding to a successfully
|
||||
// decrypted blob. The punisher will then construct the witness scripts and
|
||||
// witness stacks for the breached outputs. If construction of the justice
|
||||
// transaction is successful, it will be published to the network to retrieve
|
||||
// the funds and claim the watchtower's reward.
|
||||
//
|
||||
// This method MUST be run as a goroutine.
|
||||
func (l *Lookout) dispatchPunisher(desc *JusticeDescriptor) {
|
||||
defer l.wg.Done()
|
||||
|
||||
// Give the justice descriptor to the punisher to construct and publish
|
||||
// the justice transaction. The lookout's quit channel is provided so
|
||||
// that long-running tasks that watch for on-chain events can be
|
||||
// canceled during shutdown since this method is waitgrouped.
|
||||
err := l.cfg.Punisher.Punish(desc, l.quit)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to punish breach-txid %s for %x: %v",
|
||||
desc.SessionInfo.ID,
|
||||
desc.BreachedCommitTx.TxHash().String(), err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Punishment for client %s with breach-txid=%s dispatched",
|
||||
desc.SessionInfo.ID, desc.BreachedCommitTx.TxHash().String())
|
||||
}
|
249
watchtower/lookout/lookout_test.go
Normal file
249
watchtower/lookout/lookout_test.go
Normal file
@ -0,0 +1,249 @@
|
||||
// +build dev
|
||||
|
||||
package lookout_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||
"github.com/lightningnetwork/lnd/watchtower/lookout"
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
||||
)
|
||||
|
||||
type mockPunisher struct {
|
||||
matches chan *lookout.JusticeDescriptor
|
||||
}
|
||||
|
||||
func (p *mockPunisher) Punish(
|
||||
info *lookout.JusticeDescriptor, quit <-chan struct{}) error {
|
||||
|
||||
p.matches <- info
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeArray32(i uint64) [32]byte {
|
||||
var arr [32]byte
|
||||
binary.BigEndian.PutUint64(arr[:], i)
|
||||
return arr
|
||||
}
|
||||
|
||||
func makeArray33(i uint64) [33]byte {
|
||||
var arr [33]byte
|
||||
binary.BigEndian.PutUint64(arr[:], i)
|
||||
return arr
|
||||
}
|
||||
|
||||
func makePubKey(i uint64) [33]byte {
|
||||
var arr [33]byte
|
||||
arr[0] = 0x02
|
||||
if i%2 == 1 {
|
||||
arr[0] |= 0x01
|
||||
}
|
||||
binary.BigEndian.PutUint64(arr[1:], i)
|
||||
return arr
|
||||
}
|
||||
|
||||
func makeArray64(i uint64) [64]byte {
|
||||
var arr [64]byte
|
||||
binary.BigEndian.PutUint64(arr[:], i)
|
||||
return arr
|
||||
}
|
||||
|
||||
func makeAddrSlice(size int) []byte {
|
||||
addr := make([]byte, size)
|
||||
if _, err := io.ReadFull(rand.Reader, addr); err != nil {
|
||||
panic("cannot make addr")
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
func TestLookoutBreachMatching(t *testing.T) {
|
||||
db := wtdb.NewMockDB()
|
||||
|
||||
// Initialize an mock backend to feed the lookout blocks.
|
||||
backend := lookout.NewMockBackend()
|
||||
|
||||
// Initialize a punisher that will feed any successfully constructed
|
||||
// justice descriptors across the matches channel.
|
||||
matches := make(chan *lookout.JusticeDescriptor)
|
||||
punisher := &mockPunisher{matches: matches}
|
||||
|
||||
// With the resources in place, initialize and start our watcher.
|
||||
watcher := lookout.New(&lookout.Config{
|
||||
BlockFetcher: backend,
|
||||
DB: db,
|
||||
EpochRegistrar: backend,
|
||||
Punisher: punisher,
|
||||
})
|
||||
if err := watcher.Start(); err != nil {
|
||||
t.Fatalf("unable to start watcher: %v", err)
|
||||
}
|
||||
|
||||
// Create two sessions, representing two distinct clients.
|
||||
sessionInfo1 := &wtdb.SessionInfo{
|
||||
ID: makeArray33(1),
|
||||
MaxUpdates: 10,
|
||||
RewardAddress: makeAddrSlice(22),
|
||||
}
|
||||
sessionInfo2 := &wtdb.SessionInfo{
|
||||
ID: makeArray33(2),
|
||||
MaxUpdates: 10,
|
||||
RewardAddress: makeAddrSlice(22),
|
||||
}
|
||||
|
||||
// Insert both sessions into the watchtower's database.
|
||||
err := db.InsertSessionInfo(sessionInfo1)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to insert session info: %v", err)
|
||||
}
|
||||
err = db.InsertSessionInfo(sessionInfo2)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to insert session info: %v", err)
|
||||
}
|
||||
|
||||
// Construct two distinct transactions, that will be used to test the
|
||||
// breach hint matching.
|
||||
tx := wire.NewMsgTx(wire.TxVersion)
|
||||
hash1 := tx.TxHash()
|
||||
|
||||
tx2 := wire.NewMsgTx(wire.TxVersion + 1)
|
||||
hash2 := tx2.TxHash()
|
||||
|
||||
if bytes.Equal(hash1[:], hash2[:]) {
|
||||
t.Fatalf("breach txids should be different")
|
||||
}
|
||||
|
||||
// Construct a justice kit for each possible breach transaction.
|
||||
blob1 := &blob.JusticeKit{
|
||||
SweepAddress: makeAddrSlice(22),
|
||||
RevocationPubKey: makePubKey(1),
|
||||
LocalDelayPubKey: makePubKey(1),
|
||||
CSVDelay: 144,
|
||||
CommitToLocalSig: makeArray64(1),
|
||||
}
|
||||
blob2 := &blob.JusticeKit{
|
||||
SweepAddress: makeAddrSlice(22),
|
||||
RevocationPubKey: makePubKey(2),
|
||||
LocalDelayPubKey: makePubKey(2),
|
||||
CSVDelay: 144,
|
||||
CommitToLocalSig: makeArray64(2),
|
||||
}
|
||||
|
||||
// Encrypt the first justice kit under the txid of the first txn.
|
||||
encBlob1, err := blob1.Encrypt(hash1[:], 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to encrypt sweep detail 1: %v", err)
|
||||
}
|
||||
|
||||
// Encrypt the second justice kit under the txid of the second txn.
|
||||
encBlob2, err := blob2.Encrypt(hash2[:], 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to encrypt sweep detail 2: %v", err)
|
||||
}
|
||||
|
||||
// Add both state updates to the tower's database.
|
||||
txBlob1 := &wtdb.SessionStateUpdate{
|
||||
ID: makeArray33(1),
|
||||
Hint: wtdb.NewBreachHintFromHash(&hash1),
|
||||
EncryptedBlob: encBlob1,
|
||||
SeqNum: 1,
|
||||
}
|
||||
txBlob2 := &wtdb.SessionStateUpdate{
|
||||
ID: makeArray33(2),
|
||||
Hint: wtdb.NewBreachHintFromHash(&hash2),
|
||||
EncryptedBlob: encBlob2,
|
||||
SeqNum: 1,
|
||||
}
|
||||
if _, err := db.InsertStateUpdate(txBlob1); err != nil {
|
||||
t.Fatalf("unable to add tx to db: %v", err)
|
||||
}
|
||||
if _, err := db.InsertStateUpdate(txBlob2); err != nil {
|
||||
t.Fatalf("unable to add tx to db: %v", err)
|
||||
}
|
||||
|
||||
// Create a block containing the first transaction, connecting this
|
||||
// block should match the first state update's breach hint.
|
||||
block := &wire.MsgBlock{
|
||||
Header: wire.BlockHeader{
|
||||
Nonce: 1,
|
||||
},
|
||||
Transactions: []*wire.MsgTx{tx},
|
||||
}
|
||||
blockHash := block.BlockHash()
|
||||
epoch := &chainntnfs.BlockEpoch{
|
||||
Hash: &blockHash,
|
||||
Height: 1,
|
||||
}
|
||||
|
||||
// Connect the block via our mock backend.
|
||||
backend.ConnectEpoch(epoch, block)
|
||||
|
||||
// This should trigger dispatch of the justice kit for the first tx.
|
||||
select {
|
||||
case match := <-matches:
|
||||
txid := match.BreachedCommitTx.TxHash()
|
||||
if !bytes.Equal(txid[:], hash1[:]) {
|
||||
t.Fatalf("matched breach did not match tx1's txid")
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("breach tx1 was not matched")
|
||||
}
|
||||
|
||||
// Ensure that at most one txn was matched as a result of connecting the
|
||||
// first block.
|
||||
select {
|
||||
case <-matches:
|
||||
t.Fatalf("only one txn should have been matched")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
|
||||
// Now, construct a second block containing the second breach
|
||||
// transaction.
|
||||
block2 := &wire.MsgBlock{
|
||||
Header: wire.BlockHeader{
|
||||
Nonce: 2,
|
||||
},
|
||||
Transactions: []*wire.MsgTx{tx2},
|
||||
}
|
||||
blockHash2 := block2.BlockHash()
|
||||
epoch2 := &chainntnfs.BlockEpoch{
|
||||
Hash: &blockHash2,
|
||||
Height: 2,
|
||||
}
|
||||
|
||||
// Verify that the block hashes do no collide, otherwise the mock
|
||||
// backend may not function properly.
|
||||
if bytes.Equal(blockHash[:], blockHash2[:]) {
|
||||
t.Fatalf("block hashes should be different")
|
||||
}
|
||||
|
||||
// Connect the second block, such that the block is delivered via the
|
||||
// epoch stream.
|
||||
backend.ConnectEpoch(epoch2, block2)
|
||||
|
||||
// This should trigger dispatch of the justice kit for the second txn.
|
||||
select {
|
||||
case match := <-matches:
|
||||
txid := match.BreachedCommitTx.TxHash()
|
||||
if !bytes.Equal(txid[:], hash2[:]) {
|
||||
t.Fatalf("received breach did not match tx2's txid")
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("tx was not matched")
|
||||
}
|
||||
|
||||
// Ensure that at most one txn was matched as a result of connecting the
|
||||
// second block.
|
||||
select {
|
||||
case <-matches:
|
||||
t.Fatalf("only one txn should have been matched")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
}
|
61
watchtower/lookout/mock.go
Normal file
61
watchtower/lookout/mock.go
Normal file
@ -0,0 +1,61 @@
|
||||
// +build dev
|
||||
|
||||
package lookout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
)
|
||||
|
||||
type MockBackend struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
blocks chan *chainntnfs.BlockEpoch
|
||||
epochs map[chainhash.Hash]*wire.MsgBlock
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
func NewMockBackend() *MockBackend {
|
||||
return &MockBackend{
|
||||
blocks: make(chan *chainntnfs.BlockEpoch),
|
||||
epochs: make(map[chainhash.Hash]*wire.MsgBlock),
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockBackend) RegisterBlockEpochNtfn(*chainntnfs.BlockEpoch) (
|
||||
*chainntnfs.BlockEpochEvent, error) {
|
||||
|
||||
return &chainntnfs.BlockEpochEvent{
|
||||
Epochs: m.blocks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockBackend) GetBlock(hash *chainhash.Hash) (*wire.MsgBlock, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
block, ok := m.epochs[*hash]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown block for hash %x", hash)
|
||||
}
|
||||
|
||||
return block, nil
|
||||
}
|
||||
|
||||
func (m *MockBackend) ConnectEpoch(epoch *chainntnfs.BlockEpoch,
|
||||
block *wire.MsgBlock) {
|
||||
|
||||
m.mu.Lock()
|
||||
m.epochs[*epoch.Hash] = block
|
||||
m.mu.Unlock()
|
||||
|
||||
select {
|
||||
case m.blocks <- epoch:
|
||||
case <-m.quit:
|
||||
}
|
||||
}
|
58
watchtower/lookout/punisher.go
Normal file
58
watchtower/lookout/punisher.go
Normal file
@ -0,0 +1,58 @@
|
||||
package lookout
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
)
|
||||
|
||||
// PunisherConfig houses the resources required by the Punisher.
|
||||
type PunisherConfig struct {
|
||||
// PublishTx provides the ability to send a signed transaction to the
|
||||
// network.
|
||||
PublishTx func(*wire.MsgTx) error
|
||||
|
||||
// TODO(conner) add DB tracking and spend ntfn registration to see if
|
||||
// ours confirmed or not
|
||||
}
|
||||
|
||||
// BreachPunisher handles the responsibility of constructing and broadcasting
|
||||
// justice transactions. Justice transactions are constructed from previously
|
||||
// accepted state updates uploaded by the watchtower's clients.
|
||||
type BreachPunisher struct {
|
||||
cfg *PunisherConfig
|
||||
}
|
||||
|
||||
// NewBreachPunisher constructs a new BreachPunisher given a PunisherConfig.
|
||||
func NewBreachPunisher(cfg *PunisherConfig) *BreachPunisher {
|
||||
return &BreachPunisher{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Punish constructs a justice transaction given a JusticeDescriptor and
|
||||
// publishes is it to the network.
|
||||
func (p *BreachPunisher) Punish(desc *JusticeDescriptor, quit <-chan struct{}) error {
|
||||
justiceTxn, err := desc.CreateJusticeTxn()
|
||||
if err != nil {
|
||||
log.Errorf("Unable to create justice txn for "+
|
||||
"client=%s with breach-txid=%x: %v",
|
||||
desc.SessionInfo.ID, desc.BreachedCommitTx.TxHash(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Publishing justice transaction txid=%x for client=%s",
|
||||
justiceTxn.TxHash(), desc.SessionInfo.ID)
|
||||
|
||||
err = p.cfg.PublishTx(justiceTxn)
|
||||
if err != nil && err != lnwallet.ErrDoubleSpend {
|
||||
log.Errorf("Unable to publish justice txn for client=%s",
|
||||
"with breach-txid=%x: %v",
|
||||
desc.SessionInfo.ID, desc.BreachedCommitTx.TxHash(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(conner): register for spend and remove from db after
|
||||
// confirmation
|
||||
|
||||
return nil
|
||||
}
|
@ -2,16 +2,23 @@
|
||||
|
||||
package wtdb
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
)
|
||||
|
||||
type MockDB struct {
|
||||
mu sync.Mutex
|
||||
sessions map[SessionID]*SessionInfo
|
||||
mu sync.Mutex
|
||||
lastEpoch *chainntnfs.BlockEpoch
|
||||
sessions map[SessionID]*SessionInfo
|
||||
blobs map[BreachHint]map[SessionID]*SessionStateUpdate
|
||||
}
|
||||
|
||||
func NewMockDB() *MockDB {
|
||||
return &MockDB{
|
||||
sessions: make(map[SessionID]*SessionInfo),
|
||||
blobs: make(map[BreachHint]map[SessionID]*SessionStateUpdate),
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +36,13 @@ func (db *MockDB) InsertStateUpdate(update *SessionStateUpdate) (uint16, error)
|
||||
return info.LastApplied, err
|
||||
}
|
||||
|
||||
sessionsToUpdates, ok := db.blobs[update.Hint]
|
||||
if !ok {
|
||||
sessionsToUpdates = make(map[SessionID]*SessionStateUpdate)
|
||||
db.blobs[update.Hint] = sessionsToUpdates
|
||||
}
|
||||
sessionsToUpdates[update.ID] = update
|
||||
|
||||
return info.LastApplied, nil
|
||||
}
|
||||
|
||||
@ -55,3 +69,46 @@ func (db *MockDB) InsertSessionInfo(info *SessionInfo) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *MockDB) GetLookoutTip() (*chainntnfs.BlockEpoch, error) {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
return db.lastEpoch, nil
|
||||
}
|
||||
|
||||
func (db *MockDB) QueryMatches(breachHints []BreachHint) ([]Match, error) {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
var matches []Match
|
||||
for _, hint := range breachHints {
|
||||
sessionsToUpdates, ok := db.blobs[hint]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for id, update := range sessionsToUpdates {
|
||||
info, ok := db.sessions[id]
|
||||
if !ok {
|
||||
panic("session not found")
|
||||
}
|
||||
|
||||
match := Match{
|
||||
ID: id,
|
||||
SeqNum: update.SeqNum,
|
||||
Hint: hint,
|
||||
EncryptedBlob: update.EncryptedBlob,
|
||||
SessionInfo: info,
|
||||
}
|
||||
matches = append(matches, match)
|
||||
}
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
func (db *MockDB) SetLookoutTip(epoch *chainntnfs.BlockEpoch) error {
|
||||
db.lastEpoch = epoch
|
||||
return nil
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package wtdb
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
)
|
||||
|
||||
@ -34,6 +35,11 @@ var (
|
||||
// number larger than the session's max number of updates.
|
||||
ErrSessionConsumed = errors.New("all session updates have been " +
|
||||
"consumed")
|
||||
|
||||
// ErrFeeExceedsInputs signals that the total input value of breaching
|
||||
// commitment txn is insufficient to cover the fees required to sweep
|
||||
// it.
|
||||
ErrFeeExceedsInputs = errors.New("sweep fee exceeds input values")
|
||||
)
|
||||
|
||||
// SessionInfo holds the negotiated session parameters for single session id,
|
||||
@ -103,3 +109,58 @@ func (s *SessionInfo) AcceptUpdateSequence(seqNum, lastApplied uint16) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComputeSweepOutputs splits the total funds in a breaching commitment
|
||||
// transaction between the victim and the tower, according to the sweep fee rate
|
||||
// and reward rate. The fees are first subtracted from the overall total, before
|
||||
// splitting the remaining balance amongst the victim and tower.
|
||||
func (s *SessionInfo) ComputeSweepOutputs(totalAmt btcutil.Amount,
|
||||
txVSize int64) (btcutil.Amount, btcutil.Amount, error) {
|
||||
|
||||
txFee := s.SweepFeeRate.FeeForWeight(txVSize)
|
||||
if txFee > totalAmt {
|
||||
return 0, 0, ErrFeeExceedsInputs
|
||||
}
|
||||
|
||||
totalAmt -= txFee
|
||||
|
||||
// Apply the reward rate to the remaining total, specified in millionths
|
||||
// of the available balance.
|
||||
rewardAmt := (totalAmt*btcutil.Amount(s.RewardRate) + 999999) / 1000000
|
||||
sweepAmt := totalAmt - rewardAmt
|
||||
|
||||
// TODO(conner): check dustiness
|
||||
|
||||
return sweepAmt, rewardAmt, nil
|
||||
}
|
||||
|
||||
// Match is returned in response to a database query for a breach hints
|
||||
// contained in a particular block. The match encapsulates all data required to
|
||||
// properly decrypt a client's encrypted blob, and pursue action on behalf of
|
||||
// the victim by reconstructing the justice transaction and broadcasting it to
|
||||
// the network.
|
||||
//
|
||||
// NOTE: It is possible for a match to cause a false positive, since they are
|
||||
// matched on a prefix of the txid. In such an event, the likely behavior is
|
||||
// that the payload will fail to decrypt.
|
||||
type Match struct {
|
||||
// ID is the session id of the client who uploaded the state update.
|
||||
ID SessionID
|
||||
|
||||
// SeqNum is the session sequence number occupied by the client's state
|
||||
// update. Together with ID, this allows the tower to derive the
|
||||
// appropriate nonce for decryption.
|
||||
SeqNum uint16
|
||||
|
||||
// Hint is the breach hint that triggered the match.
|
||||
Hint BreachHint
|
||||
|
||||
// EncryptedBlob is the encrypted payload containing the justice kit
|
||||
// uploaded by the client.
|
||||
EncryptedBlob []byte
|
||||
|
||||
// SessionInfo is the contract negotiated between tower and client, that
|
||||
// provides input parameters such as fee rate, reward rate, and reward
|
||||
// address when attempting to reconstruct the justice transaction.
|
||||
SessionInfo *SessionInfo
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user