Data-drive script evaluation unit tests.
authorGavin Andresen <gavinandresen@gmail.com>
Tue, 17 Apr 2012 21:57:06 +0000 (17:57 -0400)
committerGavin Andresen <gavinandresen@gmail.com>
Sat, 21 Apr 2012 23:35:39 +0000 (19:35 -0400)
src/test/data/script_invalid.json [new file with mode: 0644]
src/test/data/script_valid.json [new file with mode: 0644]
src/test/script_tests.cpp

diff --git a/src/test/data/script_invalid.json b/src/test/data/script_invalid.json
new file mode 100644 (file)
index 0000000..c05d3ba
--- /dev/null
@@ -0,0 +1,25 @@
+[
+["", ""],
+["", "NOP"],
+["NOP", ""],
+["NOP","NOP"],
+["0 1","EQUAL"],
+["1 1 ADD", "0 EQUAL"],
+["11 1 ADD 12 SUB", "11 EQUAL"],
+
+["2147483648 0 ADD", "NOP", "arithmetic operands must be in range [-2^31...2^31] "],
+["-2147483648 0 ADD", "NOP", "arithmetic operands must be in range [-2^31...2^31] "],
+["2147483647 DUP ADD", "4294967294 NUMEQUAL", "NUMEQUAL must be in numeric range"],
+["0xaabbccddeeff NOT", "0 EQUAL", "NOT is an arithmetic operand"],
+
+["2 DUP MUL", "4 EQUAL", "disabled"],
+["2 DUP DIV", "1 EQUAL", "disabled"],
+["2 2MUL", "4 EQUAL", "disabled"],
+["2 2DIV", "1 EQUAL", "disabled"],
+["7 3 MOD", "1 EQUAL", "disabled"],
+["2 2 LSHIFT", "8 EQUAL", "disabled"],
+["2 1 RSHIFT", "1 EQUAL", "disabled"],
+
+
+["NOP1","NOP10"]
+]
diff --git a/src/test/data/script_valid.json b/src/test/data/script_valid.json
new file mode 100644 (file)
index 0000000..e7ad6d9
--- /dev/null
@@ -0,0 +1,78 @@
+[
+["2 -2 ADD", "0 EQUAL"],
+["2147483647 -2147483647 ADD", "0 EQUAL"],
+["-1 -1 ADD", "-2 EQUAL"],
+
+["1","NOP"],
+["0 0","EQUAL"],
+["1 1 ADD", "2 EQUAL"],
+["1 1ADD", "2 EQUAL"],
+["111 1SUB", "110 EQUAL"],
+["111 1 ADD 12 SUB", "100 EQUAL"],
+["0 ABS", "0 EQUAL"],
+["16 ABS", "16 EQUAL"],
+["-16 ABS", "-16 NEGATE EQUAL"],
+["0 NOT", "NOP"],
+["1 NOT", "0 EQUAL"],
+["11 NOT", "0 EQUAL"],
+["0 0NOTEQUAL", "0 EQUAL"],
+["1 0NOTEQUAL", "1 EQUAL"],
+["111 0NOTEQUAL", "1 EQUAL"],
+["-111 0NOTEQUAL", "1 EQUAL"],
+["1 1 BOOLAND", "NOP"],
+["1 0 BOOLAND", "NOT"],
+["0 1 BOOLAND", "NOT"],
+["0 0 BOOLAND", "NOT"],
+["16 17 BOOLAND", "NOP"],
+["1 1 BOOLOR", "NOP"],
+["1 0 BOOLOR", "NOP"],
+["0 1 BOOLOR", "NOP"],
+["0 0 BOOLOR", "NOT"],
+["16 17 BOOLOR", "NOP"],
+["11 10 1 ADD", "NUMEQUAL"],
+["11 10 1 ADD", "NUMEQUALVERIFY 1"],
+["11 10 1 ADD", "NUMNOTEQUAL NOT"],
+["111 10 1 ADD", "NUMNOTEQUAL"],
+["11 10", "LESSTHAN NOT"],
+["4 4", "LESSTHAN NOT"],
+["10 11", "LESSTHAN"],
+["-11 11", "LESSTHAN"],
+["-11 -10", "LESSTHAN"],
+["11 10", "GREATERTHAN"],
+["4 4", "GREATERTHAN NOT"],
+["10 11", "GREATERTHAN NOT"],
+["-11 11", "GREATERTHAN NOT"],
+["-11 -10", "GREATERTHAN NOT"],
+["11 10", "LESSTHANOREQUAL NOT"],
+["4 4", "LESSTHANOREQUAL"],
+["10 11", "LESSTHANOREQUAL"],
+["-11 11", "LESSTHANOREQUAL"],
+["-11 -10", "LESSTHANOREQUAL"],
+["11 10", "GREATERTHANOREQUAL"],
+["4 4", "GREATERTHANOREQUAL"],
+["10 11", "GREATERTHANOREQUAL NOT"],
+["-11 11", "GREATERTHANOREQUAL NOT"],
+["-11 -10", "GREATERTHANOREQUAL NOT"],
+["1 0 MIN", "0 NUMEQUAL"],
+["0 1 MIN", "0 NUMEQUAL"],
+["-1 0 MIN", "-1 NUMEQUAL"],
+["0 -2147483647 MIN", "-2147483647 NUMEQUAL"],
+["2147483647 0 MAX", "2147483647 NUMEQUAL"],
+["0 100 MAX", "100 NUMEQUAL"],
+["-100 0 MAX", "0 NUMEQUAL"],
+["0 -2147483647 MAX", "0 NUMEQUAL"],
+["0 0 1", "WITHIN"],
+["1 0 1", "WITHIN NOT"],
+["0 -2147483647 2147483647", "WITHIN"],
+["-1 -100 100", "WITHIN"],
+["11 -100 100", "WITHIN"],
+["-2147483647 -100 100", "WITHIN NOT"],
+["2147483647 -100 100", "WITHIN NOT"],
+
+["2147483647 2147483647 SUB", "0 EQUAL"],
+["2147483647 DUP ADD", "4294967294 EQUAL", ">32 bit EQUAL is valid"],
+["2147483647 NEGATE DUP ADD", "-4294967294 EQUAL"],
+
+
+["NOP","1"]
+]
index 493ea69..988bd24 100644 (file)
+#include <iostream>
+#include <fstream>
 #include <vector>
