poolscript+account+log: add new Taproot scripts

This commit is contained in:
Oliver Gugger 2022-06-30 22:17:23 +02:00
parent 7bc48a7a7d
commit 69d3ad6d44
No known key found for this signature in database
GPG key ID: 8E4256593F177720
6 changed files with 789 additions and 12 deletions

View file

@ -222,6 +222,7 @@ const (
// Output returns the current on-chain output associated with the account.
func (a *Account) Output() (*wire.TxOut, error) {
script, err := poolscript.AccountScript(
poolscript.VersionWitnessScript,
a.Expiry, a.TraderKey.PubKey, a.AuctioneerKey, a.BatchKey,
a.Secret,
)
@ -241,6 +242,7 @@ func (a *Account) Output() (*wire.TxOut, error) {
func (a *Account) NextOutputScript() ([]byte, error) {
nextBatchKey := poolscript.IncrementKey(a.BatchKey)
return poolscript.AccountScript(
poolscript.VersionWitnessScript,
a.Expiry, a.TraderKey.PubKey, a.AuctioneerKey, nextBatchKey,
a.Secret,
)

View file

@ -113,6 +113,7 @@ func TestFindInitialAccountState(t *testing.T) {
tc.config.InitialBatchKey,
)
script, _ := poolscript.AccountScript(
poolscript.VersionWitnessScript,
177, acc.TraderKey.PubKey,
tc.config.AuctioneerPubKey,
batchKey, acc.Secret,
@ -218,6 +219,7 @@ func TestFindAccountUpdate(t *testing.T) {
batchKey := poolscript.IncrementKey(acc.BatchKey)
script, _ := poolscript.AccountScript(
poolscript.VersionWitnessScript,
tc.expectedExpiry, acc.TraderKey.PubKey,
acc.AuctioneerKey, batchKey, acc.Secret,
)

4
log.go
View file

@ -13,6 +13,7 @@ import (
"github.com/lightninglabs/pool/clientdb"
"github.com/lightninglabs/pool/funding"
"github.com/lightninglabs/pool/order"
"github.com/lightninglabs/pool/poolscript"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/signal"
@ -56,6 +57,9 @@ func SetupLoggers(root *build.RotatingLogWriter, intercept signal.Interceptor) {
lnd.AddSubLogger(
root, clientdb.Subsystem, intercept, clientdb.UseLogger,
)
lnd.AddSubLogger(
root, poolscript.Subsystem, intercept, poolscript.UseLogger,
)
}
// genSubLogger creates a logger for a subsystem. We provide an instance of

25
poolscript/log.go Normal file
View file

@ -0,0 +1,25 @@
package poolscript
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
const Subsystem = "SCRP"
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log btclog.Logger
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger(Subsystem, nil))
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

View file

@ -2,21 +2,38 @@ package poolscript
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
)
// Version represents the type of Pool account script that is used for either
// the trader or auctioneer accounts.
type Version uint8
const (
// VersionWitnessScript is the legacy script version that used a single
// p2wsh script for both spend paths.
VersionWitnessScript Version = 0
// VersionTaprootMuSig2 is the script version that uses a MuSig2
// combined key of the auctioneer's and trader's public keys as the
// internal key and a single script leaf of the expiry path as the
// taproot script tree merkle root.
VersionTaprootMuSig2 Version = 1
// AccountKeyFamily is the key family used to derive keys which will be
// used in the 2 of 2 multi-sig construction of a CLM account.
//
// TODO(wilmer): decide on actual value.
AccountKeyFamily keychain.KeyFamily = 220
// MaxWitnessSigLen is the maximum length of a DER encoded signature and
@ -28,7 +45,7 @@ const (
// 0x30 + <1-byte> + 0x02 + 0x21 + <33 bytes> + 0x2 + 0x21 + <33 bytes>.
MaxWitnessSigLen = 72 + 1
// AccountWitnessScriptSize: 79 bytes
// AccountWitnessScriptSize evaluates to 79 bytes:
// - OP_DATA: 1 byte (trader_key length)
// - <trader_key>: 33 bytes
// - OP_CHECKSIGVERIFY: 1 byte
@ -43,8 +60,8 @@ const (
// - OP_ENDIF: 1 byte
AccountWitnessScriptSize = 1 + 33 + 1 + 1 + 33 + 1 + 1 + 1 + 1 + 4 + 1 + 1
// MultiSigWitnessSize: 227 bytes
// - num_witness_elements: 1 byte
// MultiSigWitnessSize evaluates to 227 bytes:
// - num_witness_elements: 1 byte
// - trader_sig_varint_len: 1 byte
// - <trader_sig>: 73 bytes
// - auctioneer_sig_varint_len: 1 byte
@ -54,16 +71,43 @@ const (
MultiSigWitnessSize = 1 + 1 + MaxWitnessSigLen + 1 + MaxWitnessSigLen +
1 + AccountWitnessScriptSize
// ExpiryWitnessSize: 154 bytes
// - num_witness_elements: 1 byte
// ExpiryWitnessSize evaluates to 154 bytes:
// - num_witness_elements: 1 byte
// - trader_sig_varint_len: 1 byte (trader_sig length)
// - <trader_sig>: 73 bytes
// - witness_script_varint_len: 1 byte (nil length)
// - <witness_script>: 79 bytes
ExpiryWitnessSize = 1 + 1 + MaxWitnessSigLen +
1 + AccountWitnessScriptSize
// 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 (trader_key length)
// - <trader_key>: 32 bytes
// - OP_CHECKSIGVERIFY: 1 byte
// - <account_expiry>: 4 bytes
// - OP_CHECKLOCKTIMEVERIFY: 1 byte
TaprootExpiryScriptSize = 1 + 32 + 1 + 4 + 1
// TaprootExpiryWitnessSize evaluates to 140 bytes:
// - num_witness_elements: 1 byte
// - trader_sig_varint_len: 1 byte (trader_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
)
// MuSig2Nonces is a type for a MuSig2 nonce pair (2 times 33-byte).
type MuSig2Nonces [musig2.PubNonceSize]byte
// TraderKeyTweak computes the tweak based on the current per-batch key and
// shared secret that should be applied to an account's base trader key. The
// tweak is computed as the following:
@ -230,20 +274,148 @@ func (r *RecoveryHelper) LocateAnyOutput(expiry uint32,
// AccountScript returns the output script of an account on-chain.
//
// For version 0 (p2wsh) this returns the hash of the following script:
// <trader_key> OP_CHECKSIGVERIFY
// <auctioneer_key> OP_CHECKSIG OP_IFDUP OP_NOTIF
// <account_expiry> OP_CHECKLOCKTIMEVERIFY
// OP_ENDIF.
func AccountScript(expiry uint32, traderKey, auctioneerKey,
// OP_ENDIF
//
// For version 1 (p2tr) this returns the taproot key of a MuSig2 combined key
// of the auctioneer's and trader's public keys as the internal key, tweaked
// with the hash of a single script leaf that has the following script:
// <trader_key> OP_CHECKSIGVERIFY <account_expiry> OP_CHECKLOCKTIMEVERIFY.
func AccountScript(version Version, expiry uint32, traderKey, auctioneerKey,
batchKey *btcec.PublicKey, secret [32]byte) ([]byte, error) {
witnessScript, err := AccountWitnessScript(
expiry, traderKey, auctioneerKey, batchKey, secret,
switch version {
case VersionWitnessScript:
witnessScript, err := AccountWitnessScript(
expiry, traderKey, auctioneerKey, batchKey, secret,
)
if err != nil {
return nil, err
}
return input.WitnessScriptHash(witnessScript)
case VersionTaprootMuSig2:
aggregateKey, _, err := TaprootKey(
expiry, traderKey, auctioneerKey, batchKey, secret,
)
if err != nil {
return nil, err
}
return payToWitnessTaprootScript(aggregateKey.FinalKey)
default:
return nil, fmt.Errorf("invalid script version <%d>", version)
}
}
// TaprootKey returns the aggregated MuSig2 combined internal key and the
// tweaked Taproot key of an account output, as well as the expiry script tap
// leaf.
func TaprootKey(expiry uint32, traderKey, auctioneerKey,
batchKey *btcec.PublicKey, secret [32]byte) (*musig2.AggregateKey,
*txscript.TapLeaf, error) {
expiryLeaf, err := TaprootExpiryScript(
expiry, traderKey, batchKey, secret,
)
if err != nil {
return nil, nil, err
}
rootHash := expiryLeaf.TapHash()
auctioneerKeySchnorr, err := schnorr.ParsePubKey(
schnorr.SerializePubKey(auctioneerKey),
)
if err != nil {
return nil, nil, fmt.Errorf("error parsing auctioneer key: %v",
err)
}
traderKeySchnorr, err := schnorr.ParsePubKey(
schnorr.SerializePubKey(traderKey),
)
if err != nil {
return nil, nil, fmt.Errorf("error parsing trader key: %v", err)
}
aggregateKey, err := input.MuSig2CombineKeys(
[]*btcec.PublicKey{
auctioneerKeySchnorr, traderKeySchnorr,
},
&input.MuSig2Tweaks{
TaprootTweak: rootHash[:],
},
)
if err != nil {
return nil, nil, fmt.Errorf("error combining keys: %v", err)
}
return aggregateKey, expiryLeaf, nil
}
// payToWitnessTaprootScript creates a new script to pay to a version 1
// (taproot) witness program. The passed hash is expected to be valid.
func payToWitnessTaprootScript(taprootKey *btcec.PublicKey) ([]byte, error) {
builder := txscript.NewScriptBuilder()
builder.AddOp(txscript.OP_1)
builder.AddData(schnorr.SerializePubKey(taprootKey))
return builder.Script()
}
// TaprootExpiryScript returns the leaf script of the expiry script path.
//
// <trader_key> OP_CHECKSIGVERIFY <account_expiry> OP_CHECKLOCKTIMEVERIFY.
func TaprootExpiryScript(expiry uint32, traderKey, batchKey *btcec.PublicKey,
secret [32]byte) (*txscript.TapLeaf, error) {
traderKeyTweak := TraderKeyTweak(batchKey, secret, traderKey)
tweakedTraderKey := input.TweakPubKeyWithTweak(
traderKey, traderKeyTweak,
)
builder := txscript.NewScriptBuilder()
builder.AddData(schnorr.SerializePubKey(tweakedTraderKey))
builder.AddOp(txscript.OP_CHECKSIGVERIFY)
builder.AddInt64(int64(expiry))
builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY)
script, err := builder.Script()
if err != nil {
return nil, err
}
return input.WitnessScriptHash(witnessScript)
leaf := txscript.NewBaseTapLeaf(script)
return &leaf, nil
}
// SpendExpiryTaproot returns the witness required to spend an account through
// the expiration script path of a tapscript spend.
func SpendExpiryTaproot(witnessScript, traderSig,
serializedControlBlock []byte) wire.TxWitness {
witness := make(wire.TxWitness, 3)
witness[0] = traderSig
witness[1] = witnessScript
witness[2] = serializedControlBlock
return witness
}
// SpendMuSig2Taproot returns the witness required to spend an account through
// the internal key which is a MuSig2 combined key that requires a single
// Schnorr signature.
func SpendMuSig2Taproot(combinedSig []byte) wire.TxWitness {
witness := make(wire.TxWitness, 1)
witness[0] = combinedSig
return witness
}
// SpendExpiry returns the witness required to spend an account through the
@ -278,6 +450,54 @@ func IsExpirySpend(witness wire.TxWitness) bool {
return true
}
// IsTaprootExpirySpend determines whether the provided witness corresponds to
// the expiration script path of a Taproot enabled (version 1) account.
func IsTaprootExpirySpend(witness wire.TxWitness) bool {
// We still have 3 elements, the trader's signature, the script and the
// control block.
if len(witness) != 3 {
return false
}
// An expiry spend is a script spend and therefore always has to have a
// control block. And a valid control block is at _least_ 33 bytes long
// (leaf version of 1 byte and the 32-byte x-only internal key).
ctrlBlock := witness[2]
if len(ctrlBlock) < 33 {
return false
}
// The expiry is the only variably-sized part in the script, it can be
// between 1 and 4 bytes. So the min script length is 3 bytes smaller
// than the maximum possible length.
const minScriptLen = TaprootExpiryScriptSize - 3
script := witness[1]
if len(script) < minScriptLen || len(script) > TaprootExpiryScriptSize {
return false
}
// The control block must start with the leaf version, which will always
// be 0xc0 or 0xc1 in our case (depending on the parity of the key).
const (
leafVersionEven = byte(txscript.BaseLeafVersion)
leafVersionOdd = byte(txscript.BaseLeafVersion | 1)
)
if ctrlBlock[0] != leafVersionEven && ctrlBlock[0] != leafVersionOdd {
return false
}
// The witness script should start with a 32-byte data push opcode and
// end with CLTV. There's not much else we can assert about the script
// since in this context we don't know the trader key or the actual
// expiration.
if script[0] != txscript.OP_DATA_32 ||
script[len(script)-1] != txscript.OP_CHECKLOCKTIMEVERIFY {
return false
}
return true
}
// IsMultiSigSpend determines whether the provided witness corresponds to the
// multi-sig script path of an account.
func IsMultiSigSpend(witness wire.TxWitness) bool {
@ -290,6 +510,176 @@ func IsMultiSigSpend(witness wire.TxWitness) bool {
return true
}
// IsTaprootMultiSigSpend determines whether the provided witness corresponds to
// the MuSig2 multi-sig key spend path of a Taproot enabled (version 1) account.
func IsTaprootMultiSigSpend(witness wire.TxWitness) bool {
if len(witness) != 1 {
return false
}
// We'll always use SigHashDefault, so our signature will always be 64
// bytes long exactly.
if len(witness[0]) != schnorr.SignatureSize {
return false
}
return true
}
// TaprootMuSig2SigningSession creates a MuSig2 signing session for a Taproot
// account spend.
func TaprootMuSig2SigningSession(ctx context.Context, expiry uint32, traderKey,
batchKey *btcec.PublicKey, sharedSecret [32]byte,
auctioneerKey *btcec.PublicKey, signer lndclient.SignerClient,
localKeyLocator *keychain.KeyLocator,
remoteNonces *MuSig2Nonces) (*input.MuSig2SessionInfo, func(), error) {
expiryLeaf, err := TaprootExpiryScript(
expiry, traderKey, batchKey, sharedSecret,
)
if err != nil {
return nil, nil, fmt.Errorf("error creating expiry leaf "+
"script: %v", err)
}
rootHash := expiryLeaf.TapHash()
sessionOpts := []lndclient.MuSig2SessionOpts{
lndclient.MuSig2TaprootTweakOpt(rootHash[:], false),
}
var remoteNonceBytes []byte
if remoteNonces != nil {
remoteNonceBytes = remoteNonces[:]
sessionOpts = append(sessionOpts, lndclient.MuSig2NonceOpt(
[][musig2.PubNonceSize]byte{*remoteNonces},
))
}
signers := make([][32]byte, 2)
copy(signers[0][:], schnorr.SerializePubKey(traderKey))
copy(signers[1][:], schnorr.SerializePubKey(auctioneerKey))
sessionInfo, err := signer.MuSig2CreateSession(
ctx, localKeyLocator, signers, sessionOpts...,
)
if err != nil {
return nil, nil, fmt.Errorf("error creating MuSig2 session: %v",
err)
}
log.Tracef("Created MuSig2 signing session for expiry=%d, "+
"traderKey=%x, batchKey=%x, sharedSecret=%x, "+
"auctioneerKey=%x, rootHash=%x, combinedKey=%x, "+
"localNonces=%x, remoteNonces=%x",
expiry, traderKey.SerializeCompressed(),
batchKey.SerializeCompressed(), sharedSecret[:],
auctioneerKey.SerializeCompressed(), rootHash[:],
sessionInfo.CombinedKey.SerializeCompressed(),
sessionInfo.PublicNonce[:], remoteNonceBytes)
return sessionInfo, func() {
err = signer.MuSig2Cleanup(ctx, sessionInfo.SessionID)
if err != nil {
log.Errorf("Error cleaning up MuSig2 session %x: %v",
sessionInfo.SessionID[:], err)
}
}, nil
}
// TaprootMuSig2Sign creates a partial MuSig2 signature for a Taproot account
// spend. If remoteSigs is not empty, we expect to be the second (and last)
// signer and will also attempt to combine the signatures. The return value in
// that case is the full, final signature instead of the partial signature.
func TaprootMuSig2Sign(ctx context.Context, inputIdx int,
sessionInfo *input.MuSig2SessionInfo, signer lndclient.SignerClient,
spendTx *wire.MsgTx, previousOutputs []*wire.TxOut,
remoteNonces *MuSig2Nonces,
remotePartialSig *[input.MuSig2PartialSigSize]byte) ([]byte, error) {
// In some cases we can already register all nonces during session
// creation.
var remoteNonceBytes []byte
if remoteNonces != nil {
remoteNonceBytes = remoteNonces[:]
allNonces, err := signer.MuSig2RegisterNonces(
ctx, sessionInfo.SessionID,
[][musig2.PubNonceSize]byte{*remoteNonces},
)
if err != nil {
return nil, fmt.Errorf("error registering remote "+
"nonces: %v", err)
}
if !allNonces {
return nil, fmt.Errorf("don't have all nonces after " +
"registering remote nonces")
}
}
// We now need to create the raw sighash of the transaction, as that
// will be the message we're signing collaboratively.
prevOutputFetcher := txscript.NewMultiPrevOutFetcher(nil)
for idx := range spendTx.TxIn {
prevOutputFetcher.AddPrevOut(
spendTx.TxIn[idx].PreviousOutPoint,
previousOutputs[idx],
)
}
sighashes := txscript.NewTxSigHashes(spendTx, prevOutputFetcher)
sigHash, err := txscript.CalcTaprootSignatureHash(
sighashes, txscript.SigHashDefault, spendTx, inputIdx,
prevOutputFetcher,
)
if err != nil {
return nil, fmt.Errorf("error creating sighash: %v", err)
}
// We'll attempt to combine our signature with the remote one if there
// is one. In that case we won't clean up after signing, since we still
// need the session for the combine step.
shouldCombine := remotePartialSig != nil
var digest [32]byte
copy(digest[:], sigHash)
partialSig, err := signer.MuSig2Sign(
ctx, sessionInfo.SessionID, digest, !shouldCombine,
)
if err != nil {
return nil, fmt.Errorf("error creating partial signature: %v",
err)
}
log.Tracef("Signed MuSig2 sighash=%x for taprootKey=%x, "+
"partialSig=%x, remoteNonce=%x", sigHash,
schnorr.SerializePubKey(sessionInfo.CombinedKey),
partialSig, remoteNonceBytes)
// If we're not the last signer, we're done now and the session should
// have been cleaned up. We return our partial signature, the remote
// party will do the combine step.
if !shouldCombine {
return partialSig, nil
}
// We have the remote signature, so we're the last signer.
log.Tracef("Combining ourPartialSig=%x with remotePartialSig=%x",
partialSig, remotePartialSig[:])
haveFinalSig, finalSig, err := signer.MuSig2CombineSig(
ctx, sessionInfo.SessionID, [][]byte{remotePartialSig[:]},
)
if err != nil {
return nil, fmt.Errorf("error combining sigs: %v", err)
}
// We should now have a full, complete signature. If this is true then
// the signer also automatically cleaned up the signing session, so we
// don't need to do anything.
if !haveFinalSig {
return nil, fmt.Errorf("don't have final sig after combining " +
"remote partial signature with ours")
}
return finalSig, nil
}
// IncrementKey increments the given key by the backing curve's base point.
func IncrementKey(pubKey *btcec.PublicKey) *btcec.PublicKey {
var (

View file

@ -1,19 +1,31 @@
package poolscript
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"math/rand"
"reflect"
"testing"
"testing/quick"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/input"
"github.com/stretchr/testify/require"
)
const (
numOperations = 10000
numOperationsQuickTest = 1000
oddByte = input.PubKeyFormatCompressedOdd
)
var (
@ -31,6 +43,12 @@ var (
"03d9dfc4971c9cbabb1b9a4c991914211aa21286e007c15d7e9d828da0b8" +
"f07763",
)
sharedSecret = [32]byte{11, 22, 33, 44, 55}
expiry = uint32(144 * 365)
batchPubKey, _ = btcec.ParsePubKey(
initialBatchKeyBytes,
)
)
// TestIncrementDecrementKey makes sure that incrementing and decrementing an EC
@ -109,5 +127,341 @@ func FuzzWitnessSpendDetection(f *testing.F) {
}
_ = IsMultiSigSpend(witness)
_ = IsExpirySpend(witness)
_ = IsTaprootMultiSigSpend(witness)
_ = IsTaprootExpirySpend(witness)
})
}
// TestWitnessCorrectness tests that our witness sizes are correct and that they
// can actually spend an output of the given type.
func TestWitnessCorrectness(t *testing.T) {
t.Parallel()
dummyHash := sha256.Sum256([]byte("imagine this was a transaction"))
testCases := []struct {
name string
version Version
timeout bool
expectedSize int
witness func(t *testing.T, trader,
auctioneer *btcec.PrivateKey) wire.TxWitness
check func(witness wire.TxWitness) bool
}{{
name: "v0 multisig",
version: VersionWitnessScript,
expectedSize: MultiSigWitnessSize,
witness: func(t *testing.T, trader,
auctioneer *btcec.PrivateKey) wire.TxWitness {
script, err := AccountWitnessScript(
expiry, trader.PubKey(), auctioneer.PubKey(),
batchPubKey, sharedSecret,
)
require.NoError(t, err)
traderSig := ecdsa.Sign(trader, dummyHash[:])
auctioneerSig := ecdsa.Sign(auctioneer, dummyHash[:])
return SpendMultiSig(
script, serializeSigHashAll(traderSig),
serializeSigHashAll(auctioneerSig),
)
},
check: IsMultiSigSpend,
}, {
name: "v0 timeout",
version: VersionWitnessScript,
timeout: true,
expectedSize: ExpiryWitnessSize,
witness: func(t *testing.T, trader,
auctioneer *btcec.PrivateKey) wire.TxWitness {
script, err := AccountWitnessScript(
expiry, trader.PubKey(), auctioneer.PubKey(),
batchPubKey, sharedSecret,
)
require.NoError(t, err)
traderSig := ecdsa.Sign(trader, dummyHash[:])
return SpendExpiry(
script, serializeSigHashAll(traderSig),
)
},
check: IsExpirySpend,
}, {
name: "v1 multisig",
version: VersionTaprootMuSig2,
expectedSize: TaprootMultiSigWitnessSize,
witness: func(t *testing.T, trader,
auctioneer *btcec.PrivateKey) wire.TxWitness {
traderSig, err := schnorr.Sign(trader, dummyHash[:])
require.NoError(t, err)
return SpendMuSig2Taproot(traderSig.Serialize())
},
check: IsTaprootMultiSigSpend,
}, {
name: "v1 timeout",
version: VersionTaprootMuSig2,
timeout: true,
expectedSize: TaprootExpiryWitnessSize,
witness: func(t *testing.T, trader,
auctioneer *btcec.PrivateKey) wire.TxWitness {
auctioneerPub := auctioneer.PubKey()
_, tapLeaf, err := TaprootKey(
expiry, trader.PubKey(), auctioneerPub,
batchPubKey, sharedSecret,
)
require.NoError(t, err)
traderSig, err := schnorr.Sign(trader, dummyHash[:])
require.NoError(t, err)
odd := auctioneerPub.SerializeCompressed()[0] == oddByte
controlBlock := txscript.ControlBlock{
InternalKey: auctioneerPub,
LeafVersion: txscript.BaseLeafVersion,
OutputKeyYIsOdd: odd,
}
blockBytes, err := controlBlock.ToBytes()
require.NoError(t, err)
return SpendExpiryTaproot(
tapLeaf.Script, traderSig.Serialize(),
blockBytes,
)
},
check: IsTaprootExpirySpend,
}}
scenario := func(trader, auctioneer *btcec.PrivateKey) bool {
for _, tc := range testCases {
txWitness := tc.witness(t, trader, auctioneer)
witness, err := serializeTxWitness(txWitness)
if err != nil {
t.Logf("Unexpected error: %v", err)
return false
}
// For Taproot scripts we can actually enforce exact
// witness size estimations! The only variable size item
// is the expiry because that's encoded as a VarInt. But
// we chose an expiry >32k for this test to enforce the
// 4-byte serialization.
if tc.version == VersionTaprootMuSig2 {
if len(witness) != tc.expectedSize {
t.Logf("Unexpected witness size %d: %x",
len(witness), witness)
return false
}
} else {
if len(witness) > tc.expectedSize {
t.Logf("Unexpected witness size %d: %x",
len(witness), witness)
return false
}
}
passesCheck := tc.check(txWitness)
if !passesCheck {
t.Logf("Did not pass check, trader key %x, "+
"auctioneer key %x", trader.Serialize(),
auctioneer.Serialize())
return false
}
}
return true
}
quickCfg := &quick.Config{
MaxCount: 1000,
Values: func(values []reflect.Value, r *rand.Rand) {
pkBytes := make([]byte, 32)
_, _ = r.Read(pkBytes)
_, _ = r.Read(pkBytes)
_, _ = r.Read(pkBytes)
trader, _ := btcec.PrivKeyFromBytes(pkBytes)
_, _ = r.Read(pkBytes)
auctioneer, _ := btcec.PrivKeyFromBytes(pkBytes)
values[1] = reflect.ValueOf(trader)
values[0] = reflect.ValueOf(auctioneer)
},
}
require.NoError(t, quick.Check(scenario, quickCfg))
}
// TestTaprootSpend tests that the taproot key and script spends can be executed
// correctly.
func TestTaprootSpend(t *testing.T) {
t.Parallel()
t.Run("MuSig2", func(tt *testing.T) {
testTaprootSpend(tt, false)
})
t.Run("Expiry", func(tt *testing.T) {
testTaprootSpend(tt, true)
})
}
// testTaprootSpend executes a Taproot spend, either using the MuSig2 key spend
// path or the expiry script path.
func testTaprootSpend(t *testing.T, expiryPath bool) {
trader, err := btcec.NewPrivateKey()
require.NoError(t, err)
traderPub := trader.PubKey()
auctioneer, err := btcec.NewPrivateKey()
require.NoError(t, err)
auctioneerPub := auctioneer.PubKey()
const outputSize = 2000000
tx := wire.NewMsgTx(2)
tx.LockTime = expiry
tx.TxIn = []*wire.TxIn{{
PreviousOutPoint: wire.OutPoint{
Hash: [32]byte{1, 2, 3},
Index: 2,
},
}}
taprootKey, tapLeaf, err := TaprootKey(
expiry, traderPub, auctioneerPub, batchPubKey, sharedSecret,
)
require.NoError(t, err)
pkScript, err := AccountScript(
VersionTaprootMuSig2, expiry, traderPub, auctioneerPub,
batchPubKey, sharedSecret,
)
require.NoError(t, err)
tx.TxOut = []*wire.TxOut{{
Value: outputSize - 800,
PkScript: pkScript,
}}
prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
pkScript, outputSize,
)
sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
if expiryPath {
// For the expiry path we sign the tap script sighash with the
// tweaked trader key.
sigHash, err := txscript.CalcTapscriptSignaturehash(
sigHashes, txscript.SigHashDefault, tx, 0,
prevOutputFetcher, *tapLeaf,
)
require.NoError(t, err)
traderKeyTweak := TraderKeyTweak(
batchPubKey, sharedSecret, traderPub,
)
traderTweaked := input.TweakPrivKey(trader, traderKeyTweak)
traderSig, err := schnorr.Sign(traderTweaked, sigHash)
require.NoError(t, err)
odd := taprootKey.FinalKey.SerializeCompressed()[0] == oddByte
controlBlock := txscript.ControlBlock{
InternalKey: taprootKey.PreTweakedKey,
LeafVersion: txscript.BaseLeafVersion,
OutputKeyYIsOdd: odd,
}
blockBytes, err := controlBlock.ToBytes()
require.NoError(t, err)
tx.TxIn[0].Witness = SpendExpiryTaproot(
tapLeaf.Script, traderSig.Serialize(), blockBytes,
)
} else {
// For the MuSig2 key spend path we sign the normal Taproot
// sighash with the combined MuSig2 key.
sigHash, err := txscript.CalcTaprootSignatureHash(
sigHashes, txscript.SigHashDefault, tx, 0,
prevOutputFetcher,
)
require.NoError(t, err)
traderPubSchnorr, _ := schnorr.ParsePubKey(
schnorr.SerializePubKey(traderPub),
)
auctioneerPubSchnorr, _ := schnorr.ParsePubKey(
schnorr.SerializePubKey(auctioneerPub),
)
signerKeys := []*btcec.PublicKey{
traderPubSchnorr, auctioneerPubSchnorr,
}
rootHash := tapLeaf.TapHash()
rootHashOpt := musig2.WithTaprootTweakCtx(rootHash[:])
signerOpts := musig2.WithKnownSigners(signerKeys)
traderCtx, err := musig2.NewContext(
trader, true, rootHashOpt, signerOpts,
)
require.NoError(t, err)
auctioneerCtx, err := musig2.NewContext(
auctioneer, true, rootHashOpt, signerOpts,
)
require.NoError(t, err)
traderSession, err := traderCtx.NewSession()
require.NoError(t, err)
auctioneerSession, err := auctioneerCtx.NewSession()
require.NoError(t, err)
allNonces, err := traderSession.RegisterPubNonce(
auctioneerSession.PublicNonce(),
)
require.NoError(t, err)
require.True(t, allNonces)
allNonces, err = auctioneerSession.RegisterPubNonce(
traderSession.PublicNonce(),
)
require.NoError(t, err)
require.True(t, allNonces)
var msg [32]byte
copy(msg[:], sigHash)
traderSig, err := traderSession.Sign(msg)
require.NoError(t, err)
_, err = auctioneerSession.Sign(msg)
require.NoError(t, err)
fullSigOk, err := auctioneerSession.CombineSig(traderSig)
require.NoError(t, err)
require.True(t, fullSigOk)
fullSig := auctioneerSession.FinalSig()
tx.TxIn[0].Witness = SpendMuSig2Taproot(fullSig.Serialize())
}
vm, err := txscript.NewEngine(
pkScript, tx, 0, txscript.StandardVerifyFlags, nil, sigHashes,
outputSize, txscript.NewCannedPrevOutputFetcher(
pkScript, outputSize,
),
)
require.NoError(t, err)
err = vm.Execute()
require.NoError(t, err, "invalid witness")
}
// serializeSigHash serializes the given signature to its raw byte form and also
// appends the txscript.SigHashAll flag.
func serializeSigHashAll(s input.Signature) []byte {
return append(s.Serialize(), byte(txscript.SigHashAll))
}
// serializeTxWitness return the wire witness stack into raw bytes.
func serializeTxWitness(txWitness wire.TxWitness) ([]byte, error) {
var witnessBytes bytes.Buffer
err := psbt.WriteTxWitness(&witnessBytes, txWitness)
if err != nil {
return nil, fmt.Errorf("error serializing witness: %v", err)
}
return witnessBytes.Bytes(), nil
}