mirror of
https://github.com/LBRYFoundation/lbcd.git
synced 2025-08-23 17:47:24 +00:00
remove memory pressure from parseScript
This commit is contained in:
parent
9aa4259382
commit
ccfa7af546
11 changed files with 160 additions and 64 deletions
|
@ -65,11 +65,13 @@ func (h *handler) handleTxIns(ct *claimtrie.ClaimTrie) error {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return errors.Errorf("missing input in view for %s", op.String())
|
return errors.Errorf("missing input in view for %s", op.String())
|
||||||
}
|
}
|
||||||
cs, err := txscript.DecodeClaimScript(e.pkScript)
|
cs, closer, err := txscript.DecodeClaimScript(e.pkScript)
|
||||||
if err == txscript.ErrNotClaimScript {
|
if err == txscript.ErrNotClaimScript {
|
||||||
|
closer()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
closer()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,6 +91,7 @@ func (h *handler) handleTxIns(ct *claimtrie.ClaimTrie) error {
|
||||||
copy(id[:], cs.ClaimID())
|
copy(id[:], cs.ClaimID())
|
||||||
err = ct.SpendSupport(name, op, id)
|
err = ct.SpendSupport(name, op, id)
|
||||||
}
|
}
|
||||||
|
closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "handleTxIns")
|
return errors.Wrapf(err, "handleTxIns")
|
||||||
}
|
}
|
||||||
|
@ -99,11 +102,13 @@ func (h *handler) handleTxIns(ct *claimtrie.ClaimTrie) error {
|
||||||
func (h *handler) handleTxOuts(ct *claimtrie.ClaimTrie) error {
|
func (h *handler) handleTxOuts(ct *claimtrie.ClaimTrie) error {
|
||||||
for i, txOut := range h.tx.MsgTx().TxOut {
|
for i, txOut := range h.tx.MsgTx().TxOut {
|
||||||
op := *wire.NewOutPoint(h.tx.Hash(), uint32(i))
|
op := *wire.NewOutPoint(h.tx.Hash(), uint32(i))
|
||||||
cs, err := txscript.DecodeClaimScript(txOut.PkScript)
|
cs, closer, err := txscript.DecodeClaimScript(txOut.PkScript)
|
||||||
if err == txscript.ErrNotClaimScript {
|
if err == txscript.ErrNotClaimScript {
|
||||||
|
closer()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
closer()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,12 +132,14 @@ func (h *handler) handleTxOuts(ct *claimtrie.ClaimTrie) error {
|
||||||
if !bytes.Equal(h.spent[id.Key()], normName) {
|
if !bytes.Equal(h.spent[id.Key()], normName) {
|
||||||
node.LogOnce(fmt.Sprintf("Invalid update operation: name or ID mismatch at %d for: %s, %s",
|
node.LogOnce(fmt.Sprintf("Invalid update operation: name or ID mismatch at %d for: %s, %s",
|
||||||
ct.Height(), normName, id.String()))
|
ct.Height(), normName, id.String()))
|
||||||
|
closer()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(h.spent, id.Key())
|
delete(h.spent, id.Key())
|
||||||
err = ct.UpdateClaim(name, op, amt, id)
|
err = ct.UpdateClaim(name, op, amt, id)
|
||||||
}
|
}
|
||||||
|
closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "handleTxOuts")
|
return errors.Wrapf(err, "handleTxOuts")
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,7 @@ out:
|
||||||
txIn.PreviousOutPoint, err, witness,
|
txIn.PreviousOutPoint, err, witness,
|
||||||
sigScript, pkScript)
|
sigScript, pkScript)
|
||||||
err := ruleError(ErrScriptMalformed, str)
|
err := ruleError(ErrScriptMalformed, str)
|
||||||
|
vm.Close()
|
||||||
v.sendResult(err)
|
v.sendResult(err)
|
||||||
break out
|
break out
|
||||||
}
|
}
|
||||||
|
@ -100,11 +101,13 @@ out:
|
||||||
txIn.PreviousOutPoint, err, witness,
|
txIn.PreviousOutPoint, err, witness,
|
||||||
sigScript, pkScript)
|
sigScript, pkScript)
|
||||||
err := ruleError(ErrScriptValidation, str)
|
err := ruleError(ErrScriptValidation, str)
|
||||||
|
vm.Close()
|
||||||
v.sendResult(err)
|
v.sendResult(err)
|
||||||
break out
|
break out
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation succeeded.
|
// Validation succeeded.
|
||||||
|
vm.Close()
|
||||||
v.sendResult(nil)
|
v.sendResult(nil)
|
||||||
|
|
||||||
case <-v.quitChan:
|
case <-v.quitChan:
|
||||||
|
|
|
@ -343,8 +343,9 @@ func (cb *chainConverter) processBlock() {
|
||||||
for _, txIn := range tx.MsgTx().TxIn {
|
for _, txIn := range tx.MsgTx().TxIn {
|
||||||
prevOutpoint := txIn.PreviousOutPoint
|
prevOutpoint := txIn.PreviousOutPoint
|
||||||
pkScript := utxoPubScripts[prevOutpoint]
|
pkScript := utxoPubScripts[prevOutpoint]
|
||||||
cs, err := txscript.DecodeClaimScript(pkScript)
|
cs, closer, err := txscript.DecodeClaimScript(pkScript)
|
||||||
if err == txscript.ErrNotClaimScript {
|
if err == txscript.ErrNotClaimScript {
|
||||||
|
closer()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -371,12 +372,14 @@ func (cb *chainConverter) processBlock() {
|
||||||
}
|
}
|
||||||
|
|
||||||
changes = append(changes, chg)
|
changes = append(changes, chg)
|
||||||
|
closer()
|
||||||
}
|
}
|
||||||
|
|
||||||
op := *wire.NewOutPoint(tx.Hash(), 0)
|
op := *wire.NewOutPoint(tx.Hash(), 0)
|
||||||
for i, txOut := range tx.MsgTx().TxOut {
|
for i, txOut := range tx.MsgTx().TxOut {
|
||||||
cs, err := txscript.DecodeClaimScript(txOut.PkScript)
|
cs, closer, err := txscript.DecodeClaimScript(txOut.PkScript)
|
||||||
if err == txscript.ErrNotClaimScript {
|
if err == txscript.ErrNotClaimScript {
|
||||||
|
closer()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -401,6 +404,7 @@ func (cb *chainConverter) processBlock() {
|
||||||
copy(chg.ClaimID[:], cs.ClaimID())
|
copy(chg.ClaimID[:], cs.ClaimID())
|
||||||
}
|
}
|
||||||
changes = append(changes, chg)
|
changes = append(changes, chg)
|
||||||
|
closer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cb.stat.blocksProcessed++
|
cb.stat.blocksProcessed++
|
||||||
|
|
|
@ -323,7 +323,8 @@ func lookupValue(s *rpcServer, outpoint wire.OutPoint, includeValues *bool) (str
|
||||||
}
|
}
|
||||||
|
|
||||||
txo := msgTx.TxOut[outpoint.Index]
|
txo := msgTx.TxOut[outpoint.Index]
|
||||||
cs, err := txscript.DecodeClaimScript(txo.PkScript)
|
cs, closer, err := txscript.DecodeClaimScript(txo.PkScript)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
context := "Failed to decode the claim script"
|
context := "Failed to decode the claim script"
|
||||||
return "", "", internalRPCError(err.Error(), context)
|
return "", "", internalRPCError(err.Error(), context)
|
||||||
|
|
|
@ -136,6 +136,20 @@ type Engine struct {
|
||||||
witnessVersion int
|
witnessVersion int
|
||||||
witnessProgram []byte
|
witnessProgram []byte
|
||||||
inputAmount int64
|
inputAmount int64
|
||||||
|
cleanups []func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *Engine) Close() {
|
||||||
|
for i := range vm.cleanups {
|
||||||
|
vm.cleanups[i]()
|
||||||
|
}
|
||||||
|
vm.cleanups = nil
|
||||||
|
vm.scripts = nil
|
||||||
|
vm.savedFirstStack = nil
|
||||||
|
vm.witnessProgram = nil
|
||||||
|
vm.condStack = nil
|
||||||
|
vm.sigCache = nil
|
||||||
|
vm.hashCache = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasFlag returns whether the script engine instance has the passed flag set.
|
// hasFlag returns whether the script engine instance has the passed flag set.
|
||||||
|
@ -269,8 +283,9 @@ func (vm *Engine) verifyWitnessProgram(witness [][]byte) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pops, err := parseScript(pkScript)
|
pops, closer, err := parseScript(pkScript)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
closer()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,6 +293,7 @@ func (vm *Engine) verifyWitnessProgram(witness [][]byte) error {
|
||||||
// append the pkScript generated above as the next
|
// append the pkScript generated above as the next
|
||||||
// script to execute.
|
// script to execute.
|
||||||
vm.scripts = append(vm.scripts, pops)
|
vm.scripts = append(vm.scripts, pops)
|
||||||
|
vm.cleanups = append(vm.cleanups, closer)
|
||||||
vm.SetStack(witness)
|
vm.SetStack(witness)
|
||||||
|
|
||||||
case payToWitnessScriptHashDataSize: // P2WSH
|
case payToWitnessScriptHashDataSize: // P2WSH
|
||||||
|
@ -310,8 +326,9 @@ func (vm *Engine) verifyWitnessProgram(witness [][]byte) error {
|
||||||
// With all the validity checks passed, parse the
|
// With all the validity checks passed, parse the
|
||||||
// script into individual op-codes so w can execute it
|
// script into individual op-codes so w can execute it
|
||||||
// as the next script.
|
// as the next script.
|
||||||
pops, err := parseScript(witnessScript)
|
pops, closer, err := parseScript(witnessScript)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
closer()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,6 +336,7 @@ func (vm *Engine) verifyWitnessProgram(witness [][]byte) error {
|
||||||
// the stack, and set the witnessScript to be the next
|
// the stack, and set the witnessScript to be the next
|
||||||
// script executed.
|
// script executed.
|
||||||
vm.scripts = append(vm.scripts, pops)
|
vm.scripts = append(vm.scripts, pops)
|
||||||
|
vm.cleanups = append(vm.cleanups, closer)
|
||||||
vm.SetStack(witness[:len(witness)-1])
|
vm.SetStack(witness[:len(witness)-1])
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -492,11 +510,13 @@ func (vm *Engine) Step() (done bool, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
script := vm.savedFirstStack[len(vm.savedFirstStack)-1]
|
script := vm.savedFirstStack[len(vm.savedFirstStack)-1]
|
||||||
pops, err := parseScript(script)
|
pops, closer, err := parseScript(script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
closer()
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
vm.scripts = append(vm.scripts, pops)
|
vm.scripts = append(vm.scripts, pops)
|
||||||
|
vm.cleanups = append(vm.cleanups, closer)
|
||||||
|
|
||||||
// Set stack to be the stack from first script minus the
|
// Set stack to be the stack from first script minus the
|
||||||
// script itself
|
// script itself
|
||||||
|
@ -910,10 +930,13 @@ func NewEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags ScriptFlags
|
||||||
return nil, scriptError(ErrScriptTooBig, str)
|
return nil, scriptError(ErrScriptTooBig, str)
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
vm.scripts[i], err = parseScript(scr)
|
var closer func()
|
||||||
|
vm.scripts[i], closer, err = parseScript(scr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
closer()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
vm.cleanups = append(vm.cleanups, closer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance the program counter to the public key script if the signature
|
// Advance the program counter to the public key script if the signature
|
||||||
|
|
|
@ -55,27 +55,27 @@ func UpdateClaimScript(name string, claimID []byte, value string) ([]byte, error
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeClaimScript ...
|
// DecodeClaimScript ...
|
||||||
func DecodeClaimScript(script []byte) (*ClaimScript, error) {
|
func DecodeClaimScript(script []byte) (*ClaimScript, func(), error) {
|
||||||
if len(script) == 0 {
|
if len(script) == 0 {
|
||||||
return nil, ErrNotClaimScript
|
return nil, func() {}, ErrNotClaimScript
|
||||||
}
|
}
|
||||||
op := script[0]
|
op := script[0]
|
||||||
if op != OP_CLAIMNAME && op != OP_SUPPORTCLAIM && op != OP_UPDATECLAIM {
|
if op != OP_CLAIMNAME && op != OP_SUPPORTCLAIM && op != OP_UPDATECLAIM {
|
||||||
return nil, ErrNotClaimScript
|
return nil, func() {}, ErrNotClaimScript
|
||||||
}
|
}
|
||||||
pops, err := parseScript(script)
|
pops, closer, err := parseScript(script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, closer, err
|
||||||
}
|
}
|
||||||
if isClaimName(pops) || isSupportClaim(pops) || isUpdateClaim(pops) {
|
if isClaimName(pops) || isSupportClaim(pops) || isUpdateClaim(pops) {
|
||||||
cs := &ClaimScript{op: op, pops: pops}
|
cs := &ClaimScript{op: op, pops: pops}
|
||||||
if cs.Size() > MaxClaimScriptSize {
|
if cs.Size() > MaxClaimScriptSize {
|
||||||
log.Infof("claim script of %d bytes is larger than %d", cs.Size(), MaxClaimScriptSize)
|
log.Infof("claim script of %d bytes is larger than %d", cs.Size(), MaxClaimScriptSize)
|
||||||
return nil, ErrInvalidClaimScript
|
return nil, closer, ErrInvalidClaimScript
|
||||||
}
|
}
|
||||||
return cs, nil
|
return cs, closer, nil
|
||||||
}
|
}
|
||||||
return nil, ErrInvalidClaimScript
|
return nil, closer, ErrInvalidClaimScript
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClaimScript ...
|
// ClaimScript ...
|
||||||
|
@ -132,7 +132,8 @@ func (cs *ClaimScript) Size() int {
|
||||||
|
|
||||||
// StripClaimScriptPrefix ...
|
// StripClaimScriptPrefix ...
|
||||||
func StripClaimScriptPrefix(script []byte) []byte {
|
func StripClaimScriptPrefix(script []byte) []byte {
|
||||||
cs, err := DecodeClaimScript(script)
|
cs, closer, err := DecodeClaimScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return script
|
return script
|
||||||
}
|
}
|
||||||
|
@ -141,7 +142,8 @@ func StripClaimScriptPrefix(script []byte) []byte {
|
||||||
|
|
||||||
// claimNameSize returns size of the name in a claim script or 0 if script is not a claimtrie transaction.
|
// claimNameSize returns size of the name in a claim script or 0 if script is not a claimtrie transaction.
|
||||||
func claimNameSize(script []byte) int {
|
func claimNameSize(script []byte) int {
|
||||||
cs, err := DecodeClaimScript(script)
|
cs, closer, err := DecodeClaimScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@ -200,7 +202,8 @@ func isUpdateClaim(pops []parsedOpcode) bool {
|
||||||
const illegalChars = "=&#:*$@%?/;\\\b\n\t\r\x00"
|
const illegalChars = "=&#:*$@%?/;\\\b\n\t\r\x00"
|
||||||
|
|
||||||
func AllClaimsAreSane(script []byte, enforceSoftFork bool) error {
|
func AllClaimsAreSane(script []byte, enforceSoftFork bool) error {
|
||||||
cs, err := DecodeClaimScript(script)
|
cs, closer, err := DecodeClaimScript(script)
|
||||||
|
defer closer()
|
||||||
if err != ErrNotClaimScript {
|
if err != ErrNotClaimScript {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid claim script: %s", err.Error())
|
return fmt.Errorf("invalid claim script: %s", err.Error())
|
||||||
|
|
|
@ -12,12 +12,14 @@ func TestCreationParseLoopClaim(t *testing.T) {
|
||||||
|
|
||||||
claim, err := ClaimNameScript("tester", "value")
|
claim, err := ClaimNameScript("tester", "value")
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
parsed, err := parseScript(claim)
|
parsed, closer, err := parseScript(claim)
|
||||||
|
defer closer()
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
r.True(isClaimName(parsed))
|
r.True(isClaimName(parsed))
|
||||||
r.False(isSupportClaim(parsed))
|
r.False(isSupportClaim(parsed))
|
||||||
r.False(isUpdateClaim(parsed))
|
r.False(isUpdateClaim(parsed))
|
||||||
script, err := DecodeClaimScript(claim)
|
script, closer2, err := DecodeClaimScript(claim)
|
||||||
|
defer closer2()
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
r.Equal([]byte("tester"), script.Name())
|
r.Equal([]byte("tester"), script.Name())
|
||||||
r.Equal([]byte("value"), script.Value())
|
r.Equal([]byte("value"), script.Value())
|
||||||
|
@ -30,12 +32,14 @@ func TestCreationParseLoopUpdate(t *testing.T) {
|
||||||
claimID := []byte("12345123451234512345")
|
claimID := []byte("12345123451234512345")
|
||||||
claim, err := UpdateClaimScript("tester", claimID, "value")
|
claim, err := UpdateClaimScript("tester", claimID, "value")
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
parsed, err := parseScript(claim)
|
parsed, closer, err := parseScript(claim)
|
||||||
|
defer closer()
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
r.False(isSupportClaim(parsed))
|
r.False(isSupportClaim(parsed))
|
||||||
r.False(isClaimName(parsed))
|
r.False(isClaimName(parsed))
|
||||||
r.True(isUpdateClaim(parsed))
|
r.True(isUpdateClaim(parsed))
|
||||||
script, err := DecodeClaimScript(claim)
|
script, closer2, err := DecodeClaimScript(claim)
|
||||||
|
defer closer2()
|
||||||
|
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
r.Equal([]byte("tester"), script.Name())
|
r.Equal([]byte("tester"), script.Name())
|
||||||
|
@ -50,12 +54,15 @@ func TestCreationParseLoopSupport(t *testing.T) {
|
||||||
claimID := []byte("12345123451234512345")
|
claimID := []byte("12345123451234512345")
|
||||||
claim, err := SupportClaimScript("tester", claimID, []byte("value"))
|
claim, err := SupportClaimScript("tester", claimID, []byte("value"))
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
parsed, err := parseScript(claim)
|
parsed, closer, err := parseScript(claim)
|
||||||
|
defer closer()
|
||||||
|
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
r.True(isSupportClaim(parsed))
|
r.True(isSupportClaim(parsed))
|
||||||
r.False(isClaimName(parsed))
|
r.False(isClaimName(parsed))
|
||||||
r.False(isUpdateClaim(parsed))
|
r.False(isUpdateClaim(parsed))
|
||||||
script, err := DecodeClaimScript(claim)
|
script, closer2, err := DecodeClaimScript(claim)
|
||||||
|
defer closer2()
|
||||||
|
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
r.Equal([]byte("tester"), script.Name())
|
r.Equal([]byte("tester"), script.Name())
|
||||||
|
@ -64,7 +71,8 @@ func TestCreationParseLoopSupport(t *testing.T) {
|
||||||
|
|
||||||
claim, err = SupportClaimScript("tester", claimID, nil)
|
claim, err = SupportClaimScript("tester", claimID, nil)
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
script, err = DecodeClaimScript(claim)
|
script, closer, err = DecodeClaimScript(claim)
|
||||||
|
defer closer()
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
|
|
||||||
r.Equal([]byte("tester"), script.Name())
|
r.Equal([]byte("tester"), script.Name())
|
||||||
|
|
|
@ -211,7 +211,8 @@ func computeNonWitnessPkScript(sigScript []byte) (PkScript, error) {
|
||||||
// The redeem script will always be the last data push of the
|
// The redeem script will always be the last data push of the
|
||||||
// signature script, so we'll parse the script into opcodes to
|
// signature script, so we'll parse the script into opcodes to
|
||||||
// obtain it.
|
// obtain it.
|
||||||
parsedOpcodes, err := parseScript(sigScript)
|
parsedOpcodes, closer, err := parseScript(sigScript)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PkScript{}, err
|
return PkScript{}, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
@ -63,7 +64,8 @@ func isScriptHash(pops []parsedOpcode) bool {
|
||||||
// IsPayToScriptHash returns true if the script is in the standard
|
// IsPayToScriptHash returns true if the script is in the standard
|
||||||
// pay-to-script-hash (P2SH) format, false otherwise.
|
// pay-to-script-hash (P2SH) format, false otherwise.
|
||||||
func IsPayToScriptHash(script []byte) bool {
|
func IsPayToScriptHash(script []byte) bool {
|
||||||
pops, err := parseScript(script)
|
pops, closer, err := parseScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -81,7 +83,8 @@ func isWitnessScriptHash(pops []parsedOpcode) bool {
|
||||||
// IsPayToWitnessScriptHash returns true if the is in the standard
|
// IsPayToWitnessScriptHash returns true if the is in the standard
|
||||||
// pay-to-witness-script-hash (P2WSH) format, false otherwise.
|
// pay-to-witness-script-hash (P2WSH) format, false otherwise.
|
||||||
func IsPayToWitnessScriptHash(script []byte) bool {
|
func IsPayToWitnessScriptHash(script []byte) bool {
|
||||||
pops, err := parseScript(script)
|
pops, closer, err := parseScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -91,7 +94,8 @@ func IsPayToWitnessScriptHash(script []byte) bool {
|
||||||
// IsPayToWitnessPubKeyHash returns true if the is in the standard
|
// IsPayToWitnessPubKeyHash returns true if the is in the standard
|
||||||
// pay-to-witness-pubkey-hash (P2WKH) format, false otherwise.
|
// pay-to-witness-pubkey-hash (P2WKH) format, false otherwise.
|
||||||
func IsPayToWitnessPubKeyHash(script []byte) bool {
|
func IsPayToWitnessPubKeyHash(script []byte) bool {
|
||||||
pops, err := parseScript(script)
|
pops, closer, err := parseScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -119,7 +123,8 @@ func IsWitnessProgram(script []byte) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
pops, err := parseScript(script)
|
pops, closer, err := parseScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -143,7 +148,8 @@ func isWitnessProgram(pops []parsedOpcode) bool {
|
||||||
// ExtractWitnessProgramInfo attempts to extract the witness program version,
|
// ExtractWitnessProgramInfo attempts to extract the witness program version,
|
||||||
// as well as the witness program itself from the passed script.
|
// as well as the witness program itself from the passed script.
|
||||||
func ExtractWitnessProgramInfo(script []byte) (int, []byte, error) {
|
func ExtractWitnessProgramInfo(script []byte) (int, []byte, error) {
|
||||||
pops, err := parseScript(script)
|
pops, closer, err := parseScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
@ -184,18 +190,25 @@ func isPushOnly(pops []parsedOpcode) bool {
|
||||||
//
|
//
|
||||||
// False will be returned when the script does not parse.
|
// False will be returned when the script does not parse.
|
||||||
func IsPushOnlyScript(script []byte) bool {
|
func IsPushOnlyScript(script []byte) bool {
|
||||||
pops, err := parseScript(script)
|
pops, closer, err := parseScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return isPushOnly(pops)
|
return isPushOnly(pops)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var parsedPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return make([]parsedOpcode, 0, 8)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// parseScriptTemplate is the same as parseScript but allows the passing of the
|
// parseScriptTemplate is the same as parseScript but allows the passing of the
|
||||||
// template list for testing purposes. When there are parse errors, it returns
|
// template list for testing purposes. When there are parse errors, it returns
|
||||||
// the list of parsed opcodes up to the point of failure along with the error.
|
// the list of parsed opcodes up to the point of failure along with the error.
|
||||||
func parseScriptTemplate(script []byte, opcodes *[256]opcode) ([]parsedOpcode, error) {
|
func parseScriptTemplate(script []byte, opcodes *[256]opcode) ([]parsedOpcode, func(), error) {
|
||||||
retScript := make([]parsedOpcode, 0, len(script))
|
retScript := parsedPool.Get().([]parsedOpcode)
|
||||||
var err error
|
var err error
|
||||||
for i := 0; i < len(script); {
|
for i := 0; i < len(script); {
|
||||||
instr := script[i]
|
instr := script[i]
|
||||||
|
@ -203,13 +216,19 @@ func parseScriptTemplate(script []byte, opcodes *[256]opcode) ([]parsedOpcode, e
|
||||||
pop := parsedOpcode{opcode: op}
|
pop := parsedOpcode{opcode: op}
|
||||||
i, err = pop.checkParseableInScript(script, i)
|
i, err = pop.checkParseableInScript(script, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return retScript, err
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
retScript = append(retScript, pop)
|
retScript = append(retScript, pop)
|
||||||
}
|
}
|
||||||
|
|
||||||
return retScript, nil
|
return retScript, func() {
|
||||||
|
for i := range retScript {
|
||||||
|
retScript[i].data = nil
|
||||||
|
retScript[i].opcode = nil
|
||||||
|
}
|
||||||
|
parsedPool.Put(retScript[:0])
|
||||||
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkScriptTemplateParseable is the same as parseScriptTemplate but does not
|
// checkScriptTemplateParseable is the same as parseScriptTemplate but does not
|
||||||
|
@ -252,7 +271,7 @@ func checkScriptTemplateParseable(script []byte, opcodes *[256]opcode) (*byte, e
|
||||||
|
|
||||||
// parseScript preparses the script in bytes into a list of parsedOpcodes while
|
// parseScript preparses the script in bytes into a list of parsedOpcodes while
|
||||||
// applying a number of sanity checks.
|
// applying a number of sanity checks.
|
||||||
func parseScript(script []byte) ([]parsedOpcode, error) {
|
func parseScript(script []byte) ([]parsedOpcode, func(), error) {
|
||||||
return parseScriptTemplate(script, &opcodeArray)
|
return parseScriptTemplate(script, &opcodeArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,7 +296,8 @@ func unparseScript(pops []parsedOpcode) ([]byte, error) {
|
||||||
// if the caller wants more information about the failure.
|
// if the caller wants more information about the failure.
|
||||||
func DisasmString(buf []byte) (string, error) {
|
func DisasmString(buf []byte) (string, error) {
|
||||||
var disbuf bytes.Buffer
|
var disbuf bytes.Buffer
|
||||||
opcodes, err := parseScript(buf)
|
opcodes, closer, err := parseScript(buf)
|
||||||
|
defer closer()
|
||||||
for _, pop := range opcodes {
|
for _, pop := range opcodes {
|
||||||
disbuf.WriteString(pop.print(true))
|
disbuf.WriteString(pop.print(true))
|
||||||
disbuf.WriteByte(' ')
|
disbuf.WriteByte(' ')
|
||||||
|
@ -517,7 +537,8 @@ func calcWitnessSignatureHash(subScript []parsedOpcode, sigHashes *TxSigHashes,
|
||||||
func CalcWitnessSigHash(script []byte, sigHashes *TxSigHashes, hType SigHashType,
|
func CalcWitnessSigHash(script []byte, sigHashes *TxSigHashes, hType SigHashType,
|
||||||
tx *wire.MsgTx, idx int, amt int64) ([]byte, error) {
|
tx *wire.MsgTx, idx int, amt int64) ([]byte, error) {
|
||||||
|
|
||||||
parsedScript, err := parseScript(script)
|
parsedScript, closer, err := parseScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot parse output script: %v", err)
|
return nil, fmt.Errorf("cannot parse output script: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -558,7 +579,8 @@ func shallowCopyTx(tx *wire.MsgTx) wire.MsgTx {
|
||||||
// engine instance, calculate the signature hash to be used for signing and
|
// engine instance, calculate the signature hash to be used for signing and
|
||||||
// verification.
|
// verification.
|
||||||
func CalcSignatureHash(script []byte, hashType SigHashType, tx *wire.MsgTx, idx int) ([]byte, error) {
|
func CalcSignatureHash(script []byte, hashType SigHashType, tx *wire.MsgTx, idx int) ([]byte, error) {
|
||||||
parsedScript, err := parseScript(script)
|
parsedScript, closer, err := parseScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot parse output script: %v", err)
|
return nil, fmt.Errorf("cannot parse output script: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -711,7 +733,8 @@ func getSigOpCount(pops []parsedOpcode, precise bool) int {
|
||||||
func GetSigOpCount(script []byte) int {
|
func GetSigOpCount(script []byte) int {
|
||||||
// Don't check error since parseScript returns the parsed-up-to-error
|
// Don't check error since parseScript returns the parsed-up-to-error
|
||||||
// list of pops.
|
// list of pops.
|
||||||
pops, _ := parseScript(script)
|
pops, closer, _ := parseScript(script)
|
||||||
|
defer closer()
|
||||||
return getSigOpCount(pops, false)
|
return getSigOpCount(pops, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -723,7 +746,8 @@ func GetSigOpCount(script []byte) int {
|
||||||
func GetPreciseSigOpCount(scriptSig, scriptPubKey []byte, bip16 bool) int {
|
func GetPreciseSigOpCount(scriptSig, scriptPubKey []byte, bip16 bool) int {
|
||||||
// Don't check error since parseScript returns the parsed-up-to-error
|
// Don't check error since parseScript returns the parsed-up-to-error
|
||||||
// list of pops.
|
// list of pops.
|
||||||
pops, _ := parseScript(scriptPubKey)
|
pops, closer1, _ := parseScript(scriptPubKey)
|
||||||
|
defer closer1()
|
||||||
|
|
||||||
// Treat non P2SH transactions as normal.
|
// Treat non P2SH transactions as normal.
|
||||||
if !(bip16 && isScriptHash(pops)) {
|
if !(bip16 && isScriptHash(pops)) {
|
||||||
|
@ -733,7 +757,9 @@ func GetPreciseSigOpCount(scriptSig, scriptPubKey []byte, bip16 bool) int {
|
||||||
// The public key script is a pay-to-script-hash, so parse the signature
|
// The public key script is a pay-to-script-hash, so parse the signature
|
||||||
// script to get the final item. Scripts that fail to fully parse count
|
// script to get the final item. Scripts that fail to fully parse count
|
||||||
// as 0 signature operations.
|
// as 0 signature operations.
|
||||||
sigPops, err := parseScript(scriptSig)
|
sigPops, closer2, err := parseScript(scriptSig)
|
||||||
|
defer closer2()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@ -756,7 +782,9 @@ func GetPreciseSigOpCount(scriptSig, scriptPubKey []byte, bip16 bool) int {
|
||||||
// returns the parsed-up-to-error list of pops and the consensus rules
|
// returns the parsed-up-to-error list of pops and the consensus rules
|
||||||
// dictate signature operations are counted up to the first parse
|
// dictate signature operations are counted up to the first parse
|
||||||
// failure.
|
// failure.
|
||||||
shPops, _ := parseScript(shScript)
|
shPops, closer3, _ := parseScript(shScript)
|
||||||
|
defer closer3()
|
||||||
|
|
||||||
return getSigOpCount(shPops, true)
|
return getSigOpCount(shPops, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -776,7 +804,8 @@ func GetWitnessSigOpCount(sigScript, pkScript []byte, witness wire.TxWitness) in
|
||||||
// Next, we'll check the sigScript to see if this is a nested p2sh
|
// Next, we'll check the sigScript to see if this is a nested p2sh
|
||||||
// witness program. This is a case wherein the sigScript is actually a
|
// witness program. This is a case wherein the sigScript is actually a
|
||||||
// datapush of a p2wsh witness program.
|
// datapush of a p2wsh witness program.
|
||||||
sigPops, err := parseScript(sigScript)
|
sigPops, closer, err := parseScript(sigScript)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@ -811,7 +840,8 @@ func getWitnessSigOps(pkScript []byte, witness wire.TxWitness) int {
|
||||||
len(witness) > 0:
|
len(witness) > 0:
|
||||||
|
|
||||||
witnessScript := witness[len(witness)-1]
|
witnessScript := witness[len(witness)-1]
|
||||||
pops, _ := parseScript(witnessScript)
|
pops, closer, _ := parseScript(witnessScript)
|
||||||
|
defer closer()
|
||||||
return getSigOpCount(pops, true)
|
return getSigOpCount(pops, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,8 @@ func RawTxInWitnessSignature(tx *wire.MsgTx, sigHashes *TxSigHashes, idx int,
|
||||||
amt int64, subScript []byte, hashType SigHashType,
|
amt int64, subScript []byte, hashType SigHashType,
|
||||||
key *btcec.PrivateKey) ([]byte, error) {
|
key *btcec.PrivateKey) ([]byte, error) {
|
||||||
|
|
||||||
parsedScript, err := parseScript(subScript)
|
parsedScript, closer, err := parseScript(subScript)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot parse output script: %v", err)
|
return nil, fmt.Errorf("cannot parse output script: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -232,11 +233,13 @@ func mergeScripts(chainParams *chaincfg.Params, tx *wire.MsgTx, idx int,
|
||||||
case ScriptHashTy:
|
case ScriptHashTy:
|
||||||
// Remove the last push in the script and then recurse.
|
// Remove the last push in the script and then recurse.
|
||||||
// this could be a lot less inefficient.
|
// this could be a lot less inefficient.
|
||||||
sigPops, err := parseScript(sigScript)
|
sigPops, closer1, err := parseScript(sigScript)
|
||||||
|
defer closer1()
|
||||||
if err != nil || len(sigPops) == 0 {
|
if err != nil || len(sigPops) == 0 {
|
||||||
return prevScript
|
return prevScript
|
||||||
}
|
}
|
||||||
prevPops, err := parseScript(prevScript)
|
prevPops, closer2, err := parseScript(prevScript)
|
||||||
|
defer closer2()
|
||||||
if err != nil || len(prevPops) == 0 {
|
if err != nil || len(prevPops) == 0 {
|
||||||
return sigScript
|
return sigScript
|
||||||
}
|
}
|
||||||
|
@ -293,14 +296,17 @@ func mergeMultiSig(tx *wire.MsgTx, idx int, addresses []btcutil.Address,
|
||||||
// This is an internal only function and we already parsed this script
|
// This is an internal only function and we already parsed this script
|
||||||
// as ok for multisig (this is how we got here), so if this fails then
|
// as ok for multisig (this is how we got here), so if this fails then
|
||||||
// all assumptions are broken and who knows which way is up?
|
// all assumptions are broken and who knows which way is up?
|
||||||
pkPops, _ := parseScript(pkScript)
|
pkPops, closer1, _ := parseScript(pkScript)
|
||||||
|
defer closer1()
|
||||||
|
|
||||||
sigPops, err := parseScript(sigScript)
|
sigPops, closer2, err := parseScript(sigScript)
|
||||||
|
defer closer2()
|
||||||
if err != nil || len(sigPops) == 0 {
|
if err != nil || len(sigPops) == 0 {
|
||||||
return prevScript
|
return prevScript
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPops, err := parseScript(prevScript)
|
prevPops, closer3, err := parseScript(prevScript)
|
||||||
|
defer closer3()
|
||||||
if err != nil || len(prevPops) == 0 {
|
if err != nil || len(prevPops) == 0 {
|
||||||
return sigScript
|
return sigScript
|
||||||
}
|
}
|
||||||
|
|
|
@ -183,7 +183,8 @@ func typeOfScript(pops []parsedOpcode) ScriptClass {
|
||||||
//
|
//
|
||||||
// NonStandardTy will be returned when the script does not parse.
|
// NonStandardTy will be returned when the script does not parse.
|
||||||
func GetScriptClass(script []byte) ScriptClass {
|
func GetScriptClass(script []byte) ScriptClass {
|
||||||
pops, err := parseScript(script)
|
pops, closer, err := parseScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return NonStandardTy
|
return NonStandardTy
|
||||||
}
|
}
|
||||||
|
@ -273,12 +274,14 @@ type ScriptInfo struct {
|
||||||
func CalcScriptInfo(sigScript, pkScript []byte, witness wire.TxWitness,
|
func CalcScriptInfo(sigScript, pkScript []byte, witness wire.TxWitness,
|
||||||
bip16, segwit bool) (*ScriptInfo, error) {
|
bip16, segwit bool) (*ScriptInfo, error) {
|
||||||
|
|
||||||
sigPops, err := parseScript(sigScript)
|
sigPops, closer1, err := parseScript(sigScript)
|
||||||
|
defer closer1()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pkPops, err := parseScript(pkScript)
|
pkPops, closer2, err := parseScript(pkScript)
|
||||||
|
defer closer2()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -301,7 +304,8 @@ func CalcScriptInfo(sigScript, pkScript []byte, witness wire.TxWitness,
|
||||||
// The pay-to-hash-script is the final data push of the
|
// The pay-to-hash-script is the final data push of the
|
||||||
// signature script.
|
// signature script.
|
||||||
script := sigPops[len(sigPops)-1].data
|
script := sigPops[len(sigPops)-1].data
|
||||||
shPops, err := parseScript(script)
|
shPops, closer3, err := parseScript(script)
|
||||||
|
defer closer3()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -332,7 +336,8 @@ func CalcScriptInfo(sigScript, pkScript []byte, witness wire.TxWitness,
|
||||||
|
|
||||||
// Extract the pushed witness program from the sigScript so we
|
// Extract the pushed witness program from the sigScript so we
|
||||||
// can determine the number of expected inputs.
|
// can determine the number of expected inputs.
|
||||||
pkPops, _ := parseScript(sigScript[1:])
|
pkPops, closer4, _ := parseScript(sigScript[1:])
|
||||||
|
defer closer4()
|
||||||
shInputs := expectedInputs(pkPops, typeOfScript(pkPops))
|
shInputs := expectedInputs(pkPops, typeOfScript(pkPops))
|
||||||
if shInputs == -1 {
|
if shInputs == -1 {
|
||||||
si.ExpectedInputs = -1
|
si.ExpectedInputs = -1
|
||||||
|
@ -351,7 +356,8 @@ func CalcScriptInfo(sigScript, pkScript []byte, witness wire.TxWitness,
|
||||||
// The witness script is the final element of the witness
|
// The witness script is the final element of the witness
|
||||||
// stack.
|
// stack.
|
||||||
witnessScript := witness[len(witness)-1]
|
witnessScript := witness[len(witness)-1]
|
||||||
pops, _ := parseScript(witnessScript)
|
pops, closer5, _ := parseScript(witnessScript)
|
||||||
|
defer closer5()
|
||||||
|
|
||||||
shInputs := expectedInputs(pops, typeOfScript(pops))
|
shInputs := expectedInputs(pops, typeOfScript(pops))
|
||||||
if shInputs == -1 {
|
if shInputs == -1 {
|
||||||
|
@ -378,7 +384,8 @@ func CalcScriptInfo(sigScript, pkScript []byte, witness wire.TxWitness,
|
||||||
// a multi-signature transaction script. The passed script MUST already be
|
// a multi-signature transaction script. The passed script MUST already be
|
||||||
// known to be a multi-signature script.
|
// known to be a multi-signature script.
|
||||||
func CalcMultiSigStats(script []byte) (int, int, error) {
|
func CalcMultiSigStats(script []byte) (int, int, error) {
|
||||||
pops, err := parseScript(script)
|
pops, closer, err := parseScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
@ -519,7 +526,8 @@ func MultiSigScript(pubkeys []*btcutil.AddressPubKey, nrequired int) ([]byte, er
|
||||||
// PushedData returns an array of byte slices containing any pushed data found
|
// PushedData returns an array of byte slices containing any pushed data found
|
||||||
// in the passed script. This includes OP_0, but not OP_1 - OP_16.
|
// in the passed script. This includes OP_0, but not OP_1 - OP_16.
|
||||||
func PushedData(script []byte) ([][]byte, error) {
|
func PushedData(script []byte) ([][]byte, error) {
|
||||||
pops, err := parseScript(script)
|
pops, closer, err := parseScript(script)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -547,7 +555,8 @@ func ExtractPkScriptAddrs(pkScript []byte, chainParams *chaincfg.Params) (Script
|
||||||
|
|
||||||
// No valid addresses or required signatures if the script doesn't
|
// No valid addresses or required signatures if the script doesn't
|
||||||
// parse.
|
// parse.
|
||||||
pops, err := parseScript(stripped)
|
pops, closer, err := parseScript(stripped)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return NonStandardTy, nil, 0, err
|
return NonStandardTy, nil, 0, err
|
||||||
}
|
}
|
||||||
|
@ -669,7 +678,8 @@ type AtomicSwapDataPushes struct {
|
||||||
// This function is only defined in the txscript package due to API limitations
|
// This function is only defined in the txscript package due to API limitations
|
||||||
// which prevent callers using txscript to parse nonstandard scripts.
|
// which prevent callers using txscript to parse nonstandard scripts.
|
||||||
func ExtractAtomicSwapDataPushes(version uint16, pkScript []byte) (*AtomicSwapDataPushes, error) {
|
func ExtractAtomicSwapDataPushes(version uint16, pkScript []byte) (*AtomicSwapDataPushes, error) {
|
||||||
pops, err := parseScript(pkScript)
|
pops, closer, err := parseScript(pkScript)
|
||||||
|
defer closer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue