Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
run:
timeout: 1m
deadline: 10m
issues-exit-code: 1
tests: true

Expand All @@ -11,7 +11,7 @@ linters:
- durationcheck # verifies whether durations are multiplied, usually a mistake
- errcheck # find unchecked errors
# - errorlint # finds misuses of errors
- exportloopref # check for exported loop vars
- copyloopvar # check for exported loop vars
- gocritic # checks for style, performance issues, and common programming errors
- godot # dots for everything
- err113 # check error expressions
Expand All @@ -26,7 +26,7 @@ linters:
- nilerr # checks for misuses of `if err != nil { return nil }`
- noctx # finds locations that should use context
- revive # check standard linting rules
- tenv # ensure we use t.SetEnv instead of os.SetEnv
- usetesting # ensure we use t.SetEnv instead of os.SetEnv
- unconvert # remove unnecessary conversions
- wastedassign
disable:
Expand All @@ -36,14 +36,12 @@ linters:
- containedctx # gives false positives, however might be good to re-evaluate
- contextcheck # doesn't look like it's useful
- cyclop # this complexity is not a good metric
- deadcode # deprecated and part of staticcheck
- decorder # not that useful
- depguard # unused
- dupl # slow
- errchkjson # false positives, checks for non-encodable json types
- errname # we have different approach
- exhaustive # doesn't handle default case
- exhaustivestruct # false positivies
- forbidigo # not useful
- funlen # no limit on func length
- gci # we have custom import checking
Expand All @@ -53,14 +51,11 @@ linters:
- godox # too many false positivies
- goheader # separate tool
- goimports # disabled, because it's slow, using scripts/check-imports.go instead.
- gomnd # false positives
- gomoddirectives # not useful
- gomodguard # not useful
- gosec # needs tweaking
- gosimple # part of staticcheck
- grouper # we have a custom implementation
- ifshort # usefulness, depends on the context
- interfacer # not that useful
- ireturn # not that useful for us
- lll # don't need this check
- maintidx # code complexity based on halsted V and cyclomatic, both shown to be ineffective
Expand All @@ -70,14 +65,12 @@ linters:
- promlinter # not relevant
- rowserrcheck # checks if sql.Rows.Err is checked correctly - Disabled because it reports false positive with defer statements after Query call
- sqlclosecheck # we have tagsql, which checks this better
- structcheck # deprecated and part of staticcheck
- stylecheck # has false positives
- tagliatelle # not our style
- testpackage # sometimes it's useful to have tests on private funcs
- thelper # too many false positives
- tparallel # false positivies
- unused # part of staticcheck
- varcheck # deprecated and part of staticcheck
- varnamelen # unenecssary
- wrapcheck # too much noise and false positives
- wsl # too much noise
Expand Down Expand Up @@ -118,7 +111,7 @@ linters-settings:
disabled-checks:
- ifElseChain
goimports:
local-prefixes: "blockchain"
local-prefixes: "tricorn"
golint:
min-confidence: 0.8
gofmt:
Expand Down
187 changes: 151 additions & 36 deletions bitcoin/signer/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
Expand All @@ -21,12 +22,26 @@ type SignTaprootParams struct {
PrivateKey *btcec.PrivateKey
}

// SignTaprootMultiParams defines parameters for SignTaprootMulti method.
//
// NOTE: TapScriptPrivateKeys must be in reverse order relatively to public keys in locking script!!!
// Ex.: Locking script pub keys order: {<pub1>, <pub2>, ..., <pubN>}, then private keys order must be: {<prN>, ... <pr2>, <pr1>}.
//
// INFO: Either MasterPrivateKey or TapScriptPrivateKeys must be provided.
type SignTaprootMultiParams struct {
SerializedPSBT []byte
Inputs []int // inputs indexes.
MasterPrivateKey *btcec.PrivateKey // primary key which is used to create taproot public key (not tweaked).
TapScriptPrivateKeys []*btcec.PrivateKey // holds private keys needed to unlock MultiSig tapScript. Key-spend path will be used in case of empty array.
}

// signTaprootInputParams defines parameters for signTaprootInput method.
type signTaprootInputParams struct {
packet *psbt.Packet
input int
inputFetcher txscript.PrevOutputFetcher
privateKey *btcec.PrivateKey
packet *psbt.Packet
input int
inputFetcher txscript.PrevOutputFetcher
masterPrivateKey *btcec.PrivateKey
tapScriptPrivateKeys []*btcec.PrivateKey
}

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

err = signer.signTaprootInput(signTaprootInputParams{
packet: packet,
input: input,
inputFetcher: prevOutputFetcher,
privateKey: params.PrivateKey,
packet: packet,
input: input,
inputFetcher: prevOutputFetcher,
masterPrivateKey: params.PrivateKey,
tapScriptPrivateKeys: []*btcec.PrivateKey{params.PrivateKey},
})
if err != nil {
return nil, err
Expand All @@ -82,7 +98,49 @@ func (signer *Signer) SignTaproot(params SignTaprootParams) ([]byte, error) {
return w.Bytes(), nil
}

// signTaprootInput signs taproot input with or without witness script.
// SignTaprootMulti signs taproot inputs by provided indexes using 1+ private keys, returns updated serialized PSBT.
func (signer *Signer) SignTaprootMulti(params SignTaprootMultiParams) ([]byte, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi or MuSig?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi

packet, err := psbt.NewFromRawBytes(bytes.NewBuffer(params.SerializedPSBT), false)
if err != nil {
return nil, err
}

var (
tx = packet.UnsignedTx
prevOutputFetcherMap = make(map[wire.OutPoint]*wire.TxOut, len(tx.TxIn))
)
for idx, in := range packet.Inputs {
prevOutputFetcherMap[tx.TxIn[idx].PreviousOutPoint] = in.WitnessUtxo
}

var prevOutputFetcher = txscript.NewMultiPrevOutFetcher(prevOutputFetcherMap)
for _, input := range params.Inputs {
if len(packet.Inputs) <= input {
return nil, errors.New("invalid input index")
}

err = signer.signTaprootInput(signTaprootInputParams{
packet: packet,
input: input,
inputFetcher: prevOutputFetcher,
masterPrivateKey: params.MasterPrivateKey,
tapScriptPrivateKeys: params.TapScriptPrivateKeys,
})
if err != nil {
return nil, err
}
}

w := bytes.NewBuffer(nil)
err = packet.Serialize(w)
if err != nil {
return nil, err
}

return w.Bytes(), nil
}

// signTaprootInput signs taproot input with or without witness script with provided private keys.
func (signer *Signer) signTaprootInput(params signTaprootInputParams) error {
var (
input = &params.packet.Inputs[params.input]
Expand All @@ -96,49 +154,53 @@ func (signer *Signer) signTaprootInput(params signTaprootInputParams) error {

if len(input.WitnessScript) != 0 {
var (
tapLeaf = txscript.NewBaseTapLeaf(input.WitnessScript)
tapScriptTree = txscript.AssembleTaprootScriptTree(tapLeaf)
ctrlBlock = tapScriptTree.LeafMerkleProofs[0].ToControlBlock(params.privateKey.PubKey())
ctrlBlockBytes []byte
sig []byte
leafHash = tapLeaf.TapHash()
sig []byte
tsrd *taprootSignatureRequiredData
)

ctrlBlockBytes, err = ctrlBlock.ToBytes()
tsrd, err = recoverTaprootSignatureRequiredData(input)
if err != nil {
return err
}

sig, err = txscript.RawTxInTapscriptSignature(
params.packet.UnsignedTx, sigHashes, params.input,
value, pkScript, tapLeaf, sigHashType, params.privateKey,
)
if err != nil {
if len(params.tapScriptPrivateKeys) == 0 {
if params.masterPrivateKey == nil {
return errors.New("either master private key or tapScript private keys list was expected")
}

input.TaprootKeySpendSig, err = txscript.RawTxInTaprootSignature(
params.packet.UnsignedTx, sigHashes, params.input, value, pkScript,
input.TaprootMerkleRoot, sigHashType, params.masterPrivateKey)

return err
}

if len(sig) > 64 {
sig = sig[:64]
for _, privateKey := range params.tapScriptPrivateKeys {
sig, err = txscript.RawTxInTapscriptSignature(
params.packet.UnsignedTx, sigHashes, params.input,
value, pkScript, tsrd.tapLeaf, sigHashType, privateKey,
)
if err != nil {
return err
}

if len(sig) > 64 {
sig = sig[:64]
}
input.TaprootScriptSpendSig = append(input.TaprootScriptSpendSig, &psbt.TaprootScriptSpendSig{
XOnlyPubKey: privateKey.PubKey().SerializeCompressed()[1:],
LeafHash: tsrd.leafHash,
Signature: sig,
SigHash: sigHashType,
})
}
input.TaprootScriptSpendSig = []*psbt.TaprootScriptSpendSig{{
XOnlyPubKey: params.privateKey.PubKey().SerializeCompressed()[1:],
LeafHash: leafHash.CloneBytes(),
Signature: sig,
SigHash: sigHashType,
}}

input.TaprootLeafScript = []*psbt.TaprootTapLeafScript{{
ControlBlock: ctrlBlockBytes,
Script: tapLeaf.Script,
LeafVersion: tapLeaf.LeafVersion,
}}

return nil
}

witness, err = txscript.TaprootWitnessSignature(
params.packet.UnsignedTx, sigHashes, params.input,
value, pkScript, sigHashType, params.privateKey)
value, pkScript, sigHashType, params.masterPrivateKey)
if err != nil {
return err
}
Expand All @@ -147,3 +209,56 @@ func (signer *Signer) signTaprootInput(params signTaprootInputParams) error {

return nil
}

type taprootSignatureRequiredData struct {
ctrlBlock *txscript.ControlBlock
tapLeaf txscript.TapLeaf
leafHash []byte
}

// recoverTaprootSignatureRequiredData parses all needed data from PSBT or recover from WitnessScript if not found and updates provided input with it.
func recoverTaprootSignatureRequiredData(input *psbt.PInput) (tsrd *taprootSignatureRequiredData, err error) {
if len(input.TaprootInternalKey) == 0 {
return nil, errors.New("taproot internal key is empty")
}

var masterPublicKey *btcec.PublicKey
masterPublicKey, err = schnorr.ParsePubKey(input.TaprootInternalKey)
if err != nil {
return nil, err
}

tsrd = new(taprootSignatureRequiredData)
if len(input.TaprootLeafScript) > 0 && input.TaprootLeafScript[0] != nil {
leafScriptData := input.TaprootLeafScript[0]
tsrd.tapLeaf = txscript.NewTapLeaf(leafScriptData.LeafVersion, leafScriptData.Script)
tsrd.ctrlBlock, err = txscript.ParseControlBlock(leafScriptData.ControlBlock)
if err != nil {
return nil, err
}
} else {
tsrd.tapLeaf = txscript.NewBaseTapLeaf(input.WitnessScript)
tapScriptTree := txscript.AssembleTaprootScriptTree(tsrd.tapLeaf)
ctrlBlock := tapScriptTree.LeafMerkleProofs[0].ToControlBlock(masterPublicKey)
tsrd.ctrlBlock = &ctrlBlock

tapLeafScript := &psbt.TaprootTapLeafScript{
Script: tsrd.tapLeaf.Script,
LeafVersion: tsrd.tapLeaf.LeafVersion,
}
tapLeafScript.ControlBlock, err = tsrd.ctrlBlock.ToBytes()
if err != nil {
return nil, err
}

input.TaprootLeafScript = []*psbt.TaprootTapLeafScript{tapLeafScript}
}
leafHash := tsrd.tapLeaf.TapHash()
tsrd.leafHash = leafHash[:]

if len(input.TaprootMerkleRoot) == 0 {
input.TaprootMerkleRoot = tsrd.ctrlBlock.RootHash(tsrd.tapLeaf.Script)
}

return tsrd, nil
}
Loading