mirror of https://github.com/lightninglabs/loop
script: static address taproot script
parent
7fa10ee7a4
commit
7c2640da81
@ -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,
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue