Skip to content

Commit 8d51e66

Browse files
committed
taproot script multisig support added, utils for address and script generation created, updated signer tests
1 parent a6c54dc commit 8d51e66

5 files changed

Lines changed: 515 additions & 47 deletions

File tree

.golangci.yml

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
run:
2-
timeout: 1m
2+
deadline: 10m
33
issues-exit-code: 1
44
tests: true
55

@@ -11,7 +11,7 @@ linters:
1111
- durationcheck # verifies whether durations are multiplied, usually a mistake
1212
- errcheck # find unchecked errors
1313
# - errorlint # finds misuses of errors
14-
- exportloopref # check for exported loop vars
14+
- copyloopvar # check for exported loop vars
1515
- gocritic # checks for style, performance issues, and common programming errors
1616
- godot # dots for everything
1717
- err113 # check error expressions
@@ -26,7 +26,7 @@ linters:
2626
- nilerr # checks for misuses of `if err != nil { return nil }`
2727
- noctx # finds locations that should use context
2828
- revive # check standard linting rules
29-
- tenv # ensure we use t.SetEnv instead of os.SetEnv
29+
- usetesting # ensure we use t.SetEnv instead of os.SetEnv
3030
- unconvert # remove unnecessary conversions
3131
- wastedassign
3232
disable:
@@ -36,14 +36,12 @@ linters:
3636
- containedctx # gives false positives, however might be good to re-evaluate
3737
- contextcheck # doesn't look like it's useful
3838
- cyclop # this complexity is not a good metric
39-
- deadcode # deprecated and part of staticcheck
4039
- decorder # not that useful
4140
- depguard # unused
4241
- dupl # slow
4342
- errchkjson # false positives, checks for non-encodable json types
4443
- errname # we have different approach
4544
- exhaustive # doesn't handle default case
46-
- exhaustivestruct # false positivies
4745
- forbidigo # not useful
4846
- funlen # no limit on func length
4947
- gci # we have custom import checking
@@ -53,14 +51,11 @@ linters:
5351
- godox # too many false positivies
5452
- goheader # separate tool
5553
- goimports # disabled, because it's slow, using scripts/check-imports.go instead.
56-
- gomnd # false positives
5754
- gomoddirectives # not useful
5855
- gomodguard # not useful
5956
- gosec # needs tweaking
6057
- gosimple # part of staticcheck
6158
- grouper # we have a custom implementation
62-
- ifshort # usefulness, depends on the context
63-
- interfacer # not that useful
6459
- ireturn # not that useful for us
6560
- lll # don't need this check
6661
- maintidx # code complexity based on halsted V and cyclomatic, both shown to be ineffective
@@ -70,14 +65,12 @@ linters:
7065
- promlinter # not relevant
7166
- rowserrcheck # checks if sql.Rows.Err is checked correctly - Disabled because it reports false positive with defer statements after Query call
7267
- sqlclosecheck # we have tagsql, which checks this better
73-
- structcheck # deprecated and part of staticcheck
7468
- stylecheck # has false positives
7569
- tagliatelle # not our style
7670
- testpackage # sometimes it's useful to have tests on private funcs
7771
- thelper # too many false positives
7872
- tparallel # false positivies
7973
- unused # part of staticcheck
80-
- varcheck # deprecated and part of staticcheck
8174
- varnamelen # unenecssary
8275
- wrapcheck # too much noise and false positives
8376
- wsl # too much noise
@@ -118,7 +111,7 @@ linters-settings:
118111
disabled-checks:
119112
- ifElseChain
120113
goimports:
121-
local-prefixes: "blockchain"
114+
local-prefixes: "tricorn"
122115
golint:
123116
min-confidence: 0.8
124117
gofmt:

bitcoin/signer/signer.go

Lines changed: 151 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"errors"
99

1010
"github.com/btcsuite/btcd/btcec/v2"
11+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
1112
"github.com/btcsuite/btcd/btcutil/psbt"
1213
"github.com/btcsuite/btcd/chaincfg"
1314
"github.com/btcsuite/btcd/txscript"
@@ -21,12 +22,26 @@ type SignTaprootParams struct {
2122
PrivateKey *btcec.PrivateKey
2223
}
2324

25+
// SignTaprootMultiParams defines parameters for SignTaprootMulti method.
26+
//
27+
// NOTE: TapScriptPrivateKeys must be in reverse order relatively to public keys in locking script!!!
28+
// Ex.: Locking script pub keys order: {<pub1>, <pub2>, ..., <pubN>}, then private keys order must be: {<prN>, ... <pr2>, <pr1>}.
29+
//
30+
// INFO: Either MasterPrivateKey or TapScriptPrivateKeys must be provided.
31+
type SignTaprootMultiParams struct {
32+
SerializedPSBT []byte
33+
Inputs []int // inputs indexes.
34+
MasterPrivateKey *btcec.PrivateKey // primary key which is used to create taproot public key (not tweaked).
35+
TapScriptPrivateKeys []*btcec.PrivateKey // holds private keys needed to unlock MultiSig tapScript. Key-spend path will be used in case of empty array.
36+
}
37+
2438
// signTaprootInputParams defines parameters for signTaprootInput method.
2539
type signTaprootInputParams struct {
26-
packet *psbt.Packet
27-
input int
28-
inputFetcher txscript.PrevOutputFetcher
29-
privateKey *btcec.PrivateKey
40+
packet *psbt.Packet
41+
input int
42+
inputFetcher txscript.PrevOutputFetcher
43+
masterPrivateKey *btcec.PrivateKey
44+
tapScriptPrivateKeys []*btcec.PrivateKey
3045
}
3146

3247
// Signer provides transaction signing related logic.
@@ -63,10 +78,11 @@ func (signer *Signer) SignTaproot(params SignTaprootParams) ([]byte, error) {
6378
}
6479

6580
err = signer.signTaprootInput(signTaprootInputParams{
66-
packet: packet,
67-
input: input,
68-
inputFetcher: prevOutputFetcher,
69-
privateKey: params.PrivateKey,
81+
packet: packet,
82+
input: input,
83+
inputFetcher: prevOutputFetcher,
84+
masterPrivateKey: params.PrivateKey,
85+
tapScriptPrivateKeys: []*btcec.PrivateKey{params.PrivateKey},
7086
})
7187
if err != nil {
7288
return nil, err
@@ -82,7 +98,49 @@ func (signer *Signer) SignTaproot(params SignTaprootParams) ([]byte, error) {
8298
return w.Bytes(), nil
8399
}
84100

85-
// signTaprootInput signs taproot input with or without witness script.
101+
// SignTaprootMulti signs taproot inputs by provided indexes using 1+ private keys, returns updated serialized PSBT.
102+
func (signer *Signer) SignTaprootMulti(params SignTaprootMultiParams) ([]byte, error) {
103+
packet, err := psbt.NewFromRawBytes(bytes.NewBuffer(params.SerializedPSBT), false)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
var (
109+
tx = packet.UnsignedTx
110+
prevOutputFetcherMap = make(map[wire.OutPoint]*wire.TxOut, len(tx.TxIn))
111+
)
112+
for idx, in := range packet.Inputs {
113+
prevOutputFetcherMap[tx.TxIn[idx].PreviousOutPoint] = in.WitnessUtxo
114+
}
115+
116+
var prevOutputFetcher = txscript.NewMultiPrevOutFetcher(prevOutputFetcherMap)
117+
for _, input := range params.Inputs {
118+
if len(packet.Inputs) <= input {
119+
return nil, errors.New("invalid input index")
120+
}
121+
122+
err = signer.signTaprootInput(signTaprootInputParams{
123+
packet: packet,
124+
input: input,
125+
inputFetcher: prevOutputFetcher,
126+
masterPrivateKey: params.MasterPrivateKey,
127+
tapScriptPrivateKeys: params.TapScriptPrivateKeys,
128+
})
129+
if err != nil {
130+
return nil, err
131+
}
132+
}
133+
134+
w := bytes.NewBuffer(nil)
135+
err = packet.Serialize(w)
136+
if err != nil {
137+
return nil, err
138+
}
139+
140+
return w.Bytes(), nil
141+
}
142+
143+
// signTaprootInput signs taproot input with or without witness script with provided private keys.
86144
func (signer *Signer) signTaprootInput(params signTaprootInputParams) error {
87145
var (
88146
input = &params.packet.Inputs[params.input]
@@ -96,49 +154,53 @@ func (signer *Signer) signTaprootInput(params signTaprootInputParams) error {
96154

97155
if len(input.WitnessScript) != 0 {
98156
var (
99-
tapLeaf = txscript.NewBaseTapLeaf(input.WitnessScript)
100-
tapScriptTree = txscript.AssembleTaprootScriptTree(tapLeaf)
101-
ctrlBlock = tapScriptTree.LeafMerkleProofs[0].ToControlBlock(params.privateKey.PubKey())
102-
ctrlBlockBytes []byte
103-
sig []byte
104-
leafHash = tapLeaf.TapHash()
157+
sig []byte
158+
tsrd *taprootSignatureRequiredData
105159
)
106160

107-
ctrlBlockBytes, err = ctrlBlock.ToBytes()
161+
tsrd, err = recoverTaprootSignatureRequiredData(input)
108162
if err != nil {
109163
return err
110164
}
111165

112-
sig, err = txscript.RawTxInTapscriptSignature(
113-
params.packet.UnsignedTx, sigHashes, params.input,
114-
value, pkScript, tapLeaf, sigHashType, params.privateKey,
115-
)
116-
if err != nil {
166+
if len(params.tapScriptPrivateKeys) == 0 {
167+
if params.masterPrivateKey == nil {
168+
return errors.New("either master private key or tapScript private keys list was expected")
169+
}
170+
171+
input.TaprootKeySpendSig, err = txscript.RawTxInTaprootSignature(
172+
params.packet.UnsignedTx, sigHashes, params.input, value, pkScript,
173+
input.TaprootMerkleRoot, sigHashType, params.masterPrivateKey)
174+
117175
return err
118176
}
119177

120-
if len(sig) > 64 {
121-
sig = sig[:64]
178+
for _, privateKey := range params.tapScriptPrivateKeys {
179+
sig, err = txscript.RawTxInTapscriptSignature(
180+
params.packet.UnsignedTx, sigHashes, params.input,
181+
value, pkScript, tsrd.tapLeaf, sigHashType, privateKey,
182+
)
183+
if err != nil {
184+
return err
185+
}
186+
187+
if len(sig) > 64 {
188+
sig = sig[:64]
189+
}
190+
input.TaprootScriptSpendSig = append(input.TaprootScriptSpendSig, &psbt.TaprootScriptSpendSig{
191+
XOnlyPubKey: privateKey.PubKey().SerializeCompressed()[1:],
192+
LeafHash: tsrd.leafHash,
193+
Signature: sig,
194+
SigHash: sigHashType,
195+
})
122196
}
123-
input.TaprootScriptSpendSig = []*psbt.TaprootScriptSpendSig{{
124-
XOnlyPubKey: params.privateKey.PubKey().SerializeCompressed()[1:],
125-
LeafHash: leafHash.CloneBytes(),
126-
Signature: sig,
127-
SigHash: sigHashType,
128-
}}
129-
130-
input.TaprootLeafScript = []*psbt.TaprootTapLeafScript{{
131-
ControlBlock: ctrlBlockBytes,
132-
Script: tapLeaf.Script,
133-
LeafVersion: tapLeaf.LeafVersion,
134-
}}
135197

136198
return nil
137199
}
138200

139201
witness, err = txscript.TaprootWitnessSignature(
140202
params.packet.UnsignedTx, sigHashes, params.input,
141-
value, pkScript, sigHashType, params.privateKey)
203+
value, pkScript, sigHashType, params.masterPrivateKey)
142204
if err != nil {
143205
return err
144206
}
@@ -147,3 +209,56 @@ func (signer *Signer) signTaprootInput(params signTaprootInputParams) error {
147209

148210
return nil
149211
}
212+
213+
type taprootSignatureRequiredData struct {
214+
ctrlBlock *txscript.ControlBlock
215+
tapLeaf txscript.TapLeaf
216+
leafHash []byte
217+
}
218+
219+
// recoverTaprootSignatureRequiredData parses all needed data from PSBT or recover from WitnessScript if not found and updates provided input with it.
220+
func recoverTaprootSignatureRequiredData(input *psbt.PInput) (tsrd *taprootSignatureRequiredData, err error) {
221+
if len(input.TaprootInternalKey) == 0 {
222+
return nil, errors.New("taproot internal key is empty")
223+
}
224+
225+
var masterPublicKey *btcec.PublicKey
226+
masterPublicKey, err = schnorr.ParsePubKey(input.TaprootInternalKey)
227+
if err != nil {
228+
return nil, err
229+
}
230+
231+
tsrd = new(taprootSignatureRequiredData)
232+
if len(input.TaprootLeafScript) > 0 && input.TaprootLeafScript[0] != nil {
233+
leafScriptData := input.TaprootLeafScript[0]
234+
tsrd.tapLeaf = txscript.NewTapLeaf(leafScriptData.LeafVersion, leafScriptData.Script)
235+
tsrd.ctrlBlock, err = txscript.ParseControlBlock(leafScriptData.ControlBlock)
236+
if err != nil {
237+
return nil, err
238+
}
239+
} else {
240+
tsrd.tapLeaf = txscript.NewBaseTapLeaf(input.WitnessScript)
241+
tapScriptTree := txscript.AssembleTaprootScriptTree(tsrd.tapLeaf)
242+
ctrlBlock := tapScriptTree.LeafMerkleProofs[0].ToControlBlock(masterPublicKey)
243+
tsrd.ctrlBlock = &ctrlBlock
244+
245+
tapLeafScript := &psbt.TaprootTapLeafScript{
246+
Script: tsrd.tapLeaf.Script,
247+
LeafVersion: tsrd.tapLeaf.LeafVersion,
248+
}
249+
tapLeafScript.ControlBlock, err = tsrd.ctrlBlock.ToBytes()
250+
if err != nil {
251+
return nil, err
252+
}
253+
254+
input.TaprootLeafScript = []*psbt.TaprootTapLeafScript{tapLeafScript}
255+
}
256+
leafHash := tsrd.tapLeaf.TapHash()
257+
tsrd.leafHash = leafHash[:]
258+
259+
if len(input.TaprootMerkleRoot) == 0 {
260+
input.TaprootMerkleRoot = tsrd.ctrlBlock.RootHash(tsrd.tapLeaf.Script)
261+
}
262+
263+
return tsrd, nil
264+
}

0 commit comments

Comments
 (0)