dcrd/txscript/stdscript/scriptshortform_test.go
Dave Collins 7d97fd5ff8
stdscript: Move from internal/staging to txscript.
This moves the new stdscript package from the internal staging area to
the txscript module and updates the relevant paths and package README.md
accordingly.
2021-11-18 12:29:53 -06:00

216 lines
7.0 KiB
Go

// Copyright (c) 2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package stdscript
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/decred/dcrd/txscript/v4"
)
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 txscript.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 != txscript.OP_0 && (opcodeValue < txscript.OP_1 ||
opcodeValue > txscript.OP_16)) {
ops[strings.TrimPrefix(opcodeName, "OP_")] = opcodeValue
}
}
shortFormOps = ops
}
builder := txscript.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, makeError(ErrUnsupportedScriptVersion, str)
}
// 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("invalid short form script in test source: err " + err.Error() +
", script: " + script)
}
return s
}