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
No known key found for this signature in database
GPG key ID: D7D916376026F177
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 "+
"fetcher: %v", err)
}
hashes := txscript.NewTxSigHashes(justiceTxn, prevOutFetcher)
for _, inp := range inputs {
// Lookup the input's new post-sort position.
i := inputIndex[inp.outPoint]
@ -186,7 +188,7 @@ func (p *JusticeDescriptor) assembleJusticeTxn(txWeight int64,
vm, err := txscript.NewEngine(
inp.txOut.PkScript, justiceTxn, i,
txscript.StandardVerifyFlags,
nil, nil, inp.txOut.Value, prevOutFetcher,
nil, hashes, inp.txOut.Value, prevOutFetcher,
)
if err != nil {
return nil, err

View file

@ -10,6 +10,7 @@ import (
"github.com/btcsuite/btcd/btcutil/txsort"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
@ -53,6 +54,8 @@ var (
altruistCommitType = blob.FlagCommitOutputs.Type()
altruistAnchorCommitType = blob.TypeAltruistAnchorCommit
altruisticTaprootCommitType = blob.TypeAltruistTaprootCommit
)
// TestJusticeDescriptor asserts that a JusticeDescriptor is able to produce the
@ -74,6 +77,10 @@ func TestJusticeDescriptor(t *testing.T) {
name: "altruist anchor commit type",
blobType: altruistAnchorCommitType,
},
{
name: "altruist taproot commit type",
blobType: altruisticTaprootCommitType,
},
}
for _, test := range tests {
@ -85,6 +92,7 @@ func TestJusticeDescriptor(t *testing.T) {
func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
isAnchorChannel := blobType.IsAnchorChannel()
isTaprootChannel := blobType.IsTaprootChannel()
const (
localAmount = btcutil.Amount(100000)
@ -108,15 +116,34 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
toRemoteKeyLoc = signer.AddPrivKey(toRemoteSK)
)
// Construct the to-local witness script.
toLocalScript, err := input.CommitScriptToSelf(
csvDelay, toLocalPK, revPK,
var (
toLocalScript, toLocalScriptHash []byte
toLocalCommitTree *input.CommitScriptTree
)
require.NoError(t, err)
// Compute the to-local witness script hash.
toLocalScriptHash, err := input.WitnessScriptHash(toLocalScript)
require.NoError(t, err)
if isTaprootChannel {
toLocalCommitTree, err = input.NewLocalCommitScriptTree(
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
// sequence numbers.
@ -140,8 +167,37 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
toRemoteRedeemScript []byte
toRemoteScriptHash []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
toRemoteRedeemScript, err = input.CommitScriptToRemoteConfirmed(
toRemotePK,
@ -155,7 +211,8 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
// As it should be.
toRemoteSigningScript = toRemoteRedeemScript
} else {
default:
toRemoteRedeemScript = toRemotePK.SerializeCompressed()
toRemoteScriptHash, err = input.CommitScriptUnencumbered(
toRemotePK,
@ -269,31 +326,82 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
justiceTxn.TxOut = outputs
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.
toLocalSignDesc := &input.SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
KeyLocator: revKeyLoc,
},
WitnessScript: toLocalScript,
Output: breachTxn.TxOut[0],
SigHashes: hashCache,
InputIndex: 0,
HashType: txscript.SigHashAll,
}
if isTaprootChannel {
prevOuts := map[wire.OutPoint]*wire.TxOut{
{
Hash: breachTxID,
Index: 0,
}: breachTxn.TxOut[0],
{
Hash: breachTxID,
Index: 1,
}: breachTxn.TxOut[1],
}
prevOutputFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
hashCache := txscript.NewTxSigHashes(
justiceTxn, prevOutputFetcher,
)
// 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,
toLocalSignDesc = &input.SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
KeyLocator: revKeyLoc,
},
WitnessScript: toLocalScript,
Output: breachTxn.TxOut[0],
SigHashes: hashCache,
PrevOutputFetcher: prevOutputFetcher,
InputIndex: 0,
HashType: txscript.SigHashDefault,
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.
@ -301,21 +409,19 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
err = blockchain.CheckTransactionSanity(btx)
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)
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.
// Compute the witness for the to-remote input.
toRemoteSigRaw, err := signer.SignOutputRaw(justiceTxn, toRemoteSignDesc)
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)
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)
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
// the justice transaction to be published via the channel.
err = punisher.Punish(justiceDesc, nil)
require.Nil(t, err)
require.NoError(t, err)
// Retrieve the published justice transaction.
var wtJusticeTxn *wire.MsgTx
@ -352,18 +458,59 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
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.Serialize(),
byte(txscript.SigHashAll))
justiceTxn.TxIn[0].Witness[1] = []byte{1}
justiceTxn.TxIn[0].Witness[2] = toLocalScript
if isTaprootChannel {
revokeLeaf := txscript.NewBaseTapLeaf(toLocalScript)
outputKey := txscript.ComputeTaprootOutputKey(
&input.TaprootNUMSKey, toLocalCommitTree.TapscriptRoot,
)
// 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
var outputKeyYIsOdd bool
if outputKey.SerializeCompressed()[0] ==
secp.PubKeyFormatCompressedOdd {
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.
require.Equal(t, justiceTxn, wtJusticeTxn)

View file

@ -2,6 +2,7 @@ package wtmock
import (
"crypto/sha256"
"fmt"
"sync"
"github.com/btcsuite/btcd/btcec/v2"
@ -47,6 +48,69 @@ func (s *MockSigner) SignOutputRaw(tx *wire.MsgTx,
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(
tx, signDesc.SigHashes, signDesc.InputIndex, amt,
witnessScript, signDesc.HashType, privKey,