mirror of
https://github.com/lightninglabs/pool.git
synced 2026-08-13 12:33:04 +02:00
poolscript+account+log: add new Taproot scripts
This commit is contained in:
parent
7bc48a7a7d
commit
69d3ad6d44
6 changed files with 789 additions and 12 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
4
log.go
|
|
@ -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
25
poolscript/log.go
Normal 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
|
||||
}
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue