mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-05-23 23:36:56 +00:00

* '' * '' * '' * Changes genesis block version to 0. * a * a * All tests are done. * All tests passed for changed block version from int32 to uint16 * Adds validation of rejecting blocks with unknown versions. * Changes txn version from int32 to uint16. * . * Adds comments to exported functions. * Change functions name from ConvertFromRpcScriptPubKeyToRPCScriptPubKey to ConvertFromAppMsgRPCScriptPubKeyToRPCScriptPubKey and from ConvertFromRPCScriptPubKeyToRpcScriptPubKey to ConvertFromRPCScriptPubKeyToAppMsgRPCScriptPubKey * change comment to "ScriptPublicKey represents a Kaspad ScriptPublicKey" * delete part (tx.Version < 0) that cannot be exist on the if statement. * Revert protobuf version. * Fix a comment. * Fix a comment. * Rename a variable. * Rename a variable. * Remove a const. * Rename a type. * Rename a field. * Rename a field. * Remove commented-out code. * Remove dangerous nil case in DomainTransactionOutput.Clone(). * Remove a constant. * Fix a string. * Fix wrong totalScriptPubKeySize in transactionMassStandalonePart. * Remove a constant. * Remove an unused error. * Fix a serialization error. * Specify version types to be uint16 explicitly. * Use constants.ScriptPublicKeyVersion. * Fix a bad test. * Remove some whitespace. * Add a case to utxoEntry.Equal(). * Rename scriptPubKey to scriptPublicKey. * Remove a TODO. * Rename constants. * Rename a variable. * Add version to parseShortForm. Co-authored-by: tal <tal@daglabs.com> Co-authored-by: stasatdaglabs <stas@daglabs.com>
405 lines
12 KiB
Go
405 lines
12 KiB
Go
// Copyright (c) 2013-2017 The btcsuite developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package txscript
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
|
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
|
|
|
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
|
"github.com/kaspanet/kaspad/infrastructure/logger"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// scriptTestName returns a descriptive test name for the given reference script
|
|
// test data.
|
|
func scriptTestName(test []interface{}) (string, error) {
|
|
// The test must consist of a signature script, public key script, flags,
|
|
// and expected error. Finally, it may optionally contain a comment.
|
|
if len(test) < 4 || len(test) > 5 {
|
|
return "", errors.Errorf("invalid test length %d", len(test))
|
|
}
|
|
|
|
// Use the comment for the test name if one is specified, otherwise,
|
|
// construct the name based on the signature script, public key script,
|
|
// and flags.
|
|
var name string
|
|
if len(test) == 5 {
|
|
name = fmt.Sprintf("test (%s)", test[4])
|
|
} else {
|
|
name = fmt.Sprintf("test ([%s, %s, %s])", test[0],
|
|
test[1], test[2])
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
// parse hex string into a []byte.
|
|
func parseHex(tok string) ([]byte, error) {
|
|
if !strings.HasPrefix(tok, "0x") {
|
|
return nil, errors.New("not a hex number")
|
|
}
|
|
return hex.DecodeString(tok[2:])
|
|
}
|
|
|
|
// 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
|
|
|
|
// parseShortForm parses a string into a script 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 as-is (so
|
|
// 0x14 is OP_DATA_20)
|
|
// - Single quoted strings are pushed as data
|
|
// - Anything else is an error
|
|
func parseShortForm(script string, version uint16) ([]byte, error) {
|
|
if version > constants.MaxScriptPublicKeyVersion {
|
|
return nil, errors.Errorf("unknown version %d (max: %d)",
|
|
version, constants.MaxScriptPublicKeyVersion)
|
|
}
|
|
|
|
// 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 != Op0 && (opcodeValue < Op1 ||
|
|
opcodeValue > Op16)) {
|
|
|
|
ops[strings.TrimPrefix(opcodeName, "OP_")] = opcodeValue
|
|
}
|
|
}
|
|
shortFormOps = ops
|
|
}
|
|
|
|
// Split only does one separator so convert all \n and tab into space.
|
|
script = strings.Replace(script, "\n", " ", -1)
|
|
script = strings.Replace(script, "\t", " ", -1)
|
|
tokens := strings.Split(script, " ")
|
|
builder := NewScriptBuilder()
|
|
|
|
for _, tok := range tokens {
|
|
if len(tok) == 0 {
|
|
continue
|
|
}
|
|
// if parses as a plain number
|
|
if num, err := strconv.ParseInt(tok, 10, 64); err == nil {
|
|
builder.AddInt64(num)
|
|
continue
|
|
} else if bts, err := parseHex(tok); err == nil {
|
|
// Concatenate the bytes manually since the test code
|
|
// intentionally creates scripts that are too large and
|
|
// would cause the builder to error otherwise.
|
|
if builder.err == nil {
|
|
builder.script = append(builder.script, bts...)
|
|
}
|
|
} else if len(tok) >= 2 &&
|
|
tok[0] == '\'' && tok[len(tok)-1] == '\'' {
|
|
builder.AddFullData([]byte(tok[1 : len(tok)-1]))
|
|
} else if opcode, ok := shortFormOps[tok]; ok {
|
|
builder.AddOp(opcode)
|
|
} else {
|
|
return nil, errors.Errorf("bad token %q", tok)
|
|
}
|
|
|
|
}
|
|
return builder.Script()
|
|
}
|
|
|
|
// parseScriptFlags parses the provided flags string from the format used in the
|
|
// reference tests into ScriptFlags suitable for use in the script engine.
|
|
func parseScriptFlags(flagStr string) (ScriptFlags, error) {
|
|
var flags ScriptFlags
|
|
|
|
sFlags := strings.Split(flagStr, ",")
|
|
for _, flag := range sFlags {
|
|
switch flag {
|
|
case "":
|
|
// Nothing.
|
|
case "DISCOURAGE_UPGRADABLE_NOPS":
|
|
flags |= ScriptDiscourageUpgradableNops
|
|
default:
|
|
return flags, errors.Errorf("invalid flag: %s", flag)
|
|
}
|
|
}
|
|
return flags, nil
|
|
}
|
|
|
|
// parseExpectedResult parses the provided expected result string into allowed
|
|
// script error codes. An error is returned if the expected result string is
|
|
// not supported.
|
|
func parseExpectedResult(expected string) ([]ErrorCode, error) {
|
|
switch expected {
|
|
case "OK":
|
|
return nil, nil
|
|
case "UNKNOWN_ERROR":
|
|
return []ErrorCode{ErrNumberTooBig, ErrMinimalData}, nil
|
|
case "PUBKEYFORMAT":
|
|
return []ErrorCode{ErrPubKeyFormat}, nil
|
|
case "EVAL_FALSE":
|
|
return []ErrorCode{ErrEvalFalse, ErrEmptyStack}, nil
|
|
case "EMPTY_STACK":
|
|
return []ErrorCode{ErrEmptyStack}, nil
|
|
case "EQUALVERIFY":
|
|
return []ErrorCode{ErrEqualVerify}, nil
|
|
case "NULLFAIL":
|
|
return []ErrorCode{ErrNullFail}, nil
|
|
case "SIG_HIGH_S":
|
|
return []ErrorCode{ErrSigHighS}, nil
|
|
case "SIG_HASHTYPE":
|
|
return []ErrorCode{ErrInvalidSigHashType}, nil
|
|
case "SIG_PUSHONLY":
|
|
return []ErrorCode{ErrNotPushOnly}, nil
|
|
case "CLEANSTACK":
|
|
return []ErrorCode{ErrCleanStack}, nil
|
|
case "BAD_OPCODE":
|
|
return []ErrorCode{ErrReservedOpcode, ErrMalformedPush}, nil
|
|
case "UNBALANCED_CONDITIONAL":
|
|
return []ErrorCode{ErrUnbalancedConditional,
|
|
ErrInvalidStackOperation}, nil
|
|
case "OP_RETURN":
|
|
return []ErrorCode{ErrEarlyReturn}, nil
|
|
case "VERIFY":
|
|
return []ErrorCode{ErrVerify}, nil
|
|
case "INVALID_STACK_OPERATION", "INVALID_ALTSTACK_OPERATION":
|
|
return []ErrorCode{ErrInvalidStackOperation}, nil
|
|
case "DISABLED_OPCODE":
|
|
return []ErrorCode{ErrDisabledOpcode}, nil
|
|
case "DISCOURAGE_UPGRADABLE_NOPS":
|
|
return []ErrorCode{ErrDiscourageUpgradableNOPs}, nil
|
|
case "PUSH_SIZE":
|
|
return []ErrorCode{ErrElementTooBig}, nil
|
|
case "OP_COUNT":
|
|
return []ErrorCode{ErrTooManyOperations}, nil
|
|
case "STACK_SIZE":
|
|
return []ErrorCode{ErrStackOverflow}, nil
|
|
case "SCRIPT_SIZE":
|
|
return []ErrorCode{ErrScriptTooBig}, nil
|
|
case "PUBKEY_COUNT":
|
|
return []ErrorCode{ErrInvalidPubKeyCount}, nil
|
|
case "SIG_COUNT":
|
|
return []ErrorCode{ErrInvalidSignatureCount}, nil
|
|
case "MINIMALDATA":
|
|
return []ErrorCode{ErrMinimalData}, nil
|
|
case "NEGATIVE_LOCKTIME":
|
|
return []ErrorCode{ErrNegativeLockTime}, nil
|
|
case "UNSATISFIED_LOCKTIME":
|
|
return []ErrorCode{ErrUnsatisfiedLockTime}, nil
|
|
case "MINIMALIF":
|
|
return []ErrorCode{ErrMinimalIf}, nil
|
|
}
|
|
|
|
return nil, errors.Errorf("unrecognized expected result in test data: %v",
|
|
expected)
|
|
}
|
|
|
|
// createSpendTx generates a basic spending transaction given the passed
|
|
// signature and public key scripts.
|
|
func createSpendingTx(sigScript []byte, scriptPubKey *externalapi.ScriptPublicKey) *externalapi.DomainTransaction {
|
|
outpoint := externalapi.DomainOutpoint{
|
|
TransactionID: externalapi.DomainTransactionID{},
|
|
Index: ^uint32(0),
|
|
}
|
|
input := &externalapi.DomainTransactionInput{
|
|
PreviousOutpoint: outpoint,
|
|
SignatureScript: []byte{Op0, Op0},
|
|
Sequence: constants.MaxTxInSequenceNum,
|
|
}
|
|
output := &externalapi.DomainTransactionOutput{Value: 0, ScriptPublicKey: scriptPubKey}
|
|
coinbaseTx := &externalapi.DomainTransaction{
|
|
Version: 1,
|
|
Inputs: []*externalapi.DomainTransactionInput{input},
|
|
Outputs: []*externalapi.DomainTransactionOutput{output},
|
|
}
|
|
|
|
outpoint = externalapi.DomainOutpoint{
|
|
TransactionID: *consensushashing.TransactionID(coinbaseTx),
|
|
Index: 0,
|
|
}
|
|
input = &externalapi.DomainTransactionInput{
|
|
PreviousOutpoint: outpoint,
|
|
SignatureScript: sigScript,
|
|
Sequence: constants.MaxTxInSequenceNum,
|
|
}
|
|
output = &externalapi.DomainTransactionOutput{Value: 0, ScriptPublicKey: nil}
|
|
spendingTx := &externalapi.DomainTransaction{
|
|
Version: 1,
|
|
Inputs: []*externalapi.DomainTransactionInput{input},
|
|
Outputs: []*externalapi.DomainTransactionOutput{output},
|
|
}
|
|
|
|
return spendingTx
|
|
}
|
|
|
|
// testScripts ensures all of the passed script tests execute with the expected
|
|
// results with or without using a signature cache, as specified by the
|
|
// parameter.
|
|
func testScripts(t *testing.T, tests [][]interface{}, useSigCache bool) {
|
|
// Create a signature cache to use only if requested.
|
|
var sigCache *SigCache
|
|
if useSigCache {
|
|
sigCache = NewSigCache(10)
|
|
}
|
|
|
|
for i, test := range tests {
|
|
// "Format is: [[wit..., amount]?, scriptSig, scriptPubKey,
|
|
// flags, expected_scripterror, ... comments]"
|
|
|
|
// Skip single line comments.
|
|
if len(test) == 1 {
|
|
continue
|
|
}
|
|
|
|
// Construct a name for the test based on the comment and test
|
|
// data.
|
|
name, err := scriptTestName(test)
|
|
if err != nil {
|
|
t.Errorf("TestScripts: invalid test #%d: %v", i, err)
|
|
continue
|
|
}
|
|
|
|
// Extract and parse the signature script from the test fields.
|
|
scriptSigStr, ok := test[0].(string)
|
|
if !ok {
|
|
t.Errorf("%s: signature script is not a string", name)
|
|
continue
|
|
}
|
|
scriptSig, err := parseShortForm(scriptSigStr, 0)
|
|
if err != nil {
|
|
t.Errorf("%s: can't parse signature script: %v", name,
|
|
err)
|
|
continue
|
|
}
|
|
|
|
// Extract and parse the public key script from the test fields.
|
|
scriptPubKeyStr, ok := test[1].(string)
|
|
if !ok {
|
|
t.Errorf("%s: public key script is not a string", name)
|
|
continue
|
|
}
|
|
script, err := parseShortForm(scriptPubKeyStr, 0)
|
|
if err != nil {
|
|
t.Errorf("%s: can't parse public key script: %v", name,
|
|
err)
|
|
continue
|
|
}
|
|
scriptPubKey := &externalapi.ScriptPublicKey{Script: script, Version: 0}
|
|
|
|
// Extract and parse the script flags from the test fields.
|
|
flagsStr, ok := test[2].(string)
|
|
if !ok {
|
|
t.Errorf("%s: flags field is not a string", name)
|
|
continue
|
|
}
|
|
flags, err := parseScriptFlags(flagsStr)
|
|
if err != nil {
|
|
t.Errorf("%s: %v", name, err)
|
|
continue
|
|
}
|
|
|
|
// Extract and parse the expected result from the test fields.
|
|
//
|
|
// Convert the expected result string into the allowed script
|
|
// error codes. This is necessary because txscript is more
|
|
// fine grained with its errors than the reference test data, so
|
|
// some of the reference test data errors map to more than one
|
|
// possibility.
|
|
resultStr, ok := test[3].(string)
|
|
if !ok {
|
|
t.Errorf("%s: result field is not a string", name)
|
|
continue
|
|
}
|
|
allowedErrorCodes, err := parseExpectedResult(resultStr)
|
|
if err != nil {
|
|
t.Errorf("%s: %v", name, err)
|
|
continue
|
|
}
|
|
|
|
// Generate a transaction pair such that one spends from the
|
|
// other and the provided signature and public key scripts are
|
|
// used, then create a new engine to execute the scripts.
|
|
tx := createSpendingTx(scriptSig, scriptPubKey)
|
|
|
|
vm, err := NewEngine(scriptPubKey, tx, 0, flags, sigCache)
|
|
if err == nil {
|
|
err = vm.Execute()
|
|
}
|
|
|
|
// Ensure there were no errors when the expected result is OK.
|
|
if resultStr == "OK" {
|
|
if err != nil {
|
|
t.Errorf("%s failed to execute: %v", name, err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// At this point an error was expected so ensure the result of
|
|
// the execution matches it.
|
|
success := false
|
|
for _, code := range allowedErrorCodes {
|
|
if IsErrorCode(err, code) {
|
|
success = true
|
|
break
|
|
}
|
|
}
|
|
if !success {
|
|
var scriptErr Error
|
|
if ok := errors.As(err, &scriptErr); ok {
|
|
t.Errorf("%s: want error codes %v, got %v", name,
|
|
allowedErrorCodes, scriptErr.ErrorCode)
|
|
continue
|
|
}
|
|
t.Errorf("%s: want error codes %v, got err: %v (%T)",
|
|
name, allowedErrorCodes, err, err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestScripts ensures all of the tests in script_tests.json execute with the
|
|
// expected results as defined in the test data.
|
|
func TestScripts(t *testing.T) {
|
|
file, err := ioutil.ReadFile("data/script_tests.json")
|
|
if err != nil {
|
|
t.Fatalf("TestScripts: %v\n", err)
|
|
}
|
|
|
|
var tests [][]interface{}
|
|
err = json.Unmarshal(file, &tests)
|
|
if err != nil {
|
|
t.Fatalf("TestScripts couldn't Unmarshal: %v", err)
|
|
}
|
|
|
|
// Disable non-test logs
|
|
logLevel := log.Level()
|
|
log.SetLevel(logger.LevelOff)
|
|
defer log.SetLevel(logLevel)
|
|
|
|
// Run all script tests with and without the signature cache.
|
|
testScripts(t, tests, true)
|
|
testScripts(t, tests, false)
|
|
}
|