diff --git a/tests/data/txs/rct_bp_compact_tx_10312fd4.bin b/tests/data/txs/rct_bp_compact_tx_10312fd4.bin new file mode 100644 index 000000000..1131b48bd Binary files /dev/null and b/tests/data/txs/rct_bp_compact_tx_10312fd4.bin differ diff --git a/tests/data/txs/rct_bp_tx_a685d68e.bin b/tests/data/txs/rct_bp_tx_a685d68e.bin new file mode 100644 index 000000000..e4b48cf2f Binary files /dev/null and b/tests/data/txs/rct_bp_tx_a685d68e.bin differ diff --git a/tests/data/txs/rct_clsag_tx_200c3215.bin b/tests/data/txs/rct_clsag_tx_200c3215.bin new file mode 100644 index 000000000..3914e1652 Binary files /dev/null and b/tests/data/txs/rct_clsag_tx_200c3215.bin differ diff --git a/tests/data/txs/rct_full_tx_14056427.bin b/tests/data/txs/rct_full_tx_14056427.bin new file mode 100644 index 000000000..4eaaeecea Binary files /dev/null and b/tests/data/txs/rct_full_tx_14056427.bin differ diff --git a/tests/data/txs/rct_simple_tx_c69861bf.bin b/tests/data/txs/rct_simple_tx_c69861bf.bin new file mode 100644 index 000000000..bbbe53db2 Binary files /dev/null and b/tests/data/txs/rct_simple_tx_c69861bf.bin differ diff --git a/tests/data/txs/v1_coinbase_tx_bf4c0300.bin b/tests/data/txs/v1_coinbase_tx_bf4c0300.bin new file mode 100644 index 000000000..39744c32a Binary files /dev/null and b/tests/data/txs/v1_coinbase_tx_bf4c0300.bin differ diff --git a/tests/data/txs/v1_tx_hf3_effcceb9.bin b/tests/data/txs/v1_tx_hf3_effcceb9.bin new file mode 100644 index 000000000..55f7f1aa1 Binary files /dev/null and b/tests/data/txs/v1_tx_hf3_effcceb9.bin differ diff --git a/tests/data/txs/v2_coinbase_tx_7f88a52a.bin b/tests/data/txs/v2_coinbase_tx_7f88a52a.bin new file mode 100644 index 000000000..b91fbfcb4 Binary files /dev/null and b/tests/data/txs/v2_coinbase_tx_7f88a52a.bin differ diff --git a/tests/unit_tests/json_serialization.cpp b/tests/unit_tests/json_serialization.cpp index aa46b68dc..fc23ed7fc 100644 --- a/tests/unit_tests/json_serialization.cpp +++ b/tests/unit_tests/json_serialization.cpp @@ -12,9 +12,14 @@ #include "cryptonote_basic/cryptonote_basic.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "cryptonote_core/cryptonote_tx_utils.h" -#include "serialization/json_object.h" +#include "file_io_utils.h" #include "rpc/daemon_messages.h" +#include "serialization/json_object.h" +#include "string_tools.h" +#include "unit_tests_utils.h" +#undef MONERO_DEFAULT_LOG_CATEGORY +#define MONERO_DEFAULT_LOG_CATEGORY "cn" namespace test { @@ -107,6 +112,430 @@ namespace cryptonote::json::fromJsonValue(doc, out); return out; } + + bool compare_tx_to_json_tx(const cryptonote::transaction &tx, const std::string &tx_json) + { + using namespace epee::string_tools; // for pod_to_hex() + + const crypto::hash txid = get_transaction_hash(tx); + MINFO("Asserting validity of JSON representation of tx " << txid); + + rapidjson::Document tx_document; + tx_document.Parse(tx_json.data()); + CHECK_AND_ASSERT_MES(!tx_document.HasParseError(), false, "compare_tx_to_json_tx: failed to parse json"); + + // check existence of prefix members + CHECK_AND_ASSERT_MES(tx_document.HasMember("version") && tx_document.HasMember("unlock_time") && + tx_document.HasMember("vin") && tx_document.HasMember("vout") && tx_document.HasMember("extra"), + false, "compare_tx_to_json_tx: missing prefix members"); + + // version + CHECK_AND_ASSERT_MES(tx_document["version"].IsInt(), false, "compare_tx_to_json_tx: version isnt integer"); + CHECK_AND_ASSERT_MES(tx_document["version"].GetInt() == tx.version, false, "compare_tx_to_json_tx: wrong version"); + + // unlock_time + CHECK_AND_ASSERT_MES(tx_document["unlock_time"].IsUint64(), false, "compare_tx_to_json_tx: wrong unlock time"); + CHECK_AND_ASSERT_MES(tx_document["unlock_time"].GetUint64() == tx.unlock_time, false, "compare_tx_to_json_tx: wrong unlock time"); + + // extra + CHECK_AND_ASSERT_MES(tx_document["extra"].IsArray(), false, "compare_tx_to_json_tx: extra is not string"); + CHECK_AND_ASSERT_MES(tx_document["extra"].Size() == tx.extra.size(), false, "compare_tx_to_json_tx: wrong extra size"); + for (size_t i = 0; i < tx.extra.size(); ++i) + { + CHECK_AND_ASSERT_MES(tx_document["extra"][i].IsInt(), false, "compare_tx_to_json_tx: extra array contains non-int"); + CHECK_AND_ASSERT_MES(tx_document["extra"][i].GetInt() == tx.extra[i], false, "compare_tx_to_json_tx: extra array contains wrong int"); + } + + // vin + const size_t num_inputs = tx.vin.size(); + size_t ring_size = 0; + CHECK_AND_ASSERT_MES(tx_document["vin"].IsArray(), false, "compare_tx_to_json_tx: vin is not array"); + CHECK_AND_ASSERT_MES(tx_document["vin"].Size() == tx.vin.size(), false, "compare_tx_to_json_tx: vin wrong size"); + for (size_t i = 0; i < tx_document["vin"].Size(); i++) + { + CHECK_AND_ASSERT_MES(tx_document["vin"][i].IsObject(), false, "compare_tx_to_json_tx: txin not object"); + if (tx.vin[i].type() == typeid(cryptonote::txin_to_key)) + { + const cryptonote::txin_to_key &k = boost::get(tx.vin[i]); + ring_size = k.key_offsets.size(); + CHECK_AND_ASSERT_MES(tx_document["vin"][i].HasMember("key"), false, "compare_tx_to_json_tx: txin wrong type"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["key"].IsObject(), false, "compare_tx_to_json_tx: txin is not object"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["key"].HasMember("amount"), false, "compare_tx_to_json_tx: txin does not have amount"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["key"]["amount"].IsUint64(), false, "compare_tx_to_json_tx: txin amount is not int"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["key"]["amount"].GetUint64() == k.amount, false, "compare_tx_to_json_tx: txin wrong amount"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["key"].HasMember("k_image"), false, "compare_tx_to_json_tx: txin does not have k_image"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["key"]["k_image"].IsString(), false, "compare_tx_to_json_tx: txin kimage is not string"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["key"]["k_image"].GetString() == pod_to_hex(k.k_image), false, "compare_tx_to_json_tx: txin wrong k_image"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["key"].HasMember("key_offsets"), false, "compare_tx_to_json_tx: txin does not have key_offsets"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["key"]["key_offsets"].IsArray(), false, "compare_tx_to_json_tx: txin key_offsets is not array"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["key"]["key_offsets"].Size() == k.key_offsets.size(), false, "compare_tx_to_json_tx: txin wrong key_offsets size"); + for (size_t o = 0; o < k.key_offsets.size(); ++o) + { + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["key"]["key_offsets"][o].IsInt(), false, "compare_tx_to_json_tx: txin key offset is not int"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["key"]["key_offsets"][o].GetInt() == k.key_offsets[o], false, "compare_tx_to_json_tx: wrong key_offsets entry in txin"); + } + } + else if (tx.vin[i].type() == typeid(cryptonote::txin_gen)) + { + const cryptonote::txin_gen &g = boost::get(tx.vin[i]); + CHECK_AND_ASSERT_MES(tx_document["vin"][i].HasMember("gen"), false, "compare_tx_to_json_tx: txin wrong type"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["gen"].IsObject(), false, "compare_tx_to_json_tx: txin is not object"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["gen"].HasMember("height"), false, "compare_tx_to_json_tx: txin does not have height"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["gen"]["height"].IsInt(), false, "compare_tx_to_json_tx: txin height is not int"); + CHECK_AND_ASSERT_MES(tx_document["vin"][i]["gen"]["height"].GetInt() == g.height, false, "compare_tx_to_json_tx: txin wrong amount"); + } + else + { + CHECK_AND_ASSERT_MES(false, false, "compare_tx_to_json_tx: can't check non to_key and gen inputs"); + } + } + + // vout + CHECK_AND_ASSERT_MES(tx_document["vout"].IsArray(), false, "compare_tx_to_json_tx: vout is not array"); + CHECK_AND_ASSERT_MES(tx_document["vout"].Size() == tx.vout.size(), false, "compare_tx_to_json_tx: vout wrong size"); + for (size_t i = 0; i < tx_document["vout"].Size(); i++) + { + CHECK_AND_ASSERT_MES(tx_document["vout"][i].IsObject(), false, "compare_tx_to_json_tx: txout not object"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i].HasMember("amount"), false, "compare_tx_to_json_tx: txout does not have amount"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["amount"].IsUint64(), false, "compare_tx_to_json_tx: txout amount is not int"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["amount"].GetUint64() == tx.vout[i].amount, false, "compare_tx_to_json_tx: txout wrong amount"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i].HasMember("target"), false, "compare_tx_to_json_tx: txout has no target"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["target"].IsObject(), false, "compare_tx_to_json_tx: txout target is not an object"); + if (tx.vout[i].target.type() == typeid(cryptonote::txout_to_key)) + { + const cryptonote::txout_to_key &k = boost::get(tx.vout[i].target); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["target"].HasMember("key"), false, "compare_tx_to_json_tx: txout target wrong type"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["target"]["key"].IsString(), false, "compare_tx_to_json_tx: txout target onetime-address not string"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["target"]["key"].GetString() == pod_to_hex(k.key), false, "compare_tx_to_json_tx: txout target wrong onetime-address"); + } + else if (tx.vout[i].target.type() == typeid(cryptonote::txout_to_tagged_key)) + { + const cryptonote::txout_to_tagged_key &k = boost::get(tx.vout[i].target); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["target"].HasMember("tagged_key"), false, "compare_tx_to_json_tx: txout target wrong type"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["target"]["tagged_key"].IsObject(), false, "compare_tx_to_json_tx: txout target tagged key not object"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["target"]["tagged_key"].HasMember("key"), false, "compare_tx_to_json_tx: txout target tagged key does not have onetime-address"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["target"]["tagged_key"]["key"].IsString(), false, "compare_tx_to_json_tx: txout target tagged key onetime-address not string"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["target"]["tagged_key"]["key"].GetString() == pod_to_hex(k.key), false, "compare_tx_to_json_tx: txout target tagged key wrong onetime-address"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["target"]["tagged_key"].HasMember("view_tag"), false, "compare_tx_to_json_tx: txout target tagged key does not have view tag"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["target"]["tagged_key"]["view_tag"].IsString(), false, "compare_tx_to_json_tx: txout target tagged key view_tag not string"); + CHECK_AND_ASSERT_MES(tx_document["vout"][i]["target"]["tagged_key"]["view_tag"].GetString() == pod_to_hex(k.view_tag), false, "compare_tx_to_json_tx: txout target tagged key wrong view_tag"); + } + else + { + CHECK_AND_ASSERT_MES(false, false, "compare_tx_to_json_tx: can't check non to_key and to_tagged_key outputs"); + } + } + + if (tx.version == 1) + { + // signatures + CHECK_AND_ASSERT_MES(tx_document.HasMember("signatures"), false, "compare_tx_to_json_tx: missing signatures"); + CHECK_AND_ASSERT_MES(tx_document["signatures"].IsArray(), false, "compare_tx_to_json_tx: signatures is not array"); + + if (!cryptonote::is_coinbase(tx)) + { + CHECK_AND_ASSERT_MES(tx_document["signatures"].Size() == tx.signatures.size(), false, "compare_tx_to_json_tx: signatures wrong size"); + for (size_t i = 0; i < tx.signatures.size(); ++i) + { + CHECK_AND_ASSERT_MES(tx_document["signatures"][i].IsString(), false, "compare_tx_to_json_tx: signatures element not string"); + const std::string sig_hexbuffer{epee::to_hex::string({reinterpret_cast(tx.signatures[i].data()), tx.signatures[i].size() * sizeof(crypto::signature)})}; + CHECK_AND_ASSERT_MES(tx_document["signatures"][i].GetString() == sig_hexbuffer, false, "compare_tx_to_json_tx: signatures element wrong"); + } + } + else // coinbase + { + CHECK_AND_ASSERT_MES(tx_document["signatures"].Size() == 0, false, "compare_tx_to_json_tx: coinbase signatures not size 0"); + } + } + else // v2 tx + { + // rct_signatures + CHECK_AND_ASSERT_MES(tx_document.HasMember("rct_signatures"), false, "compare_tx_to_json_tx: missing rct_signatures"); + const rapidjson::Value &jrv = tx_document["rct_signatures"]; + CHECK_AND_ASSERT_MES(jrv.IsObject(), false, "compare_tx_to_json_tx: rct_signatures is not object"); + + // type + CHECK_AND_ASSERT_MES(jrv.HasMember("type"), false, "compare_tx_to_json_tx: rct_signatures has no type"); + CHECK_AND_ASSERT_MES(jrv["type"].IsUint(), false, "compare_tx_to_json_tx: rct_signatures type is not int"); + CHECK_AND_ASSERT_MES(jrv["type"].GetUint() == tx.rct_signatures.type, false, "compare_tx_to_json_tx: rct_signatures wrong type"); + + if (!cryptonote::is_coinbase(tx)) + { + // txnFee + CHECK_AND_ASSERT_MES(jrv.HasMember("txnFee"), false, "compare_tx_to_json_tx: rct_signatures has no fee"); + CHECK_AND_ASSERT_MES(jrv["txnFee"].IsUint64(), false, "compare_tx_to_json_tx: rct_signatures type is not uint64"); + CHECK_AND_ASSERT_MES(jrv["txnFee"].GetUint64() == tx.rct_signatures.txnFee, false, "compare_tx_to_json_tx: rct_signatures wrong fee"); + + // outPk + CHECK_AND_ASSERT_MES(jrv.HasMember("outPk"), false, "compare_tx_to_json_tx: rct_signatures has no outPk"); + CHECK_AND_ASSERT_MES(jrv["outPk"].IsArray(), false, "compare_tx_to_json_tx: rct_signatures outPk is not array"); + CHECK_AND_ASSERT_MES(jrv["outPk"].Size() == tx.rct_signatures.outPk.size(), false, "compare_tx_to_json_tx: rct_signatures wrong outPk size"); + for (size_t i = 0; i < tx.rct_signatures.outPk.size(); ++i) + { + CHECK_AND_ASSERT_MES(jrv["outPk"][i].IsString(), false, "compare_tx_to_json_tx: rct_signatures outPk entry not string"); + CHECK_AND_ASSERT_MES(jrv["outPk"][i].GetString() == pod_to_hex(tx.rct_signatures.outPk[i].mask), false, "compare_tx_to_json_tx: rct_signatures outPk wrong mask"); + } + + // ecdhInfo + const bool short_ecdh_info = tx.rct_signatures.type >= rct::RCTTypeBulletproof2; + const size_t ecdh_amount_hexlength = 2 * (short_ecdh_info ? 8 : 32); + CHECK_AND_ASSERT_MES(jrv.HasMember("ecdhInfo"), false, "compare_tx_to_json_tx: rct_signatures has no ecdhInfo"); + CHECK_AND_ASSERT_MES(jrv["ecdhInfo"].IsArray(), false, "compare_tx_to_json_tx: rct_signatures ecdhInfo is not array"); + CHECK_AND_ASSERT_MES(jrv["ecdhInfo"].Size() == tx.rct_signatures.ecdhInfo.size(), false, "compare_tx_to_json_tx: rct_signatures wrong ecdhInfo size"); + for (size_t i = 0; i < tx.rct_signatures.ecdhInfo.size(); ++i) + { + CHECK_AND_ASSERT_MES(jrv["ecdhInfo"][i].IsObject(), false, "compare_tx_to_json_tx: rct_signatures ecdhInfo entry not object"); + // amount (encrypted) + CHECK_AND_ASSERT_MES(jrv["ecdhInfo"][i].HasMember("amount"), false, "compare_tx_to_json_tx: rct_signatures ecdhInfo entry missing amount"); + CHECK_AND_ASSERT_MES(jrv["ecdhInfo"][i]["amount"].IsString(), false, "compare_tx_to_json_tx: rct_signatures ecdhInfo entry amount info not string"); + CHECK_AND_ASSERT_MES(jrv["ecdhInfo"][i]["amount"].GetStringLength() == ecdh_amount_hexlength, false, "compare_tx_to_json_tx: rct_signatures ecdhInfo entry amount string wrong size"); + const auto hex_amount{pod_to_hex(tx.rct_signatures.ecdhInfo[i].amount).substr(0, ecdh_amount_hexlength)}; + CHECK_AND_ASSERT_MES(jrv["ecdhInfo"][i]["amount"].GetString() == hex_amount, false, "compare_tx_to_json_tx: rct_signatures ecdhInfo entry wrong amount"); + if (!short_ecdh_info) + { + // mask (encrypted blinding factor) + CHECK_AND_ASSERT_MES(jrv["ecdhInfo"][i].HasMember("mask"), false, "compare_tx_to_json_tx: rct_signatures ecdhInfo entry missing mask"); + CHECK_AND_ASSERT_MES(jrv["ecdhInfo"][i]["mask"].IsString(), false, "compare_tx_to_json_tx: rct_signatures ecdhInfo entry mask not string"); + CHECK_AND_ASSERT_MES(jrv["ecdhInfo"][i]["mask"].GetString() == pod_to_hex(tx.rct_signatures.ecdhInfo[i].mask), false, "compare_tx_to_json_tx: rct_signatures ecdhInfo entry wrong mask"); + } + } + + // pseudoOuts (not pruned w/ v2 RingCT) + if (tx.rct_signatures.type == rct::RCTTypeSimple) + { + CHECK_AND_ASSERT_MES(tx.rct_signatures.pseudoOuts.size() == tx.vin.size(), false, "sanity check pseudoOuts size"); + CHECK_AND_ASSERT_MES(jrv.HasMember("pseudoOuts"), false, "compare_tx_to_json_tx: rct_signatures missing pseudoOuts"); + CHECK_AND_ASSERT_MES(jrv["pseudoOuts"].IsArray(), false, "compare_tx_to_json_tx: rct_signatures pseudoOuts is not array"); + CHECK_AND_ASSERT_MES(jrv["pseudoOuts"].Size() == tx.rct_signatures.pseudoOuts.size(), false, "compare_tx_to_json_tx: rct_signatures pseudoOuts wrong size"); + for (size_t i = 0; i < tx.rct_signatures.pseudoOuts.size(); ++i) + { + CHECK_AND_ASSERT_MES(jrv["pseudoOuts"][i].IsString(), false, "compare_tx_to_json_tx: rct_signatures pseudoOuts entry not string"); + CHECK_AND_ASSERT_MES(jrv["pseudoOuts"][i].GetString() == pod_to_hex(tx.rct_signatures.pseudoOuts[i]), false, "compare_tx_to_json_tx: rct_signatures pseudoOuts wrong entry"); + } + } + + // rctsig_prunable + CHECK_AND_ASSERT_MES(tx_document.HasMember("rctsig_prunable"), false, "compare_tx_to_json_tx: rct_signatures missing rctsig_prunable"); + const rapidjson::Value &jrvp = tx_document["rctsig_prunable"]; + const rct::rctSigPrunable &rvp = tx.rct_signatures.p; + + // range proofs + switch (tx.rct_signatures.type) + { + case rct::RCTTypeFull: + case rct::RCTTypeSimple: + // boro sig + CHECK_AND_ASSERT_MES(rvp.rangeSigs.size() == tx.vout.size(), false, "sanity check rangeSigs length"); + CHECK_AND_ASSERT_MES(jrvp.HasMember("rangeSigs"), false, "compare_tx_to_json_tx: rctsig_prunable missing rangeSigs"); + CHECK_AND_ASSERT_MES(jrvp["rangeSigs"].IsArray(), false, "compare_tx_to_json_tx: rctsig_prunable rangeSigs not array"); + CHECK_AND_ASSERT_MES(jrvp["rangeSigs"].Size() == rvp.rangeSigs.size(), false, "compare_tx_to_json_tx: rctsig_prunable rangeSigs wrong size"); + for (size_t i = 0; i < rvp.rangeSigs.size(); ++i) + { + CHECK_AND_ASSERT_MES(jrvp["rangeSigs"][i].IsObject(), false, "compare_tx_to_json_tx: rctsig_prunable rangeSig not object"); + // asig + CHECK_AND_ASSERT_MES(jrvp["rangeSigs"][i].HasMember("asig"), false, "compare_tx_to_json_tx: rctsig_prunable rangeSig missing asig"); + CHECK_AND_ASSERT_MES(jrvp["rangeSigs"][i]["asig"].IsString(), false, "compare_tx_to_json_tx: rctsig_prunable rangeSig asig is not string"); + CHECK_AND_ASSERT_MES(jrvp["rangeSigs"][i]["asig"].GetString() == pod_to_hex(rvp.rangeSigs[i].asig), false, "compare_tx_to_json_tx: rctsig_prunable rangeSig wrong asig"); + // Ci + CHECK_AND_ASSERT_MES(jrvp["rangeSigs"][i].HasMember("Ci"), false, "compare_tx_to_json_tx: rctsig_prunable rangeSig missing Ci"); + CHECK_AND_ASSERT_MES(jrvp["rangeSigs"][i]["Ci"].IsString(), false, "compare_tx_to_json_tx: rctsig_prunable rangeSig Ci is not string"); + CHECK_AND_ASSERT_MES(jrvp["rangeSigs"][i]["Ci"].GetString() == pod_to_hex(rvp.rangeSigs[i].Ci), false, "compare_tx_to_json_tx: rctsig_prunable rangeSig wrong Ci"); + } + break; + case rct::RCTTypeBulletproof: + case rct::RCTTypeBulletproof2: + case rct::RCTTypeCLSAG: + // bulletproof sig + CHECK_AND_ASSERT_MES(rvp.bulletproofs.size(), false, "sanity check bulletproofs length"); + CHECK_AND_ASSERT_MES(jrvp.HasMember("nbp"), false, "compare_tx_to_json_tx: rctsig_prunable missing nbp"); + CHECK_AND_ASSERT_MES(jrvp["nbp"].IsUint(), false, "compare_tx_to_json_tx: rctsig_prunable nbp not unsigned int"); + CHECK_AND_ASSERT_MES(jrvp["nbp"].GetUint() == rvp.bulletproofs.size(), false, "compare_tx_to_json_tx: rctsig_prunable nbp wrong value"); + CHECK_AND_ASSERT_MES(jrvp.HasMember("bp"), false, "compare_tx_to_json_tx: rctsig_prunable missing bp"); + CHECK_AND_ASSERT_MES(jrvp["bp"].IsArray(), false, "compare_tx_to_json_tx: rctsig_prunable bp not array"); + CHECK_AND_ASSERT_MES(jrvp["bp"].Size() == jrvp["nbp"].GetUint(), false, "compare_tx_to_json_tx: rctsig_prunable bp size not equal to nbp"); + for (size_t i = 0; i < jrvp["nbp"].GetUint(); ++i) + { + /* A, S, T1, T2, taux, mu, L[], R[], a, b, t */ + const rapidjson::Value &jbp = jrvp["bp"][i]; + const rct::Bulletproof &bp = rvp.bulletproofs[i]; + CHECK_AND_ASSERT_MES(jbp.IsObject(), false, "compare_tx_to_json_tx: bp entry not object"); + CHECK_AND_ASSERT_MES(jbp.HasMember("A") && jbp.HasMember("S") && jbp.HasMember("T1") && + jbp.HasMember("T2") && jbp.HasMember("taux") && jbp.HasMember("mu") && + jbp.HasMember("L") && jbp.HasMember("R") && jbp.HasMember("a") && + jbp.HasMember("b") && jbp.HasMember("t"), false, "compare_tx_to_json_tx: bp entry missing fields"); + CHECK_AND_ASSERT_MES(jbp["A"].IsString() && jbp["S"].IsString() && + jbp["T1"].IsString() && jbp["T2"].IsString() && jbp["taux"].IsString() && + jbp["mu"].IsString() && jbp["L"].IsArray() && jbp["R"].IsArray() && + jbp["a"].IsString() && jbp["b"].IsString() && jbp["t"].IsString(), false, + "compare_tx_to_json_tx: bp entry fields bad type"); + CHECK_AND_ASSERT_MES(jbp["A"].GetString() == pod_to_hex(bp.A), false, "compare_tx_to_json_tx: bp entry wrong A"); + CHECK_AND_ASSERT_MES(jbp["S"].GetString() == pod_to_hex(bp.S), false, "compare_tx_to_json_tx: bp entry wrong S"); + CHECK_AND_ASSERT_MES(jbp["T1"].GetString() == pod_to_hex(bp.T1), false, "compare_tx_to_json_tx: bp entry wrong T1"); + CHECK_AND_ASSERT_MES(jbp["T2"].GetString() == pod_to_hex(bp.T2), false, "compare_tx_to_json_tx: bp entry wrong T2"); + CHECK_AND_ASSERT_MES(jbp["taux"].GetString() == pod_to_hex(bp.taux), false, "compare_tx_to_json_tx: bp entry wrong taux"); + CHECK_AND_ASSERT_MES(jbp["mu"].GetString() == pod_to_hex(bp.mu), false, "compare_tx_to_json_tx: bp entry wrong mu"); + CHECK_AND_ASSERT_MES(jbp["a"].GetString() == pod_to_hex(bp.a), false, "compare_tx_to_json_tx: bp entry wrong a"); + CHECK_AND_ASSERT_MES(jbp["b"].GetString() == pod_to_hex(bp.b), false, "compare_tx_to_json_tx: bp entry wrong b"); + CHECK_AND_ASSERT_MES(jbp["t"].GetString() == pod_to_hex(bp.t), false, "compare_tx_to_json_tx: bp entry wrong t"); + CHECK_AND_ASSERT_MES(jbp["L"].Size() == bp.L.size(), false, "compare_tx_to_json_tx: bp entry L wrong size"); + for (size_t j = 0; j < bp.L.size(); ++j) + { + CHECK_AND_ASSERT_MES(jbp["L"][j].IsString(), false, "compare_tx_to_json_tx: bp entry L entry not string"); + CHECK_AND_ASSERT_MES(jbp["L"][j].GetString() == pod_to_hex(bp.L[j]), false, "compare_tx_to_json_tx: bp entry L entry wrong value"); + } + CHECK_AND_ASSERT_MES(jbp["R"].Size() == bp.R.size(), false, "compare_tx_to_json_tx: bp entry R wrong size"); + for (size_t j = 0; j < bp.R.size(); ++j) + { + CHECK_AND_ASSERT_MES(jbp["R"][j].IsString(), false, "compare_tx_to_json_tx: bp entry R entry not string"); + CHECK_AND_ASSERT_MES(jbp["R"][j].GetString() == pod_to_hex(bp.R[j]), false, "compare_tx_to_json_tx: bp entry R entry wrong value"); + } + } + break; + case rct::RCTTypeBulletproofPlus: + // bulletproof+ sig + CHECK_AND_ASSERT_MES(rvp.bulletproofs_plus.size(), false, "sanity check bulletproofs+ length"); + CHECK_AND_ASSERT_MES(jrvp.HasMember("nbp"), false, "compare_tx_to_json_tx: rctsig_prunable missing nbp"); + CHECK_AND_ASSERT_MES(jrvp["nbp"].IsUint(), false, "compare_tx_to_json_tx: rctsig_prunable nbp not unsigned int"); + CHECK_AND_ASSERT_MES(jrvp["nbp"].GetUint() == rvp.bulletproofs_plus.size(), false, "compare_tx_to_json_tx: rctsig_prunable nbp wrong value"); + CHECK_AND_ASSERT_MES(jrvp.HasMember("bpp"), false, "compare_tx_to_json_tx: rctsig_prunable missing bp"); + CHECK_AND_ASSERT_MES(jrvp["bpp"].IsArray(), false, "compare_tx_to_json_tx: rctsig_prunable bp not array"); + CHECK_AND_ASSERT_MES(jrvp["bpp"].Size() == jrvp["nbp"].GetUint(), false, "compare_tx_to_json_tx: rctsig_prunable bp size not equal to nbp"); + for (size_t i = 0; i < jrvp["nbp"].GetUint(); ++i) + { + /* A, A1, B, r1, s1, d1, L[], R[] */ + const rapidjson::Value &jbpp = jrvp["bpp"][i]; + const rct::BulletproofPlus &bpp = rvp.bulletproofs_plus[i]; + CHECK_AND_ASSERT_MES(jbpp.IsObject(), false, "compare_tx_to_json_tx: bpp entry not object"); + CHECK_AND_ASSERT_MES(jbpp.HasMember("A") && jbpp.HasMember("A1") && jbpp.HasMember("B") && + jbpp.HasMember("r1") && jbpp.HasMember("s1") && jbpp.HasMember("d1") && + jbpp.HasMember("L") && jbpp.HasMember("R"), false, "compare_tx_to_json_tx: bpp entry missing fields"); + CHECK_AND_ASSERT_MES(jbpp["A"].IsString() && jbpp["A1"].IsString() && + jbpp["B"].IsString() && jbpp["r1"].IsString() && jbpp["s1"].IsString() && + jbpp["d1"].IsString() && jbpp["L"].IsArray() && jbpp["R"].IsArray(), false, + "compare_tx_to_json_tx: bpp entry fields bad type"); + CHECK_AND_ASSERT_MES(jbpp["A"].GetString() == pod_to_hex(bpp.A), false, "compare_tx_to_json_tx: bpp entry wrong A"); + CHECK_AND_ASSERT_MES(jbpp["A1"].GetString() == pod_to_hex(bpp.A1), false, "compare_tx_to_json_tx: bpp entry wrong S"); + CHECK_AND_ASSERT_MES(jbpp["B"].GetString() == pod_to_hex(bpp.B), false, "compare_tx_to_json_tx: bpp entry wrong T1"); + CHECK_AND_ASSERT_MES(jbpp["r1"].GetString() == pod_to_hex(bpp.r1), false, "compare_tx_to_json_tx: bpp entry wrong T2"); + CHECK_AND_ASSERT_MES(jbpp["s1"].GetString() == pod_to_hex(bpp.s1), false, "compare_tx_to_json_tx: bpp entry wrong taux"); + CHECK_AND_ASSERT_MES(jbpp["d1"].GetString() == pod_to_hex(bpp.d1), false, "compare_tx_to_json_tx: bpp entry wrong mu"); + CHECK_AND_ASSERT_MES(jbpp["L"].Size() == bpp.L.size(), false, "compare_tx_to_json_tx: bpp entry L wrong size"); + for (size_t j = 0; j < bpp.L.size(); ++j) + { + CHECK_AND_ASSERT_MES(jbpp["L"][j].IsString(), false, "compare_tx_to_json_tx: bp entry L entry not string"); + CHECK_AND_ASSERT_MES(jbpp["L"][j].GetString() == pod_to_hex(bpp.L[j]), false, "compare_tx_to_json_tx: bpp entry L entry wrong value"); + } + CHECK_AND_ASSERT_MES(jbpp["R"].Size() == bpp.R.size(), false, "compare_tx_to_json_tx: bpp entry R wrong size"); + for (size_t j = 0; j < bpp.R.size(); ++j) + { + CHECK_AND_ASSERT_MES(jbpp["R"][j].IsString(), false, "compare_tx_to_json_tx: bpp entry R entry not string"); + CHECK_AND_ASSERT_MES(jbpp["R"][j].GetString() == pod_to_hex(bpp.R[j]), false, "compare_tx_to_json_tx: bpp entry R entry wrong value"); + } + } + break; + default: + MERROR("Unrecognized RCT Type"); + return false; + } + + // ring signatures + const bool is_full_rct = tx.rct_signatures.type == rct::RCTTypeFull; + const size_t num_mgs = is_full_rct ? 1 : num_inputs; + const size_t num_mg_cols = 1 + (is_full_rct ? num_inputs : 1); + switch (tx.rct_signatures.type) + { + case rct::RCTTypeFull: + case rct::RCTTypeSimple: + case rct::RCTTypeBulletproof: + case rct::RCTTypeBulletproof2: + // MLSAG sig + CHECK_AND_ASSERT_MES(rvp.MGs.size() == num_mgs, false, "sanity check tx has correct num MGs"); + CHECK_AND_ASSERT_MES(jrvp.HasMember("MGs"), false, "compare_tx_to_json_tx: rctsig_prunable missing MGs"); + CHECK_AND_ASSERT_MES(jrvp["MGs"].IsArray(), false, "compare_tx_to_json_tx: rctsig_prunable MGs not array"); + CHECK_AND_ASSERT_MES(jrvp["MGs"].Size() == num_mgs, false, "compare_tx_to_json_tx: rctsig_prunable MGs wrong size"); + for (size_t i = 0; i < num_mgs; ++i) + { + const rapidjson::Value &jmg = jrvp["MGs"][i]; + const rct::mgSig &mg = rvp.MGs[i]; + CHECK_AND_ASSERT_MES(jmg.IsObject(), false, "compare_tx_to_json_tx: rctsig_prunable MGs entry not object"); + CHECK_AND_ASSERT_MES(jmg.HasMember("ss") && jmg.HasMember("cc"), false, "compare_tx_to_json_tx: rctsig_prunable MGs entry missing fields"); + CHECK_AND_ASSERT_MES(jmg["cc"].IsString(), false, "compare_tx_to_json_tx: rctsig_prunable MGs cc not string"); + CHECK_AND_ASSERT_MES(jmg["cc"].GetString() == pod_to_hex(mg.cc), false, "compare_tx_to_json_tx: rctsig_prunable MGs cc wrong value"); + CHECK_AND_ASSERT_MES(jmg["ss"].IsArray(), false, "compare_tx_to_json_tx: rctsig_prunable MG ss not array"); + CHECK_AND_ASSERT_MES(jmg["ss"].Size() == ring_size, false, "compare_tx_to_json_tx: rctsig_prunable MG ss num rows not ring size"); + for (size_t j = 0; j < ring_size; ++j) + { + const rapidjson::Value &jrow = jmg["ss"][j]; + const rct::keyV &row = mg.ss[j]; + CHECK_AND_ASSERT_MES(jrow.IsArray(), false, "compare_tx_to_json_tx: rctsig_prunable MG ss row not array"); + CHECK_AND_ASSERT_MES(row.size() == num_mg_cols, false, "sanity check MG ss row size"); + CHECK_AND_ASSERT_MES(jrow.Size() == num_mg_cols, false, "compare_tx_to_json_tx: rctsig_prunable MG ss row wrong size"); + for (size_t k = 0; k < num_mg_cols; ++k) + { + CHECK_AND_ASSERT_MES(jrow[k].IsString(), false, "compare_tx_to_json_tx: rctsig_prunable MG ss scalar not string"); + CHECK_AND_ASSERT_MES(jrow[k].GetString() == pod_to_hex(row[k]), false, "compare_tx_to_json_tx: rctsig_prunable MG ss wrong scalar"); + } + } + } + break; + case rct::RCTTypeCLSAG: + case rct::RCTTypeBulletproofPlus: + // CLSAG sig + CHECK_AND_ASSERT_MES(rvp.CLSAGs.size() == num_inputs, false, "sanity check tx has correct num CLSAGs"); + CHECK_AND_ASSERT_MES(jrvp.HasMember("CLSAGs"), false, "compare_tx_to_json_tx: rctsig_prunable missing CLSAGs"); + CHECK_AND_ASSERT_MES(jrvp["CLSAGs"].IsArray(), false, "compare_tx_to_json_tx: rctsig_prunable CLSAGs not array"); + CHECK_AND_ASSERT_MES(jrvp["CLSAGs"].Size() == num_inputs, false, "compare_tx_to_json_tx: rctsig_prunable CLSAGs wrong size"); + for (size_t i = 0; i < num_inputs; ++i) + { + const rapidjson::Value &jclsag = jrvp["CLSAGs"][i]; + const rct::clsag &clsag = rvp.CLSAGs[i]; + CHECK_AND_ASSERT_MES(jclsag.IsObject(), false, "compare_tx_to_json_tx: rctsig_prunable CLSAG is not object"); + CHECK_AND_ASSERT_MES(jclsag.HasMember("s") && jclsag.HasMember("c1") && jclsag.HasMember("D"), + false, "compare_tx_to_json_tx: rctsig_prunable CLSAG missing fields"); + CHECK_AND_ASSERT_MES(jclsag["s"].IsArray() && jclsag["c1"].IsString() && jclsag["D"].IsString(), + false, "compare_tx_to_json_tx: rctsig_prunable CLSAG fields wrong type"); + CHECK_AND_ASSERT_MES(jclsag["c1"].GetString() == pod_to_hex(clsag.c1), + false, "compare_tx_to_json_tx: rctsig_prunable CLSAG wrong c1"); + CHECK_AND_ASSERT_MES(jclsag["D"].GetString() == pod_to_hex(clsag.D), + false, "compare_tx_to_json_tx: rctsig_prunable CLSAG wrong D"); + CHECK_AND_ASSERT_MES(jclsag["s"].Size() == ring_size, + false, "compare_tx_to_json_tx: rctsig_prunable CLSAG.s wrong size"); + CHECK_AND_ASSERT_MES(clsag.s.size() == ring_size, false, "sanity check CLSAG.s size"); + for (size_t j = 0; j < ring_size; ++j) + { + CHECK_AND_ASSERT_MES(jclsag["s"][j].IsString(), false, "compare_tx_to_json_tx: rctsig_prunable CLSAG c1 not string"); + CHECK_AND_ASSERT_MES(jclsag["s"][j].GetString() == pod_to_hex(clsag.s[j]), + false, "compare_tx_to_json_tx: rctsig_prunable CLSAG.s wrong key element"); + } + } + break; + default: + MERROR("Unrecognized RCT Type"); + return false; + } + } + } + + return true; + } + + bool jsonify_tx_from_file_and_parse_compare(const std::string &tx_fname) + { + const boost::filesystem::path tx_path = unit_test::data_dir / "txs" / tx_fname; + + std::string tx_blob; + CHECK_AND_ASSERT_THROW_MES(epee::file_io_utils::load_file_to_string(tx_path.string(), tx_blob), + "failed to load tx from file: " << tx_path.string()); + + cryptonote::transaction tx; + CHECK_AND_ASSERT_THROW_MES(cryptonote::parse_and_validate_tx_from_blob(tx_blob, tx), + "failed to parse tx from file: " << tx_path.string()); + + // Same JSONification used in /get_transactions RPC endpoint + const std::string tx_json{cryptonote::obj_to_json_str(tx)}; + + return compare_tx_to_json_tx(tx, tx_json); + } } // anonymous TEST(JsonSerialization, VectorBytes) @@ -247,3 +676,16 @@ TEST(JsonRpcSerialization, HandlerFromJson) cryptonote::rpc::GetHashesFast::Request request{}; EXPECT_THROW(request.fromJson(req_full.getMessage()), cryptonote::json::WRONG_TYPE); } + +TEST(JsonSerialization, jsonify_tx_from_file_and_parse_compare) +{ + EXPECT_TRUE(jsonify_tx_from_file_and_parse_compare("v1_coinbase_tx_bf4c0300.bin")); + EXPECT_TRUE(jsonify_tx_from_file_and_parse_compare("v1_tx_hf3_effcceb9.bin")); + EXPECT_TRUE(jsonify_tx_from_file_and_parse_compare("v2_coinbase_tx_7f88a52a.bin")); + EXPECT_TRUE(jsonify_tx_from_file_and_parse_compare("rct_full_tx_14056427.bin")); + EXPECT_TRUE(jsonify_tx_from_file_and_parse_compare("rct_simple_tx_c69861bf.bin")); + EXPECT_TRUE(jsonify_tx_from_file_and_parse_compare("rct_bp_tx_a685d68e.bin")); + EXPECT_TRUE(jsonify_tx_from_file_and_parse_compare("rct_bp_compact_tx_10312fd4.bin")); + EXPECT_TRUE(jsonify_tx_from_file_and_parse_compare("rct_clsag_tx_200c3215.bin")); + EXPECT_TRUE(jsonify_tx_from_file_and_parse_compare("bpp_tx_e89415.bin")); +}