From 7fa10ee7a40c70467de9e068c7a4dc0de3ee24d4 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Thu, 25 Jan 2024 15:31:34 +0100 Subject: [PATCH 1/2] utils: add MuSig2Sign function --- utils/musig.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 utils/musig.go diff --git a/utils/musig.go b/utils/musig.go new file mode 100644 index 0000000..d744233 --- /dev/null +++ b/utils/musig.go @@ -0,0 +1,82 @@ +package utils + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/lightningnetwork/lnd/input" +) + +// MuSig2Sign will create a MuSig2 signature for the passed message using the +// passed private keys. +func MuSig2Sign(version input.MuSig2Version, privKeys []*btcec.PrivateKey, + pubKeys []*btcec.PublicKey, tweaks *input.MuSig2Tweaks, + msg [32]byte) ([]byte, error) { + + // Next we'll create MuSig2 sessions for each individual private + // signing key. + sessions := make([]input.MuSig2Session, len(privKeys)) + for i, signingKey := range privKeys { + _, muSigSession, err := input.MuSig2CreateContext( + version, signingKey, pubKeys, tweaks, nil, + ) + if err != nil { + return nil, fmt.Errorf("error creating "+ + "signing context: %v", err) + } + + sessions[i] = muSigSession + } + + // Next we'll pass around all public nonces to all MuSig2 sessions so + // that they become usable for creating the partial signatures. + for i := 0; i < len(privKeys); i++ { + nonce := sessions[i].PublicNonce() + + for j := 0; j < len(privKeys); j++ { + if i == j { + // Step over if it's the same session. + continue + } + + _, err := sessions[j].RegisterPubNonce(nonce) + if err != nil { + return nil, fmt.Errorf("error sharing "+ + "MuSig2 nonces: %v", err) + } + } + } + + // Now that the sessions are properly set up, we can generate + // each partial signature. + signatures := make([]*musig2.PartialSignature, len(privKeys)) + for i, session := range sessions { + sig, err := input.MuSig2Sign(session, msg, true) + if err != nil { + return nil, err + } + + signatures[i] = sig + } + + // Now that we have all partial sigs we can just combine them to + // get the final signature. + var haveAllSigs bool + for i := 1; i < len(signatures); i++ { + var err error + haveAllSigs, err = input.MuSig2CombineSig( + sessions[0], signatures[i], + ) + if err != nil { + return nil, err + } + } + + if !haveAllSigs { + return nil, fmt.Errorf("combinging MuSig2 signatures " + + "failed") + } + + return sessions[0].FinalSig().Serialize(), nil +} From 7c2640da8138c4a8d411925e967ca1f22ec2941c Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Thu, 9 Nov 2023 19:17:21 +0100 Subject: [PATCH 2/2] script: static address taproot script --- staticaddr/script/script.go | 167 ++++++++++++++++++++ staticaddr/script/script_test.go | 260 +++++++++++++++++++++++++++++++ 2 files changed, 427 insertions(+) create mode 100644 staticaddr/script/script.go create mode 100644 staticaddr/script/script_test.go diff --git a/staticaddr/script/script.go b/staticaddr/script/script.go new file mode 100644 index 0000000..37e78a0 --- /dev/null +++ b/staticaddr/script/script.go @@ -0,0 +1,167 @@ +package script + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/input" +) + +const ( + // TaprootMultiSigWitnessSize evaluates to 66 bytes: + // - num_witness_elements: 1 byte + // - sig_varint_len: 1 byte + // - : 64 bytes + TaprootMultiSigWitnessSize = 1 + 1 + 64 + + // TaprootExpiryScriptSize evaluates to 39 bytes: + // - OP_DATA: 1 byte (client_key length) + // - : 32 bytes + // - OP_CHECKSIGVERIFY: 1 byte + // - : 4 bytes + // - OP_CHECKSEQUENCEVERIFY: 1 byte + TaprootExpiryScriptSize = 1 + 32 + 1 + 4 + 1 + + // TaprootExpiryWitnessSize evaluates to 140 bytes: + // - num_witness_elements: 1 byte + // - client_sig_varint_len: 1 byte (client_sig length) + // - : 64 bytes + // - witness_script_varint_len: 1 byte (script length) + // - : 39 bytes + // - control_block_varint_len: 1 byte (control block length) + // - : 33 bytes + TaprootExpiryWitnessSize = 1 + 1 + 64 + 1 + TaprootExpiryScriptSize + 1 + 33 +) + +// StaticAddress encapsulates the static address script. +type StaticAddress struct { + // TimeoutScript is the final locking script for the timeout path which + // is available to the sender after the set block height. + TimeoutScript []byte + + // TimeoutLeaf is the timeout leaf. + TimeoutLeaf *txscript.TapLeaf + + // ScriptTree is the assembled script tree from our timeout leaf. + ScriptTree *txscript.IndexedTapScriptTree + + // InternalPubKey is the public key for the keyspend path which bypasses + // the timeout script locking. + InternalPubKey *btcec.PublicKey + + // TaprootKey is the taproot public key which is created with the above + // 3 inputs. + TaprootKey *btcec.PublicKey + + // RootHash is the root hash of the taptree. + RootHash chainhash.Hash +} + +// NewStaticAddress constructs a static address script. +func NewStaticAddress(muSig2Version input.MuSig2Version, csvExpiry int64, + clientPubKey, serverPubKey *btcec.PublicKey) (*StaticAddress, error) { + + // Create our timeout path leaf, we'll use this separately to generate + // the timeout path leaf. + timeoutPathScript, err := GenTimeoutPathScript(clientPubKey, csvExpiry) + if err != nil { + return nil, err + } + + // Assemble our taproot script tree from our leaves. + timeoutLeaf := txscript.NewBaseTapLeaf(timeoutPathScript) + tree := txscript.AssembleTaprootScriptTree(timeoutLeaf) + + rootHash := tree.RootNode.TapHash() + + // Calculate the internal aggregate key. + aggregateKey, err := input.MuSig2CombineKeys( + muSig2Version, + []*btcec.PublicKey{clientPubKey, serverPubKey}, + true, + &input.MuSig2Tweaks{ + TaprootTweak: rootHash[:], + }, + ) + if err != nil { + return nil, err + } + + return &StaticAddress{ + TimeoutScript: timeoutPathScript, + TimeoutLeaf: &timeoutLeaf, + ScriptTree: tree, + InternalPubKey: aggregateKey.PreTweakedKey, + TaprootKey: aggregateKey.FinalKey, + RootHash: rootHash, + }, nil +} + +// StaticAddressScript creates a MuSig2 2-of-2 multisig script with CSV timeout +// path for the clientKey. This script represents a static loop-in address. +func (s *StaticAddress) StaticAddressScript() ([]byte, error) { + builder := txscript.NewScriptBuilder() + + builder.AddOp(txscript.OP_1) + builder.AddData(schnorr.SerializePubKey(s.TaprootKey)) + + return builder.Script() +} + +// GenTimeoutPathScript constructs a csv timeout script for the client. +// +// OP_CHECKSIGVERIFY OP_CHECKSEQUENCEVERIFY +func GenTimeoutPathScript(clientKey *btcec.PublicKey, csvExpiry int64) ([]byte, + error) { + + builder := txscript.NewScriptBuilder() + + builder.AddData(schnorr.SerializePubKey(clientKey)) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddInt64(csvExpiry) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + + return builder.Script() +} + +// GenSuccessWitness returns the success witness to spend the static address +// output with a combined signature. +func (s *StaticAddress) GenSuccessWitness(combinedSig []byte) (wire.TxWitness, + error) { + + return wire.TxWitness{ + combinedSig, + }, nil +} + +// GenTimeoutWitness returns the witness to spend the taproot timeout leaf. +func (s *StaticAddress) GenTimeoutWitness(senderSig []byte) (wire.TxWitness, + error) { + + ctrlBlock := s.ScriptTree.LeafMerkleProofs[0].ToControlBlock( + s.InternalPubKey, + ) + + ctrlBlockBytes, err := ctrlBlock.ToBytes() + if err != nil { + return nil, err + } + + return wire.TxWitness{ + senderSig, + s.TimeoutScript, + ctrlBlockBytes, + }, nil +} + +// ExpirySpendWeight returns the weight of the expiry path spend. +func ExpirySpendWeight() int64 { + var weightEstimator input.TxWeightEstimator + weightEstimator.AddWitnessInput(TaprootExpiryWitnessSize) + + weightEstimator.AddP2TROutput() + + return int64(weightEstimator.Weight()) +} diff --git a/staticaddr/script/script_test.go b/staticaddr/script/script_test.go new file mode 100644 index 0000000..c8805e0 --- /dev/null +++ b/staticaddr/script/script_test.go @@ -0,0 +1,260 @@ +package script + +import ( + "bytes" + "crypto/sha256" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/test" + "github.com/lightninglabs/loop/utils" + "github.com/lightningnetwork/lnd/input" + "github.com/stretchr/testify/require" +) + +// TestStaticAddressScript tests the taproot 2:2 multisig script success and CSV +// timeout spend cases. +func TestStaticAddressScript(t *testing.T) { + var ( + value int64 = 800_000 + csvExpiry int64 = 10 + + version = input.MuSig2Version100RC2 + ) + + clientPrivKey, clientPubKey := test.CreateKey(1) + serverPrivKey, serverPubKey := test.CreateKey(2) + + // Keys used for the Musig2 session. + privKeys := []*btcec.PrivateKey{clientPrivKey, serverPrivKey} + pubKeys := []*btcec.PublicKey{clientPubKey, serverPubKey} + + // Create a new static address. + staticAddress, err := NewStaticAddress( + version, csvExpiry, clientPubKey, serverPubKey, + ) + require.NoError(t, err) + + // Retrieve the static address pkScript. + staticAddressScript, err := staticAddress.StaticAddressScript() + require.NoError(t, err) + + // Create a fake transaction. The prevOutFetcher will determine which + // output the signer will fetch, independent of the tx.TxOut. + tx := wire.NewMsgTx(2) + tx.TxIn = []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: sha256.Sum256([]byte{1, 2, 3}), + Index: 50, + }, + }} + tx.TxOut = []*wire.TxOut{{ + PkScript: []byte{ + 0, 20, 2, 141, 221, 230, 144, + 171, 89, 230, 219, 198, 90, 157, + 110, 89, 89, 67, 128, 16, 150, 186, + }, + Value: value, + }} + + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + staticAddressScript, value, + ) + + sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher) + + testCases := []struct { + name string + witness func(*testing.T) wire.TxWitness + valid bool + }{ + { + "success case coop spend with combined internal key", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = 1 + + // This is what gets signed. + taprootSigHash, err := txscript.CalcTaprootSignatureHash( + sigHashes, txscript.SigHashDefault, tx, 0, + prevOutFetcher, + ) + require.NoError(t, err) + + var msg [32]byte + copy(msg[:], taprootSigHash) + + tweak := &input.MuSig2Tweaks{ + TaprootTweak: staticAddress.RootHash[:], + } + + sig, err := utils.MuSig2Sign( + version, privKeys, pubKeys, tweak, msg, + ) + require.NoError(t, err) + + witness, err := staticAddress.GenSuccessWitness( + sig, + ) + require.NoError(t, err) + + return witness + }, true, + }, + { + "success case timeout spend with client key", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = uint32(csvExpiry) + + sig, err := txscript.RawTxInTapscriptSignature( + tx, sigHashes, 0, value, + staticAddressScript, + *staticAddress.TimeoutLeaf, + txscript.SigHashAll, clientPrivKey, + ) + require.NoError(t, err) + + witness, err := staticAddress.GenTimeoutWitness( + sig, + ) + require.NoError(t, err) + + return witness + }, true, + }, + { + "error case timeout spend with client key, wrong " + + "sequence", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = uint32(csvExpiry - 1) + + sig, err := txscript.RawTxInTapscriptSignature( + tx, sigHashes, 0, value, + staticAddressScript, + *staticAddress.TimeoutLeaf, + txscript.SigHashAll, clientPrivKey, + ) + require.NoError(t, err) + + witness, err := staticAddress.GenTimeoutWitness( + sig, + ) + require.NoError(t, err) + + return witness + }, false, + }, + { + "error case timeout spend with server key, server " + + "cannot claim timeout path", + func(t *testing.T) wire.TxWitness { + tx.TxIn[0].Sequence = uint32(csvExpiry) + + sig, err := txscript.RawTxInTapscriptSignature( + tx, sigHashes, 0, value, + staticAddressScript, + *staticAddress.TimeoutLeaf, + txscript.SigHashAll, serverPrivKey, + ) + require.NoError(t, err) + + witness, err := staticAddress.GenTimeoutWitness( + sig, + ) + require.NoError(t, err) + + return witness + }, false, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + tx.TxIn[0].Witness = testCase.witness(t) + + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine( + staticAddressScript, tx, 0, + txscript.StandardVerifyFlags, nil, + sigHashes, value, prevOutFetcher, + ) + } + + assertEngineExecution(t, testCase.valid, newEngine) + }) + } +} + +// assertEngineExecution executes the VM returned by the newEngine closure, +// asserting the result matches the validity expectation. In the case where it +// doesn't match the expectation, it executes the script step-by-step and +// prints debug information to stdout. +// This code is adopted from: lnd/input/script_utils_test.go . +func assertEngineExecution(t *testing.T, valid bool, + newEngine func() (*txscript.Engine, error)) { + + t.Helper() + + // Get a new VM to execute. + vm, err := newEngine() + require.NoError(t, err, "unable to create engine") + + // Execute the VM, only go on to the step-by-step execution if it + // doesn't validate as expected. + vmErr := vm.Execute() + executionValid := vmErr == nil + if valid == executionValid { + return + } + + // Now that the execution didn't match what we expected, fetch a new VM + // to step through. + vm, err = newEngine() + require.NoError(t, err, "unable to create engine") + + // This buffer will trace execution of the Script, dumping out to + // stdout. + var debugBuf bytes.Buffer + + done := false + for !done { + dis, err := vm.DisasmPC() + require.NoError(t, err, "stepping") + debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis)) + + done, err = vm.Step() + if err != nil && valid { + fmt.Println(debugBuf.String()) + t.Fatalf("spend test case failed, spend "+ + "should be valid: %v", err) + } else if err == nil && !valid && done { + fmt.Println(debugBuf.String()) + t.Fatalf("spend test case succeed, spend "+ + "should be invalid: %v", err) + } + + debugBuf.WriteString( + fmt.Sprintf("Stack: %v", vm.GetStack()), + ) + debugBuf.WriteString( + fmt.Sprintf("AltStack: %v", vm.GetAltStack()), + ) + } + + // If we get to this point the unexpected case was not reached + // during step execution, which happens for some checks, like + // the clean-stack rule. + validity := "invalid" + if valid { + validity = "valid" + } + + fmt.Println(debugBuf.String()) + t.Fatalf( + "%v spend test case execution ended with: %v", validity, vmErr, + ) +}