Merge pull request #4576 from cfromknecht/anchor-justice-txn
watchtower: conditionally reconstruct justice txns for anchor channels
This commit is contained in:
commit
50976e5418
@ -100,6 +100,15 @@ type PubKey [33]byte
|
||||
// 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
|
||||
@ -187,17 +196,33 @@ func (b *JusticeKit) HasCommitToRemoteOutput() bool {
|
||||
}
|
||||
|
||||
// CommitToRemoteWitnessScript returns the witness script for the commitment
|
||||
// to-remote p2wkh output, which is the pubkey itself.
|
||||
// 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[:], btcec.S256(),
|
||||
)
|
||||
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 is a regular p2wkh.
|
||||
// to-remote output, which consists of a single signature satisfying either the
|
||||
// legacy or anchor witness scripts.
|
||||
// <to-remote-sig>
|
||||
func (b *JusticeKit) CommitToRemoteWitnessStack() ([][]byte, error) {
|
||||
toRemoteSig, err := b.CommitToRemoteSig.ToSignature()
|
||||
@ -218,11 +243,11 @@ func (b *JusticeKit) CommitToRemoteWitnessStack() ([][]byte, error) {
|
||||
//
|
||||
// NOTE: It is the caller's responsibility to ensure that this method is only
|
||||
// called once for a given (nonce, key) pair.
|
||||
func (b *JusticeKit) Encrypt(key BreachKey, blobType Type) ([]byte, error) {
|
||||
func (b *JusticeKit) Encrypt(key BreachKey) ([]byte, error) {
|
||||
// Encode the plaintext using the provided version, to obtain the
|
||||
// plaintext bytes.
|
||||
var ptxtBuf bytes.Buffer
|
||||
err := b.encode(&ptxtBuf, blobType)
|
||||
err := b.encode(&ptxtBuf, b.BlobType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -236,7 +261,7 @@ func (b *JusticeKit) Encrypt(key BreachKey, blobType Type) ([]byte, error) {
|
||||
// Allocate the ciphertext, which will contain the nonce, encrypted
|
||||
// plaintext and MAC.
|
||||
plaintext := ptxtBuf.Bytes()
|
||||
ciphertext := make([]byte, Size(blobType))
|
||||
ciphertext := make([]byte, Size(b.BlobType))
|
||||
|
||||
// Generate a random 24-byte nonce in the ciphertext's prefix.
|
||||
nonce := ciphertext[:NonceSize]
|
||||
@ -284,7 +309,9 @@ func Decrypt(key BreachKey, ciphertext []byte,
|
||||
|
||||
// If decryption succeeded, we will then decode the plaintext bytes
|
||||
// using the specified blob version.
|
||||
boj := &JusticeKit{}
|
||||
boj := &JusticeKit{
|
||||
BlobType: blobType,
|
||||
}
|
||||
err = boj.decode(bytes.NewReader(plaintext), blobType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func makePubKey(i uint64) blob.PubKey {
|
||||
@ -149,6 +150,7 @@ func TestBlobJusticeKitEncryptDecrypt(t *testing.T) {
|
||||
|
||||
func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
|
||||
boj := &blob.JusticeKit{
|
||||
BlobType: test.encVersion,
|
||||
SweepAddress: test.sweepAddr,
|
||||
RevocationPubKey: test.revPubKey,
|
||||
LocalDelayPubKey: test.delayPubKey,
|
||||
@ -169,7 +171,7 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
|
||||
|
||||
// Encrypt the blob plaintext using the generated key and
|
||||
// target version for this test.
|
||||
ctxt, err := boj.Encrypt(key, test.encVersion)
|
||||
ctxt, err := boj.Encrypt(key)
|
||||
if err != test.encErr {
|
||||
t.Fatalf("unable to encrypt blob: %v", err)
|
||||
} else if test.encErr != nil {
|
||||
@ -213,15 +215,48 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) {
|
||||
}
|
||||
}
|
||||
|
||||
type remoteWitnessTest struct {
|
||||
name string
|
||||
blobType blob.Type
|
||||
expWitnessScript func(pk *btcec.PublicKey) []byte
|
||||
}
|
||||
|
||||
// TestJusticeKitRemoteWitnessConstruction tests that a JusticeKit returns the
|
||||
// proper to-remote witnes script and to-remote witness stack. This should be
|
||||
// equivalent to p2wkh spend.
|
||||
func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
|
||||
tests := []remoteWitnessTest{
|
||||
{
|
||||
name: "legacy commitment",
|
||||
blobType: blob.Type(blob.FlagCommitOutputs),
|
||||
expWitnessScript: func(pk *btcec.PublicKey) []byte {
|
||||
return pk.SerializeCompressed()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anchor commitment",
|
||||
blobType: blob.Type(blob.FlagCommitOutputs |
|
||||
blob.FlagAnchorChannel),
|
||||
expWitnessScript: func(pk *btcec.PublicKey) []byte {
|
||||
script, _ := input.CommitScriptToRemoteConfirmed(pk)
|
||||
return script
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testJusticeKitRemoteWitnessConstruction(t, test)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testJusticeKitRemoteWitnessConstruction(
|
||||
t *testing.T, test remoteWitnessTest) {
|
||||
|
||||
// Generate the to-remote pubkey.
|
||||
toRemotePrivKey, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate to-remote priv key: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Copy the to-remote pubkey into the format expected by our justice
|
||||
// kit.
|
||||
@ -232,19 +267,15 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
|
||||
// doesn't matter as we won't be validating the signature's validity.
|
||||
digest := bytes.Repeat([]byte("a"), 32)
|
||||
rawToRemoteSig, err := toRemotePrivKey.Sign(digest)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate to-remote signature: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Convert the DER-encoded signature into a fixed-size sig.
|
||||
commitToRemoteSig, err := lnwire.NewSigFromSignature(rawToRemoteSig)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to convert raw to-remote signature to "+
|
||||
"Sig: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Populate the justice kit fields relevant to the to-remote output.
|
||||
justiceKit := &blob.JusticeKit{
|
||||
BlobType: test.blobType,
|
||||
CommitToRemotePubKey: toRemotePubKey,
|
||||
CommitToRemoteSig: commitToRemoteSig,
|
||||
}
|
||||
@ -252,29 +283,16 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
|
||||
// Now, compute the to-remote witness script returned by the justice
|
||||
// kit.
|
||||
toRemoteScript, err := justiceKit.CommitToRemoteWitnessScript()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to compute to-remote witness script: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Assert this is exactly the to-remote, compressed pubkey.
|
||||
if !bytes.Equal(toRemoteScript, toRemotePubKey[:]) {
|
||||
t.Fatalf("to-remote witness script should be equal to "+
|
||||
"to-remote pubkey, want: %x, got %x",
|
||||
toRemotePubKey[:], toRemoteScript)
|
||||
}
|
||||
expToRemoteScript := test.expWitnessScript(toRemotePrivKey.PubKey())
|
||||
require.Equal(t, expToRemoteScript, toRemoteScript)
|
||||
|
||||
// Next, compute the to-remote witness stack, which should be a p2wkh
|
||||
// witness stack consisting solely of a signature.
|
||||
toRemoteWitnessStack, err := justiceKit.CommitToRemoteWitnessStack()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to compute to-remote witness stack: %v", err)
|
||||
}
|
||||
|
||||
// Assert that the witness stack only has one element.
|
||||
if len(toRemoteWitnessStack) != 1 {
|
||||
t.Fatalf("to-remote witness stack should be of length 1, is %d",
|
||||
len(toRemoteWitnessStack))
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Compute the expected first element, by appending a sighash all byte
|
||||
// to our raw DER-encoded signature.
|
||||
@ -282,13 +300,11 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
|
||||
rawToRemoteSig.Serialize(), byte(txscript.SigHashAll),
|
||||
)
|
||||
|
||||
// Assert that the expected signature matches the first element in the
|
||||
// witness stack.
|
||||
if !bytes.Equal(rawToRemoteSigWithSigHash, toRemoteWitnessStack[0]) {
|
||||
t.Fatalf("mismatched sig in to-remote witness stack, want: %v, "+
|
||||
"got: %v", rawToRemoteSigWithSigHash,
|
||||
toRemoteWitnessStack[0])
|
||||
// Assert that the expected witness stack is returned.
|
||||
expWitnessStack := [][]byte{
|
||||
rawToRemoteSigWithSigHash,
|
||||
}
|
||||
require.Equal(t, expWitnessStack, toRemoteWitnessStack)
|
||||
|
||||
// Finally, set the CommitToRemotePubKey to be a blank value.
|
||||
justiceKit.CommitToRemotePubKey = blob.PubKey{}
|
||||
@ -297,9 +313,7 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
|
||||
// ErrNoCommitToRemoteOutput since a valid pubkey could not be parsed
|
||||
// from CommitToRemotePubKey.
|
||||
_, err = justiceKit.CommitToRemoteWitnessScript()
|
||||
if err != blob.ErrNoCommitToRemoteOutput {
|
||||
t.Fatalf("expected ErrNoCommitToRemoteOutput, got: %v", err)
|
||||
}
|
||||
require.Error(t, blob.ErrNoCommitToRemoteOutput, err)
|
||||
}
|
||||
|
||||
// TestJusticeKitToLocalWitnessConstruction tests that a JusticeKit returns the
|
||||
@ -310,14 +324,10 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) {
|
||||
|
||||
// Generate the revocation and delay private keys.
|
||||
revPrivKey, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate revocation priv key: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
delayPrivKey, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate delay priv key: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Copy the revocation and delay pubkeys into the format expected by our
|
||||
// justice kit.
|
||||
@ -331,16 +341,11 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) {
|
||||
// doesn't matter as we won't be validating the signature's validity.
|
||||
digest := bytes.Repeat([]byte("a"), 32)
|
||||
rawRevSig, err := revPrivKey.Sign(digest)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate revocation signature: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Convert the DER-encoded signature into a fixed-size sig.
|
||||
commitToLocalSig, err := lnwire.NewSigFromSignature(rawRevSig)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to convert raw revocation signature to "+
|
||||
"Sig: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Populate the justice kit with fields relevant to the to-local output.
|
||||
justiceKit := &blob.JusticeKit{
|
||||
@ -355,52 +360,29 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) {
|
||||
expToLocalScript, err := input.CommitScriptToSelf(
|
||||
csvDelay, delayPrivKey.PubKey(), revPrivKey.PubKey(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate expected to-local script: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Compute the to-local script that is returned by the justice kit.
|
||||
toLocalScript, err := justiceKit.CommitToLocalWitnessScript()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to compute to-local witness script: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Assert that the expected to-local script matches the actual script.
|
||||
if !bytes.Equal(expToLocalScript, toLocalScript) {
|
||||
t.Fatalf("mismatched to-local witness script, want: %v, got %v",
|
||||
expToLocalScript, toLocalScript)
|
||||
}
|
||||
require.Equal(t, expToLocalScript, toLocalScript)
|
||||
|
||||
// Next, compute the to-local witness stack returned by the justice kit.
|
||||
toLocalWitnessStack, err := justiceKit.CommitToLocalRevokeWitnessStack()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to compute to-local witness stack: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// A valid witness that spends the revocation path should have exactly
|
||||
// two elements on the stack.
|
||||
if len(toLocalWitnessStack) != 2 {
|
||||
t.Fatalf("to-local witness stack should be of length 2, is %d",
|
||||
len(toLocalWitnessStack))
|
||||
}
|
||||
|
||||
// First, we'll verify that the top element is 0x01, which triggers the
|
||||
// revocation path within the to-local witness script.
|
||||
if !bytes.Equal(toLocalWitnessStack[1], []byte{0x01}) {
|
||||
t.Fatalf("top item on witness stack should be 0x01, found: %v",
|
||||
toLocalWitnessStack[1])
|
||||
}
|
||||
|
||||
// Next, compute the expected signature in the bottom element of the
|
||||
// stack, by appending a sighash all flag to the raw DER signature.
|
||||
// Compute the expected signature in the bottom element of the stack, by
|
||||
// appending a sighash all flag to the raw DER signature.
|
||||
rawRevSigWithSigHash := append(
|
||||
rawRevSig.Serialize(), byte(txscript.SigHashAll),
|
||||
)
|
||||
|
||||
// Assert that the second element on the stack matches our expected
|
||||
// signature under the revocation pubkey.
|
||||
if !bytes.Equal(rawRevSigWithSigHash, toLocalWitnessStack[0]) {
|
||||
t.Fatalf("mismatched sig in to-local witness stack, want: %v, "+
|
||||
"got: %v", rawRevSigWithSigHash, toLocalWitnessStack[0])
|
||||
// Finally, validate against our expected witness stack.
|
||||
expWitnessStack := [][]byte{
|
||||
rawRevSigWithSigHash,
|
||||
{1},
|
||||
}
|
||||
require.Equal(t, expWitnessStack, toLocalWitnessStack)
|
||||
}
|
||||
|
@ -14,11 +14,16 @@ const (
|
||||
// include the reward script negotiated during session creation. Without
|
||||
// the flag, there is only one output sweeping clients funds back to
|
||||
// them solely.
|
||||
FlagReward Flag = 1 << iota
|
||||
FlagReward Flag = 1
|
||||
|
||||
// FlagCommitOutputs signals that the blob contains the information
|
||||
// required to sweep commitment outputs.
|
||||
FlagCommitOutputs
|
||||
FlagCommitOutputs Flag = 1 << 1
|
||||
|
||||
// FlagAnchorChannel signals that this blob is meant to spend an anchor
|
||||
// channel, and therefore must expect a P2WSH-style to-remote output if
|
||||
// one exists.
|
||||
FlagAnchorChannel Flag = 1 << 2
|
||||
)
|
||||
|
||||
// Type returns a Type consisting solely of this flag enabled.
|
||||
@ -33,6 +38,8 @@ func (f Flag) String() string {
|
||||
return "FlagReward"
|
||||
case FlagCommitOutputs:
|
||||
return "FlagCommitOutputs"
|
||||
case FlagAnchorChannel:
|
||||
return "FlagAnchorChannel"
|
||||
default:
|
||||
return "FlagUnknown"
|
||||
}
|
||||
@ -50,6 +57,11 @@ const (
|
||||
// controlled by the user, and does not give the tower a reward.
|
||||
TypeAltruistCommit = Type(FlagCommitOutputs)
|
||||
|
||||
// TypeAltruistAnchorCommit sweeps only commitment outputs from an
|
||||
// anchor commitment to a sweep address controlled by the user, and does
|
||||
// not give the tower a reward.
|
||||
TypeAltruistAnchorCommit = Type(FlagCommitOutputs | FlagAnchorChannel)
|
||||
|
||||
// TypeRewardCommit sweeps only commitment outputs to a sweep address
|
||||
// controlled by the user, and pays a negotiated reward to the tower.
|
||||
TypeRewardCommit = Type(FlagCommitOutputs | FlagReward)
|
||||
@ -70,10 +82,16 @@ func TypeFromFlags(flags ...Flag) Type {
|
||||
return typ
|
||||
}
|
||||
|
||||
// IsAnchorChannel returns true if the blob type is for an anchor channel.
|
||||
func (t Type) IsAnchorChannel() bool {
|
||||
return t.Has(FlagAnchorChannel)
|
||||
}
|
||||
|
||||
// knownFlags maps the supported flags to their name.
|
||||
var knownFlags = map[Flag]struct{}{
|
||||
FlagReward: {},
|
||||
FlagCommitOutputs: {},
|
||||
FlagAnchorChannel: {},
|
||||
}
|
||||
|
||||
// String returns a human readable description of a Type.
|
||||
|
@ -18,17 +18,17 @@ var typeStringTests = []typeStringTest{
|
||||
{
|
||||
name: "commit no-reward",
|
||||
typ: blob.TypeAltruistCommit,
|
||||
expStr: "[FlagCommitOutputs|No-FlagReward]",
|
||||
expStr: "[No-FlagAnchorChannel|FlagCommitOutputs|No-FlagReward]",
|
||||
},
|
||||
{
|
||||
name: "commit reward",
|
||||
typ: blob.TypeRewardCommit,
|
||||
expStr: "[FlagCommitOutputs|FlagReward]",
|
||||
expStr: "[No-FlagAnchorChannel|FlagCommitOutputs|FlagReward]",
|
||||
},
|
||||
{
|
||||
name: "unknown flag",
|
||||
typ: unknownFlag.Type(),
|
||||
expStr: "0000000000010000[No-FlagCommitOutputs|No-FlagReward]",
|
||||
expStr: "0000000000010000[No-FlagAnchorChannel|No-FlagCommitOutputs|No-FlagReward]",
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,7 @@ type breachedInput struct {
|
||||
txOut *wire.TxOut
|
||||
outPoint wire.OutPoint
|
||||
witness [][]byte
|
||||
sequence uint32
|
||||
}
|
||||
|
||||
// commitToLocalInput extracts the information required to spend the commit
|
||||
@ -104,20 +105,35 @@ func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) {
|
||||
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 := input.CommitScriptUnencumbered(
|
||||
toRemotePubKey,
|
||||
var (
|
||||
toRemoteScriptHash []byte
|
||||
toRemoteSequence uint32
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
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, 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 = input.CommitScriptUnencumbered(
|
||||
toRemotePubKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Locate the to-remote output on the breaching commitment transaction.
|
||||
@ -146,6 +162,7 @@ func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) {
|
||||
txOut: toRemoteTxOut,
|
||||
outPoint: toRemoteOutPoint,
|
||||
witness: buildWitness(witnessStack, toRemoteScript),
|
||||
sequence: toRemoteSequence,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -164,6 +181,7 @@ func (p *JusticeDescriptor) assembleJusticeTxn(txWeight int64,
|
||||
totalAmt += btcutil.Amount(input.txOut.Value)
|
||||
justiceTxn.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: input.outPoint,
|
||||
Sequence: input.sequence,
|
||||
})
|
||||
}
|
||||
|
||||
@ -266,8 +284,13 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) {
|
||||
// An older ToLocalPenaltyWitnessSize constant used to underestimate the
|
||||
// size by one byte. The diferrence in weight can cause different output
|
||||
// values on the sweep transaction, so we mimic the original bug to
|
||||
// avoid invalidating signatures by older clients.
|
||||
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1)
|
||||
// avoid invalidating signatures by older clients. For anchor channels
|
||||
// we correct this and use the correct witness size.
|
||||
if p.JusticeKit.BlobType.IsAnchorChannel() {
|
||||
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize)
|
||||
} else {
|
||||
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1)
|
||||
}
|
||||
|
||||
sweepInputs = append(sweepInputs, toLocalInput)
|
||||
|
||||
@ -279,8 +302,13 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
|
||||
sweepInputs = append(sweepInputs, toRemoteInput)
|
||||
|
||||
if p.JusticeKit.BlobType.IsAnchorChannel() {
|
||||
weightEstimate.AddWitnessInput(input.ToRemoteConfirmedWitnessSize)
|
||||
} else {
|
||||
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(conner): sweep htlc outputs
|
||||
|
@ -1,7 +1,6 @@
|
||||
package lookout_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -11,7 +10,6 @@ import (
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/txsort"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
@ -20,6 +18,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtmock"
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtpolicy"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const csvDelay uint32 = 144
|
||||
@ -51,6 +50,8 @@ var (
|
||||
)
|
||||
|
||||
altruistCommitType = blob.FlagCommitOutputs.Type()
|
||||
|
||||
altruistAnchorCommitType = blob.TypeAltruistAnchorCommit
|
||||
)
|
||||
|
||||
// TestJusticeDescriptor asserts that a JusticeDescriptor is able to produce the
|
||||
@ -68,6 +69,10 @@ func TestJusticeDescriptor(t *testing.T) {
|
||||
name: "altruist and commit type",
|
||||
blobType: altruistCommitType,
|
||||
},
|
||||
{
|
||||
name: "altruist anchor commit type",
|
||||
blobType: altruistAnchorCommitType,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@ -78,6 +83,8 @@ func TestJusticeDescriptor(t *testing.T) {
|
||||
}
|
||||
|
||||
func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
|
||||
isAnchorChannel := blobType.IsAnchorChannel()
|
||||
|
||||
const (
|
||||
localAmount = btcutil.Amount(100000)
|
||||
remoteAmount = btcutil.Amount(200000)
|
||||
@ -106,20 +113,59 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
|
||||
toLocalScript, err := input.CommitScriptToSelf(
|
||||
csvDelay, toLocalPK, revPK,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create to-local script: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Compute the to-local witness script hash.
|
||||
toLocalScriptHash, err := input.WitnessScriptHash(toLocalScript)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create to-local witness script hash: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Compute the to-remote witness script hash.
|
||||
toRemoteScriptHash, err := input.CommitScriptUnencumbered(toRemotePK)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create to-remote script: %v", err)
|
||||
// Compute the to-remote redeem script, witness script hash, and
|
||||
// sequence numbers.
|
||||
//
|
||||
// NOTE: This is pretty subtle.
|
||||
//
|
||||
// The actual redeem script for a p2wkh output is just the pubkey, but
|
||||
// the witness sighash calculation injects the classic p2kh script:
|
||||
// OP_DUP OP_HASH160 <pubkey-hash160> OP_EQUALVERIFY OP_CHECKSIG. When
|
||||
// signing for p2wkh we don't pass the raw pubkey as the witness script
|
||||
// to the sign descriptor (since that's also not a valid script).
|
||||
// Instead we give it the _pkscript_ of the form OP_0 <pubkey-hash160>
|
||||
// from which pubkey-hash160 is extracted during sighash calculation.
|
||||
//
|
||||
// On the other hand, signing for the anchor p2wsh to-remote outputs
|
||||
// requires the sign descriptor to contain the redeem script ver batim.
|
||||
// This difference in behavior forces us to use a distinct
|
||||
// toRemoteSigningScript to handle both cases.
|
||||
var (
|
||||
toRemoteSequence uint32
|
||||
toRemoteRedeemScript []byte
|
||||
toRemoteScriptHash []byte
|
||||
toRemoteSigningScript []byte
|
||||
)
|
||||
if isAnchorChannel {
|
||||
toRemoteSequence = 1
|
||||
toRemoteRedeemScript, err = input.CommitScriptToRemoteConfirmed(
|
||||
toRemotePK,
|
||||
)
|
||||
require.Nil(t, err)
|
||||
|
||||
toRemoteScriptHash, err = input.WitnessScriptHash(
|
||||
toRemoteRedeemScript,
|
||||
)
|
||||
require.Nil(t, err)
|
||||
|
||||
// As it should be.
|
||||
toRemoteSigningScript = toRemoteRedeemScript
|
||||
|
||||
} else {
|
||||
toRemoteRedeemScript = toRemotePK.SerializeCompressed()
|
||||
toRemoteScriptHash, err = input.CommitScriptUnencumbered(
|
||||
toRemotePK,
|
||||
)
|
||||
require.Nil(t, err)
|
||||
|
||||
// NOTE: This is the _pkscript_.
|
||||
toRemoteSigningScript = toRemoteScriptHash
|
||||
}
|
||||
|
||||
// Construct the breaching commitment txn, containing the to-local and
|
||||
@ -146,10 +192,19 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
|
||||
// An older ToLocalPenaltyWitnessSize constant used to underestimate the
|
||||
// size by one byte. The diferrence in weight can cause different output
|
||||
// values on the sweep transaction, so we mimic the original bug and
|
||||
// create signatures using the original weight estimate.
|
||||
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1)
|
||||
// create signatures using the original weight estimate. For anchor
|
||||
// channels we fix this and use the correct witness size.
|
||||
if isAnchorChannel {
|
||||
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize)
|
||||
} else {
|
||||
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize - 1)
|
||||
}
|
||||
|
||||
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
|
||||
if isAnchorChannel {
|
||||
weightEstimate.AddWitnessInput(input.ToRemoteConfirmedWitnessSize)
|
||||
} else {
|
||||
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
|
||||
}
|
||||
weightEstimate.AddP2WKHOutput()
|
||||
if blobType.Has(blob.FlagReward) {
|
||||
weightEstimate.AddP2WKHOutput()
|
||||
@ -174,6 +229,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
|
||||
// Begin to assemble the justice kit, starting with the sweep address,
|
||||
// pubkeys, and csv delay.
|
||||
justiceKit := &blob.JusticeKit{
|
||||
BlobType: blobType,
|
||||
SweepAddress: makeAddrSlice(22),
|
||||
CSVDelay: csvDelay,
|
||||
}
|
||||
@ -199,6 +255,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
|
||||
Hash: breachTxID,
|
||||
Index: 1,
|
||||
},
|
||||
Sequence: toRemoteSequence,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -207,9 +264,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
|
||||
totalAmount, int64(txWeight), justiceKit.SweepAddress,
|
||||
sessionInfo.RewardAddress,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to compute justice txouts: %v", err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Attach the txouts and BIP69 sort the resulting transaction.
|
||||
justiceTxn.TxOut = outputs
|
||||
@ -235,7 +290,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
|
||||
KeyLocator: toRemoteKeyLoc,
|
||||
PubKey: toRemotePK,
|
||||
},
|
||||
WitnessScript: toRemoteScriptHash,
|
||||
WitnessScript: toRemoteSigningScript,
|
||||
Output: breachTxn.TxOut[1],
|
||||
SigHashes: hashCache,
|
||||
InputIndex: 1,
|
||||
@ -244,38 +299,26 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
|
||||
|
||||
// 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)
|
||||
}
|
||||
err = blockchain.CheckTransactionSanity(btx)
|
||||
require.Nil(t, 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)
|
||||
}
|
||||
require.Nil(t, 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 := input.CommitSpendNoDelay(
|
||||
signer, toRemoteSignDesc, justiceTxn, false,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to sign to-remote input: %v", err)
|
||||
}
|
||||
toRemoteSigRaw := toRemoteWitness[0][:len(toRemoteWitness[0])-1]
|
||||
toRemoteSigRaw, err := signer.SignOutputRaw(justiceTxn, toRemoteSignDesc)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Convert the DER to-local sig into a fixed-size signature.
|
||||
toLocalSig, err := lnwire.NewSigFromSignature(toLocalSigRaw)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to parse to-local signature: %v", err)
|
||||
}
|
||||
require.Nil(t, 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)
|
||||
}
|
||||
toRemoteSig, err := lnwire.NewSigFromSignature(toRemoteSigRaw)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Complete our justice kit by copying the signatures into the payload.
|
||||
copy(justiceKit.CommitToLocalSig[:], toLocalSig[:])
|
||||
@ -300,9 +343,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
|
||||
// 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)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
// Retrieve the published justice transaction.
|
||||
var wtJusticeTxn *wire.MsgTx
|
||||
@ -321,14 +362,10 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
|
||||
|
||||
// Construct the test's to-remote witness.
|
||||
justiceTxn.TxIn[1].Witness = make([][]byte, 2)
|
||||
justiceTxn.TxIn[1].Witness[0] = append(toRemoteSigRaw,
|
||||
justiceTxn.TxIn[1].Witness[0] = append(toRemoteSigRaw.Serialize(),
|
||||
byte(txscript.SigHashAll))
|
||||
justiceTxn.TxIn[1].Witness[1] = toRemotePK.SerializeCompressed()
|
||||
justiceTxn.TxIn[1].Witness[1] = toRemoteRedeemScript
|
||||
|
||||
// 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))
|
||||
}
|
||||
require.Equal(t, justiceTxn, wtJusticeTxn)
|
||||
}
|
||||
|
@ -137,7 +137,9 @@ func TestLookoutBreachMatching(t *testing.T) {
|
||||
}
|
||||
|
||||
// Construct a justice kit for each possible breach transaction.
|
||||
blobType := blob.FlagCommitOutputs.Type()
|
||||
blob1 := &blob.JusticeKit{
|
||||
BlobType: blobType,
|
||||
SweepAddress: makeAddrSlice(22),
|
||||
RevocationPubKey: makePubKey(1),
|
||||
LocalDelayPubKey: makePubKey(1),
|
||||
@ -145,6 +147,7 @@ func TestLookoutBreachMatching(t *testing.T) {
|
||||
CommitToLocalSig: makeArray64(1),
|
||||
}
|
||||
blob2 := &blob.JusticeKit{
|
||||
BlobType: blobType,
|
||||
SweepAddress: makeAddrSlice(22),
|
||||
RevocationPubKey: makePubKey(2),
|
||||
LocalDelayPubKey: makePubKey(2),
|
||||
@ -156,13 +159,13 @@ func TestLookoutBreachMatching(t *testing.T) {
|
||||
key2 := blob.NewBreachKeyFromHash(&hash2)
|
||||
|
||||
// Encrypt the first justice kit under breach key one.
|
||||
encBlob1, err := blob1.Encrypt(key1, blob.FlagCommitOutputs.Type())
|
||||
encBlob1, err := blob1.Encrypt(key1)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to encrypt sweep detail 1: %v", err)
|
||||
}
|
||||
|
||||
// Encrypt the second justice kit under breach key two.
|
||||
encBlob2, err := blob2.Encrypt(key2, blob.FlagCommitOutputs.Type())
|
||||
encBlob2, err := blob2.Encrypt(key2)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to encrypt sweep detail 2: %v", err)
|
||||
}
|
||||
|
@ -194,6 +194,7 @@ func (t *backupTask) craftSessionPayload(
|
||||
// to-local script, and the remote CSV delay.
|
||||
keyRing := t.breachInfo.KeyRing
|
||||
justiceKit := &blob.JusticeKit{
|
||||
BlobType: t.blobType,
|
||||
SweepAddress: t.sweepPkScript,
|
||||
RevocationPubKey: toBlobPubKey(keyRing.RevocationKey),
|
||||
LocalDelayPubKey: toBlobPubKey(keyRing.ToLocalKey),
|
||||
@ -299,7 +300,7 @@ func (t *backupTask) craftSessionPayload(
|
||||
// Then, we'll encrypt the computed justice kit using the full breach
|
||||
// transaction id, which will allow the tower to recover the contents
|
||||
// after the transaction is seen in the chain or mempool.
|
||||
encBlob, err := justiceKit.Encrypt(key, t.blobType)
|
||||
encBlob, err := justiceKit.Encrypt(key)
|
||||
if err != nil {
|
||||
return hint, nil, err
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user