From 77fd96753c8571937663bc913ddc0c938fc4f05b Mon Sep 17 00:00:00 2001 From: Dan Cline Date: Fri, 14 Aug 2020 18:38:55 -0400 Subject: [PATCH] txscript: add benchmark for IsUnspendable - create benchmarks to measure allocations - add test for benchmark input - create a low alloc parseScriptTemplate - refactor parsing logic for a single opcode --- txscript/opcode.go | 73 ++++++++++++++++++++++++ txscript/script.go | 120 ++++++++++++++++------------------------ txscript/script_test.go | 35 ++++++++++++ 3 files changed, 156 insertions(+), 72 deletions(-) diff --git a/txscript/opcode.go b/txscript/opcode.go index 5ffb3982..a878a966 100644 --- a/txscript/opcode.go +++ b/txscript/opcode.go @@ -656,6 +656,79 @@ func (pop *parsedOpcode) isDisabled() bool { } } +// checkParseableInScript checks whether or not the current opcode is able to be +// parsed at a certain position in a script. +// This returns the position of the next opcode to be parsed in the script. +func (pop *parsedOpcode) checkParseableInScript(script []byte, scriptPos int) (int, error) { + // Parse data out of instruction. + switch { + // No additional data. Note that some of the opcodes, notably + // OP_1NEGATE, OP_0, and OP_[1-16] represent the data + // themselves. + case pop.opcode.length == 1: + scriptPos++ + + // Data pushes of specific lengths -- OP_DATA_[1-75]. + case pop.opcode.length > 1: + if len(script[scriptPos:]) < pop.opcode.length { + str := fmt.Sprintf("opcode %s requires %d "+ + "bytes, but script only has %d remaining", + pop.opcode.name, pop.opcode.length, len(script[scriptPos:])) + return 0, scriptError(ErrMalformedPush, str) + } + + // Slice out the data. + pop.data = script[scriptPos+1 : scriptPos+pop.opcode.length] + scriptPos += pop.opcode.length + + // Data pushes with parsed lengths -- OP_PUSHDATAP{1,2,4}. + case pop.opcode.length < 0: + var l uint + off := scriptPos + 1 + + if len(script[off:]) < -pop.opcode.length { + str := fmt.Sprintf("opcode %s requires %d "+ + "bytes, but script only has %d remaining", + pop.opcode.name, -pop.opcode.length, len(script[off:])) + return 0, scriptError(ErrMalformedPush, str) + } + + // Next -length bytes are little endian length of data. + switch pop.opcode.length { + case -1: + l = uint(script[off]) + case -2: + l = ((uint(script[off+1]) << 8) | + uint(script[off])) + case -4: + l = ((uint(script[off+3]) << 24) | + (uint(script[off+2]) << 16) | + (uint(script[off+1]) << 8) | + uint(script[off])) + default: + str := fmt.Sprintf("invalid opcode length %d", + pop.opcode.length) + return 0, scriptError(ErrMalformedPush, str) + } + + // Move offset to beginning of the data. + off += -pop.opcode.length + + // Disallow entries that do not fit script or were + // sign extended. + if int(l) > len(script[off:]) || int(l) < 0 { + str := fmt.Sprintf("opcode %s pushes %d bytes, "+ + "but script only has %d remaining", + pop.opcode.name, int(l), len(script[off:])) + return 0, scriptError(ErrMalformedPush, str) + } + + pop.data = script[off : off+int(l)] + scriptPos += 1 - pop.opcode.length + int(l) + } + return scriptPos, nil +} + // alwaysIllegal returns whether or not the opcode is always illegal when passed // over by the program counter even if in a non-executed branch (it isn't a // coincidence that they are conditionals). diff --git a/txscript/script.go b/txscript/script.go index aac3d4aa..92a50e37 100644 --- a/txscript/script.go +++ b/txscript/script.go @@ -196,80 +196,14 @@ func IsPushOnlyScript(script []byte) bool { // the list of parsed opcodes up to the point of failure along with the error. func parseScriptTemplate(script []byte, opcodes *[256]opcode) ([]parsedOpcode, error) { retScript := make([]parsedOpcode, 0, len(script)) + var err error for i := 0; i < len(script); { instr := script[i] op := &opcodes[instr] pop := parsedOpcode{opcode: op} - - // Parse data out of instruction. - switch { - // No additional data. Note that some of the opcodes, notably - // OP_1NEGATE, OP_0, and OP_[1-16] represent the data - // themselves. - case op.length == 1: - i++ - - // Data pushes of specific lengths -- OP_DATA_[1-75]. - case op.length > 1: - if len(script[i:]) < op.length { - str := fmt.Sprintf("opcode %s requires %d "+ - "bytes, but script only has %d remaining", - op.name, op.length, len(script[i:])) - return retScript, scriptError(ErrMalformedPush, - str) - } - - // Slice out the data. - pop.data = script[i+1 : i+op.length] - i += op.length - - // Data pushes with parsed lengths -- OP_PUSHDATAP{1,2,4}. - case op.length < 0: - var l uint - off := i + 1 - - if len(script[off:]) < -op.length { - str := fmt.Sprintf("opcode %s requires %d "+ - "bytes, but script only has %d remaining", - op.name, -op.length, len(script[off:])) - return retScript, scriptError(ErrMalformedPush, - str) - } - - // Next -length bytes are little endian length of data. - switch op.length { - case -1: - l = uint(script[off]) - case -2: - l = ((uint(script[off+1]) << 8) | - uint(script[off])) - case -4: - l = ((uint(script[off+3]) << 24) | - (uint(script[off+2]) << 16) | - (uint(script[off+1]) << 8) | - uint(script[off])) - default: - str := fmt.Sprintf("invalid opcode length %d", - op.length) - return retScript, scriptError(ErrMalformedPush, - str) - } - - // Move offset to beginning of the data. - off += -op.length - - // Disallow entries that do not fit script or were - // sign extended. - if int(l) > len(script[off:]) || int(l) < 0 { - str := fmt.Sprintf("opcode %s pushes %d bytes, "+ - "but script only has %d remaining", - op.name, int(l), len(script[off:])) - return retScript, scriptError(ErrMalformedPush, - str) - } - - pop.data = script[off : off+int(l)] - i += 1 - op.length + int(l) + i, err = pop.checkParseableInScript(script, i) + if err != nil { + return retScript, err } retScript = append(retScript, pop) @@ -278,6 +212,44 @@ func parseScriptTemplate(script []byte, opcodes *[256]opcode) ([]parsedOpcode, e return retScript, nil } +// checkScriptTemplateParseable is the same as parseScriptTemplate but does not +// return the list of opcodes up until the point of failure so that this can be +// used in functions which do not necessarily have a need for the failed list of +// opcodes, such as IsUnspendable. +// +// This function returns a pointer to a byte. This byte is nil if the parsing +// has an error, or if the script length is zero. If the script length is not +// zero and parsing succeeds, then the first opcode parsed will be returned. +// +// Not returning the full opcode list up until failure also has the benefit of +// reducing GC pressure, as the list would get immediately thrown away. +func checkScriptTemplateParseable(script []byte, opcodes *[256]opcode) (*byte, error) { + var err error + + // A script of length zero is an unspendable script but it is parseable. + var firstOpcode byte + var numParsedInstr uint = 0 + + for i := 0; i < len(script); { + instr := script[i] + op := &opcodes[instr] + pop := parsedOpcode{opcode: op} + i, err = pop.checkParseableInScript(script, i) + if err != nil { + return nil, err + } + + // if this is a op_return then it is unspendable so we set the first + // parsed instruction in case it's an op_return + if numParsedInstr == 0 { + firstOpcode = pop.opcode.value + } + numParsedInstr++ + } + + return &firstOpcode, nil +} + // parseScript preparses the script in bytes into a list of parsedOpcodes while // applying a number of sanity checks. func parseScript(script []byte) ([]parsedOpcode, error) { @@ -851,10 +823,14 @@ func getWitnessSigOps(pkScript []byte, witness wire.TxWitness) int { // guaranteed to fail at execution. This allows inputs to be pruned instantly // when entering the UTXO set. func IsUnspendable(pkScript []byte) bool { - pops, err := parseScript(pkScript) + // Not provably unspendable + if len(pkScript) == 0 { + return false + } + firstOpcode, err := checkScriptTemplateParseable(pkScript, &opcodeArray) if err != nil { return true } - return len(pops) > 0 && pops[0].opcode.value == OP_RETURN + return firstOpcode != nil && *firstOpcode == OP_RETURN } diff --git a/txscript/script_test.go b/txscript/script_test.go index 6a725e27..34c8ef97 100644 --- a/txscript/script_test.go +++ b/txscript/script_test.go @@ -4301,6 +4301,28 @@ func TestIsUnspendable(t *testing.T) { 0xfa, 0x0b, 0x5c, 0x88, 0xac}, expected: false, }, + { + // Spendable + pkScript: []byte{0xa9, 0x14, 0x82, 0x1d, 0xba, 0x94, 0xbc, 0xfb, + 0xa2, 0x57, 0x36, 0xa3, 0x9e, 0x5d, 0x14, 0x5d, 0x69, 0x75, + 0xba, 0x8c, 0x0b, 0x42, 0x87}, + expected: false, + }, + { + // Not Necessarily Unspendable + pkScript: []byte{}, + expected: false, + }, + { + // Spendable + pkScript: []byte{OP_TRUE}, + expected: false, + }, + { + // Unspendable + pkScript: []byte{OP_RETURN}, + expected: true, + }, } for i, test := range tests { @@ -4312,3 +4334,16 @@ func TestIsUnspendable(t *testing.T) { } } } + +// BenchmarkIsUnspendable adds a benchmark to compare the time and allocations +// necessary for the IsUnspendable function. +func BenchmarkIsUnspendable(b *testing.B) { + pkScriptToUse := []byte{0xa9, 0x14, 0x82, 0x1d, 0xba, 0x94, 0xbc, 0xfb, 0xa2, 0x57, 0x36, 0xa3, 0x9e, 0x5d, 0x14, 0x5d, 0x69, 0x75, 0xba, 0x8c, 0x0b, 0x42, 0x87} + var res bool = false + for i := 0; i < b.N; i++ { + res = IsUnspendable(pkScriptToUse) + } + if res { + b.Fatalf("Benchmark should never have res be %t\n", res) + } +}