Skip to content

Commit 210b4a1

Browse files
authored
Merge pull request #8 from BoostyLabs/vs/signer_append
bitcoin/signer: SignTaprootMultiAppend implemented
2 parents 2d24bfb + 08b21ee commit 210b4a1

6 files changed

Lines changed: 169 additions & 24 deletions

File tree

bitcoin/signer/signer.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ type SignTaprootMultiParams struct {
3535
TapScriptPrivateKeys []*btcec.PrivateKey // holds private keys needed to unlock MultiSig tapScript. Key-spend path will be used in case of empty array.
3636
}
3737

38+
// SignTaprootMultiAppendParams defines parameters for SignTaprootMultiAppend method.
39+
//
40+
// NOTE: TapScriptPrivateKey must be used in reverse order relatively to public keys in locking script!!!
41+
// See [SignTaprootMultiParams].
42+
type SignTaprootMultiAppendParams struct {
43+
SerializedPSBT []byte
44+
Inputs []int // inputs indexes.
45+
TapScriptPrivateKey *btcec.PrivateKey // holds the next private key needed to unlock MultiSig tapScript
46+
}
47+
3848
// signTaprootInputParams defines parameters for signTaprootInput method.
3949
type signTaprootInputParams struct {
4050
packet *psbt.Packet
@@ -140,6 +150,16 @@ func (signer *Signer) SignTaprootMulti(params SignTaprootMultiParams) ([]byte, e
140150
return w.Bytes(), nil
141151
}
142152

153+
// SignTaprootMultiAppend appends provided psbt with signature taproot inputs by provided indexes using one (the next) private keys, returns updated serialized PSBT.
154+
// NOTE: See [SignTaprootMultiParams] comments for valid signature processing (the order must be the same).
155+
func (signer *Signer) SignTaprootMultiAppend(params SignTaprootMultiAppendParams) ([]byte, error) {
156+
return signer.SignTaprootMulti(SignTaprootMultiParams{
157+
SerializedPSBT: params.SerializedPSBT,
158+
Inputs: params.Inputs,
159+
TapScriptPrivateKeys: []*btcec.PrivateKey{params.TapScriptPrivateKey},
160+
})
161+
}
162+
143163
// signTaprootInput signs taproot input with or without witness script with provided private keys.
144164
func (signer *Signer) signTaprootInput(params signTaprootInputParams) error {
145165
var (

bitcoin/signer/signer_test.go

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,15 @@ func TestSignerMulti(t *testing.T) {
154154
}
155155

156156
// INFO: Build MultiSig 4 of 4.
157-
leafTapScript, err := utils.NewTaprootMultiSigLeafTapScript(tapScriptPrivateKey1, tapScriptPrivateKey2,
158-
tapScriptPrivateKey3, tapScriptPrivateKey4)
157+
leafTapScript, err := utils.NewTaprootMultiSigLeafTapScript(tapScriptPrivateKey1.PubKey(), tapScriptPrivateKey2.PubKey(),
158+
tapScriptPrivateKey3.PubKey(), tapScriptPrivateKey4.PubKey())
159159
require.NoErrorf(t, err, "leaf tapScript building")
160160

161161
leafTapScriptUnspendable, err := utils.NewUnspendableScript([]byte("really_unspendable_!")...)
162162
require.NoErrorf(t, err, "leaf tapScript unspendable building")
163163

164164
// INFO: Generate Taproot address.
165-
taprootAddress, err := utils.NewTaprootAddressFromScripts(chainParams, masterPrivateKey, leafTapScript, leafTapScriptUnspendable)
165+
taprootAddress, err := utils.NewTaprootAddressFromScripts(chainParams, masterPrivateKey.PubKey(), leafTapScript, leafTapScriptUnspendable)
166166
require.NoErrorf(t, err, "taproot address generation")
167167

168168
// INFO: Generate TapScript tree.
@@ -257,6 +257,127 @@ func TestSignerMulti(t *testing.T) {
257257
}
258258
}
259259

260+
func TestSignerMultiAppend(t *testing.T) {
261+
chainParams := &chaincfg.MainNetParams
262+
s := signer.NewSigner(chainParams)
263+
264+
var (
265+
masterPrivateKey,
266+
tapScriptPrivateKey1, tapScriptPrivateKey2,
267+
tapScriptPrivateKey3, tapScriptPrivateKey4,
268+
invalidPrivateKey1, invalidPrivateKey2 *btcec.PrivateKey
269+
err error
270+
)
271+
for _, privateKeyP := range []**btcec.PrivateKey{
272+
&masterPrivateKey, &tapScriptPrivateKey1, &tapScriptPrivateKey2,
273+
&tapScriptPrivateKey3, &tapScriptPrivateKey4, &invalidPrivateKey1, &invalidPrivateKey2,
274+
} {
275+
*privateKeyP, err = btcec.NewPrivateKey()
276+
require.NoError(t, err)
277+
}
278+
279+
// INFO: Build MultiSig 4 of 4.
280+
leafTapScript, err := utils.NewTaprootMultiSigLeafTapScript(tapScriptPrivateKey1.PubKey(), tapScriptPrivateKey2.PubKey(),
281+
tapScriptPrivateKey3.PubKey(), tapScriptPrivateKey4.PubKey())
282+
require.NoErrorf(t, err, "leaf tapScript building")
283+
284+
leafTapScriptUnspendable, err := utils.NewUnspendableScript([]byte("really_unspendable_!")...)
285+
require.NoErrorf(t, err, "leaf tapScript unspendable building")
286+
287+
// INFO: Generate Taproot address.
288+
taprootAddress, err := utils.NewTaprootAddressFromScripts(chainParams, masterPrivateKey.PubKey(), leafTapScript, leafTapScriptUnspendable)
289+
require.NoErrorf(t, err, "taproot address generation")
290+
291+
// INFO: Generate TapScript tree.
292+
tapScriptTree, err := utils.NewTapScriptTreeFromRawScripts(leafTapScript, leafTapScriptUnspendable)
293+
require.NoErrorf(t, err, "tapScript tree generation")
294+
295+
invalidTapScriptTree, err := utils.NewTapScriptTreeFromRawScripts(leafTapScript)
296+
require.NoErrorf(t, err, "tapScript tree invalid generation")
297+
298+
masterPublicKeyXOnly := masterPrivateKey.PubKey().SerializeCompressed()[1:]
299+
300+
tests := []struct {
301+
name string
302+
tapScriptPrivateKeys []*btcec.PrivateKey
303+
tapScriptTree *txscript.IndexedTapScriptTree
304+
err error
305+
}{
306+
{
307+
name: "valid 4 of 4 signature",
308+
tapScriptPrivateKeys: []*btcec.PrivateKey{tapScriptPrivateKey4, tapScriptPrivateKey3, tapScriptPrivateKey2, tapScriptPrivateKey1},
309+
tapScriptTree: tapScriptTree,
310+
},
311+
{
312+
name: "not enough signatures (3 of 4 private keys for leaf signatures)",
313+
tapScriptPrivateKeys: []*btcec.PrivateKey{tapScriptPrivateKey3, tapScriptPrivateKey2, tapScriptPrivateKey1},
314+
tapScriptTree: tapScriptTree,
315+
err: txscript.Error{ErrorCode: txscript.ErrInvalidStackOperation, Description: "index 0 is invalid for stack size 0"},
316+
},
317+
{
318+
name: "private keys invalid order",
319+
tapScriptPrivateKeys: []*btcec.PrivateKey{tapScriptPrivateKey1, tapScriptPrivateKey2, tapScriptPrivateKey3, tapScriptPrivateKey4},
320+
tapScriptTree: tapScriptTree,
321+
err: txscript.Error{ErrorCode: txscript.ErrNullFail, Description: "signature not empty on failed checksig"},
322+
},
323+
{
324+
name: "invalid leaf keys",
325+
tapScriptPrivateKeys: []*btcec.PrivateKey{invalidPrivateKey1, tapScriptPrivateKey3, tapScriptPrivateKey2, invalidPrivateKey2},
326+
tapScriptTree: tapScriptTree,
327+
err: txscript.Error{ErrorCode: txscript.ErrNullFail, Description: "signature not empty on failed checksig"},
328+
},
329+
{
330+
name: "unable to unlock by script spend path without correct script tree",
331+
tapScriptPrivateKeys: []*btcec.PrivateKey{tapScriptPrivateKey4, tapScriptPrivateKey3, tapScriptPrivateKey2, tapScriptPrivateKey1},
332+
err: txscript.Error{ErrorCode: txscript.ErrTaprootMerkleProofInvalid},
333+
},
334+
{
335+
name: "unable to unlock by script spend path with incorrect script tree",
336+
tapScriptPrivateKeys: []*btcec.PrivateKey{tapScriptPrivateKey4, tapScriptPrivateKey3, tapScriptPrivateKey2, tapScriptPrivateKey1},
337+
tapScriptTree: invalidTapScriptTree,
338+
err: txscript.Error{ErrorCode: txscript.ErrTaprootMerkleProofInvalid},
339+
},
340+
}
341+
for _, test := range tests {
342+
t.Run(test.name, func(t *testing.T) {
343+
var signedPSBTs [2][]byte
344+
t.Run("group signature", func(t *testing.T) {
345+
packetBytes := prepareTxPacketBytes(t, taprootAddress, masterPublicKeyXOnly, leafTapScript, test.tapScriptTree)
346+
347+
signedPSBTs[0], err = s.SignTaprootMulti(signer.SignTaprootMultiParams{
348+
SerializedPSBT: packetBytes,
349+
Inputs: []int{0},
350+
TapScriptPrivateKeys: test.tapScriptPrivateKeys,
351+
})
352+
require.NoError(t, err)
353+
354+
err = prepareMultiSigEngine(t, signedPSBTs[0]).Execute()
355+
require.ErrorIs(t, err, test.err)
356+
})
357+
358+
t.Run("append signature", func(t *testing.T) {
359+
packetBytes := prepareTxPacketBytes(t, taprootAddress, masterPublicKeyXOnly, leafTapScript, test.tapScriptTree)
360+
361+
signParams := signer.SignTaprootMultiAppendParams{
362+
SerializedPSBT: packetBytes,
363+
Inputs: []int{0},
364+
}
365+
for _, tapScriptPrivateKey := range test.tapScriptPrivateKeys {
366+
signParams.TapScriptPrivateKey = tapScriptPrivateKey
367+
signParams.SerializedPSBT, err = s.SignTaprootMultiAppend(signParams)
368+
require.NoError(t, err)
369+
}
370+
371+
signedPSBTs[1] = signParams.SerializedPSBT
372+
err = prepareMultiSigEngine(t, signedPSBTs[1]).Execute()
373+
require.ErrorIs(t, err, test.err)
374+
})
375+
376+
require.EqualValues(t, signedPSBTs[0], signedPSBTs[1])
377+
})
378+
}
379+
}
380+
260381
func mustHex(s string) []byte {
261382
b, _ := hex.DecodeString(s)
262383

bitcoin/txbuilder/insufficienterror.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88
"math/big"
99
)
1010

11+
// BalanceErrorType defines currency specification type for balance errors.
1112
type BalanceErrorType string
1213

14+
// CauserSign defines causer specification type for balances errors.
1315
type CauserSign string
1416

1517
const (

bitcoin/txbuilder/txbuilder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ var (
4747
nonDustBitcoinAmount = big.NewInt(546)
4848
)
4949

50-
// NonDustBitcoinAmount provides public usage of variable nonDustBitcoinAmount
50+
// NonDustBitcoinAmount provides public usage of variable nonDustBitcoinAmount.
5151
func NonDustBitcoinAmount() *big.Int { return big.NewInt(546) }
5252

5353
const (

bitcoin/utils/addresses.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ import (
1313

1414
// NewTaprootAddressWithMultiSig generates taproot address with one leaf tapScript that holds multi-sig locking script build on provided privateKeys.
1515
// NOTE: At least 2 private keys for multi-sig script generation is required.
16-
func NewTaprootAddressWithMultiSig(chainParams *chaincfg.Params, masterPrivateKey *btcec.PrivateKey, privateKeys ...*btcec.PrivateKey) (*btcutil.AddressTaproot, error) {
17-
leafTapScript, err := NewTaprootMultiSigLeafTapScript(privateKeys...)
16+
func NewTaprootAddressWithMultiSig(chainParams *chaincfg.Params, masterPublicKey *btcec.PublicKey, publicKeys ...*btcec.PublicKey) (*btcutil.AddressTaproot, error) {
17+
leafTapScript, err := NewTaprootMultiSigLeafTapScript(publicKeys...)
1818
if err != nil {
1919
return nil, err
2020
}
2121

22-
return NewTaprootAddressFromScripts(chainParams, masterPrivateKey, leafTapScript)
22+
return NewTaprootAddressFromScripts(chainParams, masterPublicKey, leafTapScript)
2323
}
2424

2525
// MustTaprootAddressWithMultiSig uses NewTaprootAddressWithMultiSig, panics in case of error.
26-
func MustTaprootAddressWithMultiSig(chainParams *chaincfg.Params, masterPrivateKey *btcec.PrivateKey, privateKeys ...*btcec.PrivateKey) *btcutil.AddressTaproot {
27-
address, err := NewTaprootAddressWithMultiSig(chainParams, masterPrivateKey, privateKeys...)
26+
func MustTaprootAddressWithMultiSig(chainParams *chaincfg.Params, masterPublicKey *btcec.PublicKey, publicKeys ...*btcec.PublicKey) *btcutil.AddressTaproot {
27+
address, err := NewTaprootAddressWithMultiSig(chainParams, masterPublicKey, publicKeys...)
2828
if err != nil {
2929
panic(err)
3030
}
@@ -33,21 +33,21 @@ func MustTaprootAddressWithMultiSig(chainParams *chaincfg.Params, masterPrivateK
3333
}
3434

3535
// NewTaprootAddressFromScripts generates taproot address with tree built from provided leaf scripts.
36-
func NewTaprootAddressFromScripts(chainParams *chaincfg.Params, masterPrivateKey *btcec.PrivateKey, leafScripts ...[]byte) (*btcutil.AddressTaproot, error) {
36+
func NewTaprootAddressFromScripts(chainParams *chaincfg.Params, masterPublicKey *btcec.PublicKey, leafScripts ...[]byte) (*btcutil.AddressTaproot, error) {
3737
tapScriptTree, err := NewTapScriptTreeFromRawScripts(leafScripts...)
3838
if err != nil {
3939
return nil, err
4040
}
4141

4242
tapScriptRootHash := tapScriptTree.RootNode.TapHash()
43-
outputKey := txscript.ComputeTaprootOutputKey(masterPrivateKey.PubKey(), tapScriptRootHash[:])
43+
outputKey := txscript.ComputeTaprootOutputKey(masterPublicKey, tapScriptRootHash[:])
4444

4545
return btcutil.NewAddressTaproot(schnorr.SerializePubKey(outputKey), chainParams)
4646
}
4747

4848
// MustTaprootAddressFromScripts uses NewTaprootAddressFromScripts, panics in case of error.
49-
func MustTaprootAddressFromScripts(chainParams *chaincfg.Params, masterPrivateKey *btcec.PrivateKey, leafScripts ...[]byte) *btcutil.AddressTaproot {
50-
address, err := NewTaprootAddressFromScripts(chainParams, masterPrivateKey, leafScripts...)
49+
func MustTaprootAddressFromScripts(chainParams *chaincfg.Params, masterPublicKey *btcec.PublicKey, leafScripts ...[]byte) *btcutil.AddressTaproot {
50+
address, err := NewTaprootAddressFromScripts(chainParams, masterPublicKey, leafScripts...)
5151
if err != nil {
5252
panic(err)
5353
}

bitcoin/utils/scripts.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,36 +13,38 @@ import (
1313
)
1414

1515
// NewTaprootMultiSigLeafTapScript generates N of N multi-sig locking script for taproot leaf.
16+
//
1617
// INFO: Script will have the next format: {<pubKey1> OP_CHECKSIG [<pubKey2> OP_CHECKSIG_ADD [<pubKey3> OP_CHECKSIG_ADD ...]] <signListSize> OP_EQUAL}.
17-
// NOTE: At least 2 private keys for multi-sig script generation is required.
18-
func NewTaprootMultiSigLeafTapScript(privateKeys ...*btcec.PrivateKey) ([]byte, error) {
19-
if len(privateKeys) < 2 {
20-
return nil, errors.New("at least 2 private keys are required")
18+
//
19+
// NOTE: At least 2 keys for multi-sig script generation is required.
20+
func NewTaprootMultiSigLeafTapScript(keys ...*btcec.PublicKey) ([]byte, error) {
21+
if len(keys) < 2 {
22+
return nil, errors.New("at least 2 keys are required")
2123
}
22-
if len(privateKeys) > 999 {
23-
return nil, errors.New("max allowed private keys: 999")
24+
if len(keys) > 999 {
25+
return nil, errors.New("max allowed keys: 999")
2426
}
2527

2628
checkSigOp := byte(txscript.OP_CHECKSIG)
2729
scriptBuilder := txscript.NewScriptBuilder()
28-
for i, privateKey := range privateKeys {
30+
for i, key := range keys {
2931
scriptBuilder.
30-
AddData(privateKey.PubKey().SerializeCompressed()[1:]).
32+
AddData(key.SerializeCompressed()[1:]).
3133
AddOp(checkSigOp)
3234
if i == 0 {
3335
checkSigOp = txscript.OP_CHECKSIGADD
3436
}
3537
}
3638

3739
return scriptBuilder.
38-
AddInt64(int64(len(privateKeys))).
40+
AddInt64(int64(len(keys))).
3941
AddOp(txscript.OP_EQUAL).
4042
Script()
4143
}
4244

4345
// MustTaprootMultiSigLeafTapScript uses NewTaprootMultiSigLeafTapScript, panics in case of error.
44-
func MustTaprootMultiSigLeafTapScript(privateKeys ...*btcec.PrivateKey) []byte {
45-
script, err := NewTaprootMultiSigLeafTapScript(privateKeys...)
46+
func MustTaprootMultiSigLeafTapScript(keys ...*btcec.PublicKey) []byte {
47+
script, err := NewTaprootMultiSigLeafTapScript(keys...)
4648
if err != nil {
4749
panic(err)
4850
}

0 commit comments

Comments
 (0)