watchtower/lookout: make justice desciptor taproot ready

This commit is contained in:
Elle Mouton
2023-05-31 09:19:05 +02:00
parent 5960253357
commit c50aa10194
3 changed files with 264 additions and 51 deletions

View File

@@ -176,6 +176,8 @@ func (p *JusticeDescriptor) assembleJusticeTxn(txWeight int64,
return nil, fmt.Errorf("error creating previous output "+ return nil, fmt.Errorf("error creating previous output "+
"fetcher: %v", err) "fetcher: %v", err)
} }
hashes := txscript.NewTxSigHashes(justiceTxn, prevOutFetcher)
for _, inp := range inputs { for _, inp := range inputs {
// Lookup the input's new post-sort position. // Lookup the input's new post-sort position.
i := inputIndex[inp.outPoint] i := inputIndex[inp.outPoint]
@@ -186,7 +188,7 @@ func (p *JusticeDescriptor) assembleJusticeTxn(txWeight int64,
vm, err := txscript.NewEngine( vm, err := txscript.NewEngine(
inp.txOut.PkScript, justiceTxn, i, inp.txOut.PkScript, justiceTxn, i,
txscript.StandardVerifyFlags, txscript.StandardVerifyFlags,
nil, nil, inp.txOut.Value, prevOutFetcher, nil, hashes, inp.txOut.Value, prevOutFetcher,
) )
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -10,6 +10,7 @@ import (
"github.com/btcsuite/btcd/btcutil/txsort" "github.com/btcsuite/btcd/btcutil/txsort"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
@@ -53,6 +54,8 @@ var (
altruistCommitType = blob.FlagCommitOutputs.Type() altruistCommitType = blob.FlagCommitOutputs.Type()
altruistAnchorCommitType = blob.TypeAltruistAnchorCommit altruistAnchorCommitType = blob.TypeAltruistAnchorCommit
altruisticTaprootCommitType = blob.TypeAltruistTaprootCommit
) )
// TestJusticeDescriptor asserts that a JusticeDescriptor is able to produce the // TestJusticeDescriptor asserts that a JusticeDescriptor is able to produce the
@@ -74,6 +77,10 @@ func TestJusticeDescriptor(t *testing.T) {
name: "altruist anchor commit type", name: "altruist anchor commit type",
blobType: altruistAnchorCommitType, blobType: altruistAnchorCommitType,
}, },
{
name: "altruist taproot commit type",
blobType: altruisticTaprootCommitType,
},
} }
for _, test := range tests { for _, test := range tests {
@@ -85,6 +92,7 @@ func TestJusticeDescriptor(t *testing.T) {
func testJusticeDescriptor(t *testing.T, blobType blob.Type) { func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
isAnchorChannel := blobType.IsAnchorChannel() isAnchorChannel := blobType.IsAnchorChannel()
isTaprootChannel := blobType.IsTaprootChannel()
const ( const (
localAmount = btcutil.Amount(100000) localAmount = btcutil.Amount(100000)
@@ -108,15 +116,34 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
toRemoteKeyLoc = signer.AddPrivKey(toRemoteSK) toRemoteKeyLoc = signer.AddPrivKey(toRemoteSK)
) )
// Construct the to-local witness script. var (
toLocalScript, err := input.CommitScriptToSelf( toLocalScript, toLocalScriptHash []byte
csvDelay, toLocalPK, revPK, toLocalCommitTree *input.CommitScriptTree
) )
require.NoError(t, err)
// Compute the to-local witness script hash. if isTaprootChannel {
toLocalScriptHash, err := input.WitnessScriptHash(toLocalScript) toLocalCommitTree, err = input.NewLocalCommitScriptTree(
require.NoError(t, err) csvDelay, toLocalPK, revPK,
)
require.NoError(t, err)
toLocalScript = toLocalCommitTree.RevocationLeaf.Script
toLocalScriptHash, err = input.PayToTaprootScript(
toLocalCommitTree.TaprootKey,
)
require.NoError(t, err)
} else {
// Construct the to-local witness script.
toLocalScript, err = input.CommitScriptToSelf(
csvDelay, toLocalPK, revPK,
)
require.NoError(t, err)
// Compute the to-local witness script hash.
toLocalScriptHash, err = input.WitnessScriptHash(toLocalScript)
require.NoError(t, err)
}
// Compute the to-remote redeem script, witness script hash, and // Compute the to-remote redeem script, witness script hash, and
// sequence numbers. // sequence numbers.
@@ -140,8 +167,37 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
toRemoteRedeemScript []byte toRemoteRedeemScript []byte
toRemoteScriptHash []byte toRemoteScriptHash []byte
toRemoteSigningScript []byte toRemoteSigningScript []byte
toRemoteCtrlBlock []byte
) )
if isAnchorChannel { switch {
case isTaprootChannel:
toRemoteSequence = 1
commitScriptTree, err := input.NewRemoteCommitScriptTree(
toRemotePK,
)
require.NoError(t, err)
toRemoteSigningScript = commitScriptTree.SettleLeaf.Script
toRemoteScriptHash, err = input.PayToTaprootScript(
commitScriptTree.TaprootKey,
)
require.NoError(t, err)
tree := commitScriptTree.TapscriptTree
settleTapleafHash := commitScriptTree.SettleLeaf.TapHash()
settleIdx := tree.LeafProofIndex[settleTapleafHash]
settleMerkleProof := tree.LeafMerkleProofs[settleIdx]
settleControlBlock := settleMerkleProof.ToControlBlock(
&input.TaprootNUMSKey,
)
ctrlBytes, err := settleControlBlock.ToBytes()
require.NoError(t, err)
toRemoteCtrlBlock = ctrlBytes
case isAnchorChannel:
toRemoteSequence = 1 toRemoteSequence = 1
toRemoteRedeemScript, err = input.CommitScriptToRemoteConfirmed( toRemoteRedeemScript, err = input.CommitScriptToRemoteConfirmed(
toRemotePK, toRemotePK,
@@ -155,7 +211,8 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
// As it should be. // As it should be.
toRemoteSigningScript = toRemoteRedeemScript toRemoteSigningScript = toRemoteRedeemScript
} else {
default:
toRemoteRedeemScript = toRemotePK.SerializeCompressed() toRemoteRedeemScript = toRemotePK.SerializeCompressed()
toRemoteScriptHash, err = input.CommitScriptUnencumbered( toRemoteScriptHash, err = input.CommitScriptUnencumbered(
toRemotePK, toRemotePK,
@@ -269,31 +326,82 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
justiceTxn.TxOut = outputs justiceTxn.TxOut = outputs
txsort.InPlaceSort(justiceTxn) txsort.InPlaceSort(justiceTxn)
hashCache := input.NewTxSigHashesV0Only(justiceTxn) var (
toLocalSignDesc *input.SignDescriptor
toRemoteSignDesc *input.SignDescriptor
)
// Create the sign descriptor used to sign for the to-local input. if isTaprootChannel {
toLocalSignDesc := &input.SignDescriptor{ prevOuts := map[wire.OutPoint]*wire.TxOut{
KeyDesc: keychain.KeyDescriptor{ {
KeyLocator: revKeyLoc, Hash: breachTxID,
}, Index: 0,
WitnessScript: toLocalScript, }: breachTxn.TxOut[0],
Output: breachTxn.TxOut[0], {
SigHashes: hashCache, Hash: breachTxID,
InputIndex: 0, Index: 1,
HashType: txscript.SigHashAll, }: breachTxn.TxOut[1],
} }
prevOutputFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
hashCache := txscript.NewTxSigHashes(
justiceTxn, prevOutputFetcher,
)
// Create the sign descriptor used to sign for the to-remote input. toLocalSignDesc = &input.SignDescriptor{
toRemoteSignDesc := &input.SignDescriptor{ KeyDesc: keychain.KeyDescriptor{
KeyDesc: keychain.KeyDescriptor{ KeyLocator: revKeyLoc,
KeyLocator: toRemoteKeyLoc, },
PubKey: toRemotePK, WitnessScript: toLocalScript,
}, Output: breachTxn.TxOut[0],
WitnessScript: toRemoteSigningScript, SigHashes: hashCache,
Output: breachTxn.TxOut[1], PrevOutputFetcher: prevOutputFetcher,
SigHashes: hashCache, InputIndex: 0,
InputIndex: 1, HashType: txscript.SigHashDefault,
HashType: txscript.SigHashAll, SignMethod: input.TaprootScriptSpendSignMethod,
}
toRemoteSignDesc = &input.SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
KeyLocator: toRemoteKeyLoc,
PubKey: toRemotePK,
},
WitnessScript: toRemoteSigningScript,
Output: breachTxn.TxOut[1],
PrevOutputFetcher: prevOutputFetcher,
SigHashes: hashCache,
InputIndex: 1,
HashType: txscript.SigHashDefault,
SignMethod: input.TaprootScriptSpendSignMethod,
}
} else {
hashCache := input.NewTxSigHashesV0Only(justiceTxn)
// Create the sign descriptor used to sign for the to-local
// input.
toLocalSignDesc = &input.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 = &input.SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
KeyLocator: toRemoteKeyLoc,
PubKey: toRemotePK,
},
WitnessScript: toRemoteSigningScript,
Output: breachTxn.TxOut[1],
SigHashes: hashCache,
InputIndex: 1,
HashType: txscript.SigHashAll,
}
} }
// Verify that our test justice transaction is sane. // Verify that our test justice transaction is sane.
@@ -301,21 +409,19 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
err = blockchain.CheckTransactionSanity(btx) err = blockchain.CheckTransactionSanity(btx)
require.Nil(t, err) require.Nil(t, err)
// Compute a DER-encoded signature for the to-local input. // Compute a signature for the to-local input.
toLocalSigRaw, err := signer.SignOutputRaw(justiceTxn, toLocalSignDesc) toLocalSigRaw, err := signer.SignOutputRaw(justiceTxn, toLocalSignDesc)
require.Nil(t, err) require.Nil(t, err)
// Compute the witness for the to-remote input. The first element is a // Compute the witness for the to-remote input.
// DER-encoded signature under the to-remote pubkey. The sighash flag is
// also present, so we trim it.
toRemoteSigRaw, err := signer.SignOutputRaw(justiceTxn, toRemoteSignDesc) toRemoteSigRaw, err := signer.SignOutputRaw(justiceTxn, toRemoteSignDesc)
require.Nil(t, err) require.Nil(t, err)
// Convert the DER to-local sig into a fixed-size signature. // Convert the to-local sig into a fixed-size signature.
toLocalSig, err := lnwire.NewSigFromSignature(toLocalSigRaw) toLocalSig, err := lnwire.NewSigFromSignature(toLocalSigRaw)
require.Nil(t, err) require.Nil(t, err)
// Convert the DER to-remote sig into a fixed-size signature. // Convert the to-remote sig into a fixed-size signature.
toRemoteSig, err := lnwire.NewSigFromSignature(toRemoteSigRaw) toRemoteSig, err := lnwire.NewSigFromSignature(toRemoteSigRaw)
require.Nil(t, err) require.Nil(t, err)
@@ -342,7 +448,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
// Exact retribution on the offender. If no error is returned, we expect // Exact retribution on the offender. If no error is returned, we expect
// the justice transaction to be published via the channel. // the justice transaction to be published via the channel.
err = punisher.Punish(justiceDesc, nil) err = punisher.Punish(justiceDesc, nil)
require.Nil(t, err) require.NoError(t, err)
// Retrieve the published justice transaction. // Retrieve the published justice transaction.
var wtJusticeTxn *wire.MsgTx var wtJusticeTxn *wire.MsgTx
@@ -352,18 +458,59 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
t.Fatalf("punisher did not publish justice txn") t.Fatalf("punisher did not publish justice txn")
} }
// Construct the test's to-local witness. if isTaprootChannel {
justiceTxn.TxIn[0].Witness = make([][]byte, 3) revokeLeaf := txscript.NewBaseTapLeaf(toLocalScript)
justiceTxn.TxIn[0].Witness[0] = append(toLocalSigRaw.Serialize(), outputKey := txscript.ComputeTaprootOutputKey(
byte(txscript.SigHashAll)) &input.TaprootNUMSKey, toLocalCommitTree.TapscriptRoot,
justiceTxn.TxIn[0].Witness[1] = []byte{1} )
justiceTxn.TxIn[0].Witness[2] = toLocalScript
// Construct the test's to-remote witness. var outputKeyYIsOdd bool
justiceTxn.TxIn[1].Witness = make([][]byte, 2) if outputKey.SerializeCompressed()[0] ==
justiceTxn.TxIn[1].Witness[0] = append(toRemoteSigRaw.Serialize(), secp.PubKeyFormatCompressedOdd {
byte(txscript.SigHashAll))
justiceTxn.TxIn[1].Witness[1] = toRemoteRedeemScript outputKeyYIsOdd = true
}
delayScriptHash := toLocalCommitTree.SettleLeaf.TapHash()
ctrlBlock := txscript.ControlBlock{
InternalKey: &input.TaprootNUMSKey,
OutputKeyYIsOdd: outputKeyYIsOdd,
LeafVersion: revokeLeaf.LeafVersion,
InclusionProof: delayScriptHash[:],
}
ctrlBytes, err := ctrlBlock.ToBytes()
require.NoError(t, err)
justiceTxn.TxIn[0].Witness = make([][]byte, 3)
justiceTxn.TxIn[0].Witness[0] = toLocalSigRaw.Serialize()
justiceTxn.TxIn[0].Witness[1] = toLocalScript
justiceTxn.TxIn[0].Witness[2] = ctrlBytes
// Construct the test's to-remote witness.
justiceTxn.TxIn[1].Witness = make([][]byte, 3)
justiceTxn.TxIn[1].Witness[0] = toRemoteSigRaw.Serialize()
justiceTxn.TxIn[1].Witness[1] = toRemoteSigningScript
justiceTxn.TxIn[1].Witness[2] = toRemoteCtrlBlock
} else {
// Construct the test's to-local witness.
justiceTxn.TxIn[0].Witness = make([][]byte, 3)
justiceTxn.TxIn[0].Witness[0] = append(
toLocalSigRaw.Serialize(),
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.Serialize(),
byte(txscript.SigHashAll),
)
justiceTxn.TxIn[1].Witness[1] = toRemoteRedeemScript
}
// Assert that the watchtower derives the same justice txn. // Assert that the watchtower derives the same justice txn.
require.Equal(t, justiceTxn, wtJusticeTxn) require.Equal(t, justiceTxn, wtJusticeTxn)