+#include <boost/algorithm/string/classification.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/split.hpp>
 #include <boost/test/unit_test.hpp>
 #include <boost/foreach.hpp>
+#include "json/json_spirit_reader_template.h"
+#include "json/json_spirit_writer_template.h"
+#include "json/json_spirit_utils.h"
 
 #include "main.h"
 #include "wallet.h"
 
 using namespace std;
+using namespace json_spirit;
+using namespace boost::algorithm;
+
 extern uint256 SignatureHash(CScript scriptCode, const CTransaction& txTo, unsigned int nIn, int nHashType);
 extern bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const CTransaction& txTo, unsigned int nIn,
                          bool fValidatePayToScriptHash, int nHashType);
 
+CScript
+ParseScript(string s)
+{
+    CScript result;
+
+    static map<string, opcodetype> mapOpNames;
+
+    if (mapOpNames.size() == 0)
+    {
+        for (int op = OP_NOP; op <= OP_NOP10; op++)
+        {
+            const char* name = GetOpName((opcodetype)op);
+            if (strcmp(name, "OP_UNKNOWN") == 0)
+                continue;
+            string strName(name);
+            mapOpNames[strName] = (opcodetype)op;
+            // Convenience: OP_ADD and just ADD are both recognized:
+            replace_first(strName, "OP_", "");
+            mapOpNames[strName] = (opcodetype)op;
+        }
+    }
+
+    vector<string> words;
+    split(words, s, is_any_of(" \t\n"), token_compress_on);
+
+    BOOST_FOREACH(string w, words)
+    {
+        if (all(w, is_digit()) ||
+            (starts_with(w, "-") && all(string(w.begin()+1, w.end()), is_digit())))
+        {
+            // Number
+            int64 n = atoi64(w);
+            result << n;
+        }
+        else if (starts_with(w, "0x") && IsHex(string(w.begin()+2, w.end())))
+        {
+            // Hex data:
+            result << ParseHex(string(w.begin()+2, w.end()));
+        }
+        else if (s.size() >= 2 && starts_with(w, "'") && ends_with(w, "'"))
+        {
+            // Single-quoted string, pushed as data:
+            std::vector<unsigned char> value(s.begin()+1, s.end()-1);
+            result << value;
+        }
+        else if (mapOpNames.count(w))
+        {
+            // opcode, e.g. OP_ADD or OP_1:
+            result << mapOpNames[w];
+        }
+        else
+        {
+            BOOST_ERROR("Parse error: " << s);
+            return CScript();
+        }                        
+    }
+
+    return result;
+}
+
+Array
+read_json(const std::string& filename)
+{
+    namespace fs = boost::filesystem;
+    fs::path testFile = fs::current_path() / "test" / "data" / filename;
+    if (!fs::exists(testFile))
+    {
+        fs::path testFile = fs::path(__FILE__).parent_path() / "data" / filename;
+    }
+
+    ifstream ifs(testFile.string().c_str(), ifstream::in);
+    Value v;
+    if (!read_stream(ifs, v))
+    {
+        BOOST_ERROR("Cound not find/open " << filename);
+        return Array();
+    }
+    if (v.type() != array_type)
+    {
+        BOOST_ERROR(filename << " does not contain a json array");
+        return Array();
+    }
+
+    return v.get_array();
+}
+
 BOOST_AUTO_TEST_SUITE(script_tests)
 
