This adds the ability for the short form script parsing to handle different script versions and updates all of the tests to use the version 0 function appropriately.
229 lines
7.4 KiB
Go
229 lines
7.4 KiB
Go
// Copyright (c) 2013-2017 The btcsuite developers
|
|
// Copyright (c) 2015-2021 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package txscript
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
// tokenRE is a regular expression used to parse tokens from short form
|
|
// scripts. It splits on repeated tokens and spaces. Repeated tokens are
|
|
// denoted by being wrapped in angular brackets followed by a suffix which
|
|
// consists of a number inside braces.
|
|
tokenRE = regexp.MustCompile(`\<.+?\>\{[0-9]+\}|[^\s]+`)
|
|
|
|
// repTokenRE is a regular expression used to parse short form scripts for a
|
|
// series of tokens repeated a specified number of times.
|
|
repTokenRE = regexp.MustCompile(`^\<(.+)\>\{([0-9]+)\}$`)
|
|
|
|
// repRawRE is a regular expression used to parse short form scripts for raw
|
|
// data that is to be repeated a specified number of times.
|
|
repRawRE = regexp.MustCompile(`^(0[xX][0-9a-fA-F]+)\{([0-9]+)\}$`)
|
|
|
|
// repQuoteRE is a regular expression used to parse short form scripts for
|
|
// quoted data that is to be repeated a specified number of times.
|
|
repQuoteRE = regexp.MustCompile(`^'(.*)'\{([0-9]+)\}$`)
|
|
)
|
|
|
|
// shortFormOps holds a map of opcode names to values for use in short form
|
|
// parsing. It is declared here so it only needs to be created once.
|
|
var shortFormOps map[string]byte
|
|
|
|
// parseHex parses a hex string token into raw bytes.
|
|
func parseHex(tok string) ([]byte, error) {
|
|
if !strings.HasPrefix(tok, "0x") {
|
|
return nil, errors.New("not a hex number")
|
|
}
|
|
return hex.DecodeString(tok[2:])
|
|
}
|
|
|
|
// parseShortFormV0 parses a version 0 script from a human-readable format that
|
|
// allows for convenient testing into the associated raw script bytes.
|
|
//
|
|
// The format used is as follows:
|
|
// - Opcodes other than the push opcodes and unknown are present as either
|
|
// OP_NAME or just NAME
|
|
// - Plain numbers are made into push operations
|
|
// - Numbers beginning with 0x are inserted into the []byte without
|
|
// modification (so 0x14 is OP_DATA_20)
|
|
// - Numbers beginning with 0x which have a suffix which consists of a number
|
|
// in braces (e.g. 0x6161{10}) repeat the raw bytes the specified number of
|
|
// times and are inserted without modification
|
|
// - Single quoted strings are pushed as data
|
|
// - Single quoted strings that have a suffix which consists of a number in
|
|
// braces (e.g. 'b'{10}) repeat the data the specified number of times and
|
|
// are pushed as a single data push
|
|
// - Tokens inside of angular brackets with a suffix which consists of a
|
|
// number in braces (e.g. <0 0 CHECKMULTSIG>{5}) is parsed as if the tokens
|
|
// inside the angular brackets were manually repeated the specified number
|
|
// of times
|
|
// - Anything else is an error
|
|
func parseShortFormV0(script string) ([]byte, error) {
|
|
// Only create the short form opcode map once.
|
|
if shortFormOps == nil {
|
|
ops := make(map[string]byte)
|
|
for opcodeName, opcodeValue := range OpcodeByName {
|
|
if strings.Contains(opcodeName, "OP_UNKNOWN") {
|
|
continue
|
|
}
|
|
ops[opcodeName] = opcodeValue
|
|
|
|
// The opcodes named OP_# can't have the OP_ prefix stripped or they
|
|
// would conflict with the plain numbers. Also, since OP_FALSE and
|
|
// OP_TRUE are aliases for the OP_0, and OP_1, respectively, they
|
|
// have the same value, so detect those by name and allow them.
|
|
if (opcodeName == "OP_FALSE" || opcodeName == "OP_TRUE") ||
|
|
(opcodeValue != OP_0 && (opcodeValue < OP_1 ||
|
|
opcodeValue > OP_16)) {
|
|
|
|
ops[strings.TrimPrefix(opcodeName, "OP_")] = opcodeValue
|
|
}
|
|
}
|
|
shortFormOps = ops
|
|
}
|
|
|
|
builder := NewScriptBuilder()
|
|
|
|
var handleToken func(tok string) error
|
|
handleToken = func(tok string) error {
|
|
// Multiple repeated tokens.
|
|
if m := repTokenRE.FindStringSubmatch(tok); m != nil {
|
|
count, err := strconv.ParseInt(m[2], 10, 32)
|
|
if err != nil {
|
|
return fmt.Errorf("bad token %q", tok)
|
|
}
|
|
tokens := tokenRE.FindAllStringSubmatch(m[1], -1)
|
|
for i := 0; i < int(count); i++ {
|
|
for _, t := range tokens {
|
|
if err := handleToken(t[0]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Plain number.
|
|
if num, err := strconv.ParseInt(tok, 10, 64); err == nil {
|
|
builder.AddInt64(num)
|
|
return nil
|
|
}
|
|
|
|
// Raw data.
|
|
if bts, err := parseHex(tok); err == nil {
|
|
// Use the unchecked variant since the test code intentionally
|
|
// creates scripts that are too large and would cause the builder to
|
|
// error otherwise.
|
|
builder.AddOpsUnchecked(bts)
|
|
return nil
|
|
}
|
|
|
|
// Repeated raw bytes.
|
|
if m := repRawRE.FindStringSubmatch(tok); m != nil {
|
|
bts, err := parseHex(m[1])
|
|
if err != nil {
|
|
return fmt.Errorf("bad token %q", tok)
|
|
}
|
|
count, err := strconv.ParseInt(m[2], 10, 32)
|
|
if err != nil {
|
|
return fmt.Errorf("bad token %q", tok)
|
|
}
|
|
|
|
// Use the unchecked variant since the test code intentionally
|
|
// creates scripts that are too large and would cause the builder to
|
|
// error otherwise.
|
|
bts = bytes.Repeat(bts, int(count))
|
|
builder.AddOpsUnchecked(bts)
|
|
return nil
|
|
}
|
|
|
|
// Quoted data.
|
|
if len(tok) >= 2 && tok[0] == '\'' && tok[len(tok)-1] == '\'' {
|
|
builder.AddDataUnchecked([]byte(tok[1 : len(tok)-1]))
|
|
return nil
|
|
}
|
|
|
|
// Repeated quoted data.
|
|
if m := repQuoteRE.FindStringSubmatch(tok); m != nil {
|
|
count, err := strconv.ParseInt(m[2], 10, 32)
|
|
if err != nil {
|
|
return fmt.Errorf("bad token %q", tok)
|
|
}
|
|
data := strings.Repeat(m[1], int(count))
|
|
builder.AddDataUnchecked([]byte(data))
|
|
return nil
|
|
}
|
|
|
|
// Named opcode.
|
|
if opcode, ok := shortFormOps[tok]; ok {
|
|
builder.AddOp(opcode)
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("bad token %q", tok)
|
|
}
|
|
|
|
for _, tokens := range tokenRE.FindAllStringSubmatch(script, -1) {
|
|
if err := handleToken(tokens[0]); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return builder.Script()
|
|
}
|
|
|
|
// parseShortForm parses a script from a human-readable format for the given
|
|
// script version that allows for convenient testing into the associated raw
|
|
// script bytes.
|
|
//
|
|
// See the associated version-specific short form parsing function for each
|
|
// version for details regarding the format since it may or may not differ
|
|
// between script version.
|
|
func parseShortForm(scriptVersion uint16, script string) ([]byte, error) {
|
|
switch scriptVersion {
|
|
case 0:
|
|
return parseShortFormV0(script)
|
|
}
|
|
|
|
str := fmt.Sprintf("parsing short form for version %d scripts is not "+
|
|
"supported", scriptVersion)
|
|
return nil, scriptError(ErrUnsupportedScriptVersion, str)
|
|
}
|
|
|
|
// mustParseShortFormV0 parses the passed version 0 short form script and
|
|
// returns the resulting bytes. It panics if an error occurs. This is only
|
|
// used in the tests as a helper since the only way it can fail is if there is
|
|
// an error in the test source code.
|
|
func mustParseShortFormV0(script string) []byte {
|
|
s, err := parseShortFormV0(script)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("invalid short form script in test source: err %v, "+
|
|
"script: %s", err, script))
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// mustParseShortForm parses the passed short form script and returns the
|
|
// resulting bytes. It panics if an error occurs. This is only used in the
|
|
// tests as a helper since the only way it can fail is if there is an error in
|
|
// the test source code.
|
|
func mustParseShortForm(scriptVersion uint16, script string) []byte {
|
|
s, err := parseShortForm(scriptVersion, script)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("invalid short form script in test source: err %v, "+
|
|
"script: %s", err, script))
|
|
}
|
|
|
|
return s
|
|
}
|