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.
216 lines
7.0 KiB
Go
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
|
|
}
|