+BOOST_AUTO_TEST_CASE(script_valid)
+{
+    // Read tests from test/data/script_valid.json
+    // Format is an array of arrays
+    // Inner arrays are [ "scriptSig", "scriptPubKey" ]
+    // ... where scriptSig and scriptPubKey are stringified
+    // scripts.
+    Array tests = read_json("script_valid.json");
+
+    BOOST_FOREACH(Value& tv, tests)
+    {
+        Array test = tv.get_array();
+        string strTest = write_string(tv, false);
+        if (test.size() < 2) // Allow size > 2; extra stuff ignored (useful for comments)
+        {
+            BOOST_ERROR("Bad test: " << strTest);
+            continue;
+        }
+        string scriptSigString = test[0].get_str();
+        CScript scriptSig = ParseScript(scriptSigString);
+        string scriptPubKeyString = test[1].get_str();
+        CScript scriptPubKey = ParseScript(scriptPubKeyString);
+
+        CTransaction tx;
+        BOOST_CHECK_MESSAGE(VerifyScript(scriptSig, scriptPubKey, tx, 0, true, SIGHASH_NONE), strTest);
+    }
+}
+
+BOOST_AUTO_TEST_CASE(script_invalid)
+{
+    // Scripts that should evaluate as invalid
+    Array tests = read_json("script_invalid.json");
+
+    BOOST_FOREACH(Value& tv, tests)
+    {
+        Array test = tv.get_array();
+        string strTest = write_string(tv, false);
+        if (test.size() < 2) // Allow size > 2; extra stuff ignored (useful for comments)
+        {
+            BOOST_ERROR("Bad test: " << strTest);
+            continue;
+        }
+        string scriptSigString = test[0].get_str();
+        CScript scriptSig = ParseScript(scriptSigString);
+        string scriptPubKeyString = test[1].get_str();
+        CScript scriptPubKey = ParseScript(scriptPubKeyString);
+
+        CTransaction tx;
+        BOOST_CHECK_MESSAGE(!VerifyScript(scriptSig, scriptPubKey, tx, 0, true, SIGHASH_NONE), strTest);
+    }
+}
+
 BOOST_AUTO_TEST_CASE(script_PushData)
 {
     // Check that PUSHDATA1, PUSHDATA2, and PUSHDATA4 create the same value on