// 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/consensusserialization" "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) ([]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 != 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, scriptPubKey []byte) *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: *consensusserialization.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) 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 } scriptPubKey, err := parseShortForm(scriptPubKeyStr) if err != nil { t.Errorf("%s: can't parse public key script: %v", name, err) continue } // 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) }