View File

@@ -2,6 +2,7 @@ package wtmock
import ( import (
"crypto/sha256" "crypto/sha256"
"fmt"
"sync" "sync"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
@@ -47,6 +48,69 @@ func (s *MockSigner) SignOutputRaw(tx *wire.MsgTx,
panic("cannot sign w/ unknown key") panic("cannot sign w/ unknown key")
} }
// In case of a taproot output any signature is always a Schnorr
// signature, based on the new tapscript sighash algorithm.
if txscript.IsPayToTaproot(signDesc.Output.PkScript) {
sigHashes := txscript.NewTxSigHashes(
tx, signDesc.PrevOutputFetcher,
)
// Are we spending a script path or the key path? The API is
// slightly different, so we need to account for that to get the
// raw signature.
var (
rawSig []byte
err error
)
switch signDesc.SignMethod {
case input.TaprootKeySpendBIP0086SignMethod,
input.TaprootKeySpendSignMethod:
// This function tweaks the private key using the tap
// root key supplied as the tweak.
rawSig, err = txscript.RawTxInTaprootSignature(
tx, sigHashes, signDesc.InputIndex,
signDesc.Output.Value, signDesc.Output.PkScript,
signDesc.TapTweak, signDesc.HashType,
privKey,
)
if err != nil {
return nil, err
}
case input.TaprootScriptSpendSignMethod:
leaf := txscript.TapLeaf{
LeafVersion: txscript.BaseLeafVersion,
Script: witnessScript,
}
rawSig, err = txscript.RawTxInTapscriptSignature(
tx, sigHashes, signDesc.InputIndex,
signDesc.Output.Value, signDesc.Output.PkScript,
leaf, signDesc.HashType, privKey,
)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown sign method: %v",
signDesc.SignMethod)
}
// The signature returned above might have a sighash flag
// attached if a non-default type was used. We'll slice this
// off if it exists to ensure we can properly parse the raw
// signature.
sig, err := schnorr.ParseSignature(
rawSig[:schnorr.SignatureSize],
)
if err != nil {
return nil, err
}
return sig, nil
}
sig, err := txscript.RawTxInWitnessSignature( sig, err := txscript.RawTxInWitnessSignature(
tx, signDesc.SigHashes, signDesc.InputIndex, amt, tx, signDesc.SigHashes, signDesc.InputIndex, amt,
witnessScript, signDesc.HashType, privKey, witnessScript, signDesc.HashType, privKey,