Merge pull request #690 from hieblmi/static-addr-script

script: static address taproot script
pull/692/head
Slyghtning 3 months ago committed by GitHub
commit 59e5460c73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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
// - <sig>: 64 bytes
TaprootMultiSigWitnessSize = 1 + 1 + 64
// TaprootExpiryScriptSize evaluates to 39 bytes:
// - OP_DATA: 1 byte (client_key length)
// - <client_key>: 32 bytes
// - OP_CHECKSIGVERIFY: 1 byte
// - <address_expiry>: 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)
// - <trader_sig>: 64 bytes
// - witness_script_varint_len: 1 byte (script length)
// - <witness_script>: 39 bytes
// - control_block_varint_len: 1 byte (control block length)
// - <control_block>: 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.
//
// <clientKey> OP_CHECKSIGVERIFY <csvExpiry> 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())
}

@ -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,
)
}

@ -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
}
Loading…
Cancel
Save