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
This commit is contained in:
parent
7bbd9b0284
commit
77fd96753c
3 changed files with 156 additions and 72 deletions
|
@ -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
|
// 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
|
// over by the program counter even if in a non-executed branch (it isn't a
|
||||||
// coincidence that they are conditionals).
|
// coincidence that they are conditionals).
|
||||||
|
|
|
@ -196,80 +196,14 @@ func IsPushOnlyScript(script []byte) bool {
|
||||||
// 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, error) {
|
||||||
retScript := make([]parsedOpcode, 0, len(script))
|
retScript := make([]parsedOpcode, 0, len(script))
|
||||||
|
var err error
|
||||||
for i := 0; i < len(script); {
|
for i := 0; i < len(script); {
|
||||||
instr := script[i]
|
instr := script[i]
|
||||||
op := &opcodes[instr]
|
op := &opcodes[instr]
|
||||||
pop := parsedOpcode{opcode: op}
|
pop := parsedOpcode{opcode: op}
|
||||||
|
i, err = pop.checkParseableInScript(script, i)
|
||||||
// Parse data out of instruction.
|
if err != nil {
|
||||||
switch {
|
return retScript, err
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
retScript = append(retScript, pop)
|
retScript = append(retScript, pop)
|
||||||
|
@ -278,6 +212,44 @@ func parseScriptTemplate(script []byte, opcodes *[256]opcode) ([]parsedOpcode, e
|
||||||
return retScript, nil
|
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
|
// 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, 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
|
// guaranteed to fail at execution. This allows inputs to be pruned instantly
|
||||||
// when entering the UTXO set.
|
// when entering the UTXO set.
|
||||||
func IsUnspendable(pkScript []byte) bool {
|
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 {
|
if err != nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return len(pops) > 0 && pops[0].opcode.value == OP_RETURN
|
return firstOpcode != nil && *firstOpcode == OP_RETURN
|
||||||
}
|
}
|
||||||
|
|
|
@ -4301,6 +4301,28 @@ func TestIsUnspendable(t *testing.T) {
|
||||||
0xfa, 0x0b, 0x5c, 0x88, 0xac},
|
0xfa, 0x0b, 0x5c, 0x88, 0xac},
|
||||||
expected: false,
|
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 {
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue